Add androidx.test variants of util libraries.

See go/jetpack-test-android-migration

Test: m cts
Bug: 127482512
Change-Id: If88bf99864ddaa769c754b6e910e4bb45a9fbb3b
Merged-In: Iaff9af3c774c7f4867cc1861dcf96d7b5c5e8ff4
diff --git a/common/device-side/util-axt/Android.mk b/common/device-side/util-axt/Android.mk
new file mode 100644
index 0000000..53b4a83
--- /dev/null
+++ b/common/device-side/util-axt/Android.mk
@@ -0,0 +1,41 @@
+# Copyright (C) 2014 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.
+
+LOCAL_PATH := $(call my-dir)
+
+include $(CLEAR_VARS)
+
+LOCAL_SRC_FILES := $(call all-java-files-under, src) \
+    $(call all-Iaidl-files-under, src)
+
+LOCAL_STATIC_JAVA_LIBRARIES := \
+    compatibility-common-util-devicesidelib \
+    androidx.test.rules \
+    ub-uiautomator \
+    mockito-target-minus-junit4 \
+    androidx.annotation_annotation
+
+LOCAL_JAVA_LIBRARIES := \
+    android.test.runner.stubs \
+    android.test.base.stubs
+
+LOCAL_MODULE_TAGS := optional
+
+LOCAL_MODULE := compatibility-device-util-axt
+
+LOCAL_SDK_VERSION := test_current
+
+include $(BUILD_STATIC_JAVA_LIBRARY)
+
+include $(call all-makefiles-under,$(LOCAL_PATH))
diff --git a/common/device-side/util-axt/src/com/android/compatibility/common/util/AmUtils.java b/common/device-side/util-axt/src/com/android/compatibility/common/util/AmUtils.java
new file mode 100644
index 0000000..f3e178b
--- /dev/null
+++ b/common/device-side/util-axt/src/com/android/compatibility/common/util/AmUtils.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.compatibility.common.util;
+
+public class AmUtils {
+    private static final String TAG = "CtsAmUtils";
+
+    private static final String DUMPSYS_ACTIVITY_PROCESSES = "dumpsys activity --proto processes";
+
+    private AmUtils() {
+    }
+
+    /** Run "adb shell am make-uid-idle PACKAGE" */
+    public static void runMakeUidIdle(String packageName) {
+        SystemUtil.runShellCommandForNoOutput("am make-uid-idle " + packageName);
+    }
+
+    /** Run "adb shell am kill PACKAGE" */
+    public static void runKill(String packageName) throws Exception {
+        runKill(packageName, false /* wait */);
+    }
+
+    public static void runKill(String packageName, boolean wait) throws Exception {
+        SystemUtil.runShellCommandForNoOutput("am kill --user cur " + packageName);
+
+        if (!wait) {
+            return;
+        }
+
+        TestUtils.waitUntil("package process was not killed:" + packageName,
+                () -> !isProcessRunning(packageName));
+    }
+
+    private static boolean isProcessRunning(String packageName) {
+        final String output = SystemUtil.runShellCommand("ps -A -o NAME");
+        String[] packages = output.split("\\n");
+        for (int i = packages.length -1; i >=0; --i) {
+            if (packages[i].equals(packageName)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /** Run "adb shell am set-standby-bucket" */
+    public static void setStandbyBucket(String packageName, int value) {
+        SystemUtil.runShellCommandForNoOutput("am set-standby-bucket " + packageName
+                + " " + value);
+    }
+
+    /** Wait until all broad queues are idle. */
+    public static void waitForBroadcastIdle() {
+        SystemUtil.runCommandAndPrintOnLogcat(TAG, "am wait-for-broadcast-idle");
+    }
+}
diff --git a/common/device-side/util-axt/src/com/android/compatibility/common/util/ApiLevelUtil.java b/common/device-side/util-axt/src/com/android/compatibility/common/util/ApiLevelUtil.java
new file mode 100644
index 0000000..943ebc7
--- /dev/null
+++ b/common/device-side/util-axt/src/com/android/compatibility/common/util/ApiLevelUtil.java
@@ -0,0 +1,95 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.compatibility.common.util;
+
+import android.os.Build;
+
+import java.lang.reflect.Field;
+
+/**
+ * Device-side compatibility utility class for reading device API level.
+ */
+public class ApiLevelUtil {
+
+    public static boolean isBefore(int version) {
+        return Build.VERSION.SDK_INT < version;
+    }
+
+    public static boolean isBefore(String version) {
+        return Build.VERSION.SDK_INT < resolveVersionString(version);
+    }
+
+    public static boolean isAfter(int version) {
+        return Build.VERSION.SDK_INT > version;
+    }
+
+    public static boolean isAfter(String version) {
+        return Build.VERSION.SDK_INT > resolveVersionString(version);
+    }
+
+    public static boolean isAtLeast(int version) {
+        return Build.VERSION.SDK_INT >= version;
+    }
+
+    public static boolean isAtLeast(String version) {
+        return Build.VERSION.SDK_INT >= resolveVersionString(version);
+    }
+
+    public static boolean isAtMost(int version) {
+        return Build.VERSION.SDK_INT <= version;
+    }
+
+    public static boolean isAtMost(String version) {
+        return Build.VERSION.SDK_INT <= resolveVersionString(version);
+    }
+
+    public static int getApiLevel() {
+        return Build.VERSION.SDK_INT;
+    }
+
+    public static boolean codenameEquals(String name) {
+        return Build.VERSION.CODENAME.equalsIgnoreCase(name.trim());
+    }
+
+    public static boolean codenameStartsWith(String prefix) {
+        return Build.VERSION.CODENAME.startsWith(prefix);
+    }
+
+    public static String getCodename() {
+        return Build.VERSION.CODENAME;
+    }
+
+    protected static int resolveVersionString(String versionString) {
+        // Attempt 1: Parse version string as an integer, e.g. "23" for M
+        try {
+            return Integer.parseInt(versionString);
+        } catch (NumberFormatException e) { /* ignore for alternate approaches below */ }
+        // Attempt 2: Find matching field in VersionCodes utility class, return value
+        try {
+            Field versionField = VersionCodes.class.getField(versionString.toUpperCase());
+            return versionField.getInt(null); // no instance for VERSION_CODES, use null
+        } catch (IllegalAccessException | NoSuchFieldException e) { /* ignore */ }
+        // Attempt 3: Find field within android.os.Build.VERSION_CODES
+        try {
+            Field versionField = Build.VERSION_CODES.class.getField(versionString.toUpperCase());
+            return versionField.getInt(null); // no instance for VERSION_CODES, use null
+        } catch (IllegalAccessException | NoSuchFieldException e) {
+            throw new RuntimeException(
+                    String.format("Failed to parse version string %s", versionString), e);
+        }
+    }
+}
diff --git a/common/device-side/util-axt/src/com/android/compatibility/common/util/AppOpsUtils.java b/common/device-side/util-axt/src/com/android/compatibility/common/util/AppOpsUtils.java
new file mode 100644
index 0000000..c9338e4
--- /dev/null
+++ b/common/device-side/util-axt/src/com/android/compatibility/common/util/AppOpsUtils.java
@@ -0,0 +1,132 @@
+/*
+ * Copyright (C) 2018 Google Inc.
+ *
+ * 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.compatibility.common.util;
+
+import static android.app.AppOpsManager.MODE_ALLOWED;
+import static android.app.AppOpsManager.MODE_DEFAULT;
+import static android.app.AppOpsManager.MODE_ERRORED;
+import static android.app.AppOpsManager.MODE_IGNORED;
+
+import android.app.AppOpsManager;
+import androidx.test.InstrumentationRegistry;
+
+import java.io.IOException;
+
+/**
+ * Utilities for controlling App Ops settings, and testing whether ops are logged.
+ */
+public class AppOpsUtils {
+
+    /**
+     * Resets a package's app ops configuration to the device default. See AppOpsManager for the
+     * default op settings.
+     *
+     * <p>
+     * It's recommended to call this in setUp() and tearDown() of your test so the test starts and
+     * ends with a reproducible default state, and so doesn't affect other tests.
+     *
+     * <p>
+     * Some app ops are configured to be non-resettable, which means that the state of these will
+     * not be reset even when calling this method.
+     */
+    public static String reset(String packageName) throws IOException {
+        return runCommand("appops reset " + packageName);
+    }
+
+    /**
+     * Sets the app op mode (e.g. allowed, denied) for a single package and operation.
+     */
+    public static String setOpMode(String packageName, String opStr, int mode)
+            throws IOException {
+        String modeStr;
+        switch (mode) {
+            case MODE_ALLOWED:
+                modeStr = "allow";
+                break;
+            case MODE_ERRORED:
+                modeStr = "deny";
+                break;
+            case MODE_IGNORED:
+                modeStr = "ignore";
+                break;
+            case MODE_DEFAULT:
+                modeStr = "default";
+                break;
+            default:
+                throw new IllegalArgumentException("Unexpected app op type");
+        }
+        String command = "appops set " + packageName + " " + opStr + " " + modeStr;
+        return runCommand(command);
+    }
+
+    /**
+     * Get the app op mode (e.g. MODE_ALLOWED, MODE_DEFAULT) for a single package and operation.
+     */
+    public static int getOpMode(String packageName, String opStr)
+            throws IOException {
+        String opState = getOpState(packageName, opStr);
+        if (opState.contains(" allow")) {
+            return MODE_ALLOWED;
+        } else if (opState.contains(" deny")) {
+            return MODE_ERRORED;
+        } else if (opState.contains(" ignore")) {
+            return MODE_IGNORED;
+        } else if (opState.contains(" default")) {
+            return MODE_DEFAULT;
+        } else {
+            throw new IllegalStateException("Unexpected app op mode returned " + opState);
+        }
+    }
+
+    /**
+     * Returns whether an allowed operation has been logged by the AppOpsManager for a
+     * package. Operations are noted when the app attempts to perform them and calls e.g.
+     * {@link AppOpsManager#noteOperation}.
+     *
+     * @param opStr The public string constant of the operation (e.g. OPSTR_READ_SMS).
+     */
+    public static boolean allowedOperationLogged(String packageName, String opStr)
+            throws IOException {
+        return getOpState(packageName, opStr).contains(" time=");
+    }
+
+    /**
+     * Returns whether a rejected operation has been logged by the AppOpsManager for a
+     * package. Operations are noted when the app attempts to perform them and calls e.g.
+     * {@link AppOpsManager#noteOperation}.
+     *
+     * @param opStr The public string constant of the operation (e.g. OPSTR_READ_SMS).
+     */
+    public static boolean rejectedOperationLogged(String packageName, String opStr)
+            throws IOException {
+        return getOpState(packageName, opStr).contains(" rejectTime=");
+    }
+
+    /**
+     * Returns the app op state for a package. Includes information on when the operation was last
+     * attempted to be performed by the package.
+     *
+     * Format: "SEND_SMS: allow; time=+23h12m54s980ms ago; rejectTime=+1h10m23s180ms"
+     */
+    private static String getOpState(String packageName, String opStr) throws IOException {
+        return runCommand("appops get " + packageName + " " + opStr);
+    }
+
+    private static String runCommand(String command) throws IOException {
+        return SystemUtil.runShellCommand(InstrumentationRegistry.getInstrumentation(), command);
+    }
+}
diff --git a/common/device-side/util-axt/src/com/android/compatibility/common/util/AppStandbyUtils.java b/common/device-side/util-axt/src/com/android/compatibility/common/util/AppStandbyUtils.java
new file mode 100644
index 0000000..eb94b60
--- /dev/null
+++ b/common/device-side/util-axt/src/com/android/compatibility/common/util/AppStandbyUtils.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.compatibility.common.util;
+
+import android.util.Log;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public class AppStandbyUtils {
+    private static final String TAG = "CtsAppStandbyUtils";
+
+    /**
+     * Returns if app standby is enabled.
+     *
+     * @return true if enabled; or false if disabled.
+     */
+    public static boolean isAppStandbyEnabled() {
+        final String result = SystemUtil.runShellCommand(
+                "dumpsys usagestats is-app-standby-enabled").trim();
+        return Boolean.parseBoolean(result);
+    }
+
+    /**
+     * Sets enabled state for app standby feature for runtime switch.
+     *
+     * App standby feature has 2 switches. This one affects the switch at runtime. If the build
+     * switch is off, enabling the runtime switch will not enable App standby.
+     *
+     * @param enabled if App standby is enabled.
+     */
+    public static void setAppStandbyEnabledAtRuntime(boolean enabled) {
+        final String value = enabled ? "1" : "0";
+        Log.d(TAG, "Setting AppStandby " + (enabled ? "enabled" : "disabled") + " at runtime.");
+        SettingsUtils.putGlobalSetting("app_standby_enabled", value);
+    }
+
+    /**
+     * Returns if app standby is enabled at runtime. Note {@link #isAppStandbyEnabled()} may still
+     * return {@code false} if this method returns {@code true}, because app standby can be disabled
+     * at build time as well.
+     *
+     * @return true if enabled at runtime; or false if disabled at runtime.
+     */
+    public static boolean isAppStandbyEnabledAtRuntime() {
+        final String result =
+                SystemUtil.runShellCommand("settings get global app_standby_enabled").trim();
+        // framework considers null value as enabled.
+        final boolean boolResult = result.equals("1") || result.equals("null");
+        Log.d(TAG, "AppStandby is " + (boolResult ? "enabled" : "disabled") + " at runtime.");
+        return boolResult;
+    }
+}
diff --git a/common/device-side/util-axt/src/com/android/compatibility/common/util/BatteryUtils.java b/common/device-side/util-axt/src/com/android/compatibility/common/util/BatteryUtils.java
new file mode 100644
index 0000000..cbd23e9
--- /dev/null
+++ b/common/device-side/util-axt/src/com/android/compatibility/common/util/BatteryUtils.java
@@ -0,0 +1,106 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.compatibility.common.util;
+
+import static com.android.compatibility.common.util.SettingsUtils.putGlobalSetting;
+import static com.android.compatibility.common.util.TestUtils.waitUntil;
+
+import android.os.BatteryManager;
+import android.os.PowerManager;
+import android.provider.Settings.Global;
+import androidx.test.InstrumentationRegistry;
+import android.util.Log;
+
+public class BatteryUtils {
+    private static final String TAG = "CtsBatteryUtils";
+
+    private BatteryUtils() {
+    }
+
+    public static BatteryManager getBatteryManager() {
+        return InstrumentationRegistry.getContext().getSystemService(BatteryManager.class);
+    }
+
+    public static PowerManager getPowerManager() {
+        return InstrumentationRegistry.getContext().getSystemService(PowerManager.class);
+    }
+
+    /** Make the target device think it's off charger. */
+    public static void runDumpsysBatteryUnplug() throws Exception {
+        SystemUtil.runShellCommandForNoOutput("dumpsys battery unplug");
+
+        Log.d(TAG, "Battery UNPLUGGED");
+    }
+
+    /** Reset {@link #runDumpsysBatteryUnplug}.  */
+    public static void runDumpsysBatteryReset() throws Exception {
+        SystemUtil.runShellCommandForNoOutput(("dumpsys battery reset"));
+
+        Log.d(TAG, "Battery RESET");
+    }
+
+    /**
+     * Enable / disable battery saver. Note {@link #runDumpsysBatteryUnplug} must have been
+     * executed before enabling BS.
+     */
+    public static void enableBatterySaver(boolean enabled) throws Exception {
+        if (enabled) {
+            SystemUtil.runShellCommandForNoOutput("cmd power set-mode 1");
+            putGlobalSetting(Global.LOW_POWER_MODE, "1");
+            waitUntil("Battery saver still off", () -> getPowerManager().isPowerSaveMode());
+            waitUntil("Location mode still " + getPowerManager().getLocationPowerSaveMode(),
+                    () -> (PowerManager.LOCATION_MODE_NO_CHANGE
+                            != getPowerManager().getLocationPowerSaveMode()));
+
+            Thread.sleep(500);
+            waitUntil("Force all apps standby still off",
+                    () -> SystemUtil.runShellCommand("dumpsys alarm")
+                            .contains(" Force all apps standby: true\n"));
+
+        } else {
+            SystemUtil.runShellCommandForNoOutput("cmd power set-mode 0");
+            putGlobalSetting(Global.LOW_POWER_MODE_STICKY, "0");
+            waitUntil("Battery saver still on", () -> !getPowerManager().isPowerSaveMode());
+            waitUntil("Location mode still " + getPowerManager().getLocationPowerSaveMode(),
+                    () -> (PowerManager.LOCATION_MODE_NO_CHANGE
+                            == getPowerManager().getLocationPowerSaveMode()));
+
+            Thread.sleep(500);
+            waitUntil("Force all apps standby still on",
+                    () -> SystemUtil.runShellCommand("dumpsys alarm")
+                            .contains(" Force all apps standby: false\n"));
+        }
+
+        AmUtils.waitForBroadcastIdle();
+        Log.d(TAG, "Battery saver turned " + (enabled ? "ON" : "OFF"));
+    }
+
+    /**
+     * Turn on/off screen.
+     */
+    public static void turnOnScreen(boolean on) throws Exception {
+        if (on) {
+            SystemUtil.runShellCommandForNoOutput("input keyevent KEYCODE_WAKEUP");
+            waitUntil("Device still not interactive", () -> getPowerManager().isInteractive());
+
+        } else {
+            SystemUtil.runShellCommandForNoOutput("input keyevent KEYCODE_SLEEP");
+            waitUntil("Device still interactive", () -> !getPowerManager().isInteractive());
+        }
+        AmUtils.waitForBroadcastIdle();
+        Log.d(TAG, "Screen turned " + (on ? "ON" : "OFF"));
+    }
+}
diff --git a/common/device-side/util-axt/src/com/android/compatibility/common/util/BeforeAfterRule.java b/common/device-side/util-axt/src/com/android/compatibility/common/util/BeforeAfterRule.java
new file mode 100644
index 0000000..be75671
--- /dev/null
+++ b/common/device-side/util-axt/src/com/android/compatibility/common/util/BeforeAfterRule.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.compatibility.common.util;
+
+import org.junit.rules.TestRule;
+import org.junit.runner.Description;
+import org.junit.runners.model.Statement;
+
+/**
+ * Custom JUnit4 rule that provides "before" / "after" callbacks, which is useful to use with
+ * {@link org.junit.rules.RuleChain}.
+ */
+public class BeforeAfterRule implements TestRule {
+    @Override
+    public Statement apply(Statement base, Description description) {
+        return new Statement() {
+
+            @Override
+            public void evaluate() throws Throwable {
+                onBefore(base, description);
+                try {
+                    base.evaluate();
+                } finally {
+                    onAfter(base, description);
+                }
+            }
+        };
+    }
+
+    protected void onBefore(Statement base, Description description) throws Throwable {
+    }
+
+    protected void onAfter(Statement base, Description description) throws Throwable {
+    }
+}
diff --git a/common/device-side/util-axt/src/com/android/compatibility/common/util/BitmapUtils.java b/common/device-side/util-axt/src/com/android/compatibility/common/util/BitmapUtils.java
new file mode 100644
index 0000000..7f94d6f
--- /dev/null
+++ b/common/device-side/util-axt/src/com/android/compatibility/common/util/BitmapUtils.java
@@ -0,0 +1,128 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.compatibility.common.util;
+
+import android.app.WallpaperManager;
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.Bitmap.CompressFormat;
+import android.graphics.Color;
+import android.util.Log;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.lang.reflect.Method;
+import java.util.Random;
+
+public class BitmapUtils {
+    private static final String TAG = "BitmapUtils";
+
+    private BitmapUtils() {}
+
+    // Compares two bitmaps by pixels.
+    public static boolean compareBitmaps(Bitmap bmp1, Bitmap bmp2) {
+        if (bmp1 == bmp2) {
+            return true;
+        }
+
+        if (bmp1 == null || bmp2 == null) {
+            return false;
+        }
+
+        if ((bmp1.getWidth() != bmp2.getWidth()) || (bmp1.getHeight() != bmp2.getHeight())) {
+            return false;
+        }
+
+        for (int i = 0; i < bmp1.getWidth(); i++) {
+            for (int j = 0; j < bmp1.getHeight(); j++) {
+                if (bmp1.getPixel(i, j) != bmp2.getPixel(i, j)) {
+                    return false;
+                }
+            }
+        }
+        return true;
+    }
+
+    public static Bitmap generateRandomBitmap(int width, int height) {
+        final Bitmap bmp = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
+        final Random generator = new Random();
+        for (int x = 0; x < width; x++) {
+            for (int y = 0; y < height; y++) {
+                bmp.setPixel(x, y, generator.nextInt(Integer.MAX_VALUE));
+            }
+        }
+        return bmp;
+    }
+
+    public static Bitmap generateWhiteBitmap(int width, int height) {
+        final Bitmap bmp = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
+        bmp.eraseColor(Color.WHITE);
+        return bmp;
+    }
+
+    public static Bitmap getWallpaperBitmap(Context context) throws Exception {
+        WallpaperManager wallpaperManager = WallpaperManager.getInstance(context);
+        Class<?> noparams[] = {};
+        Class<?> wmClass = wallpaperManager.getClass();
+        Method methodGetBitmap = wmClass.getDeclaredMethod("getBitmap", noparams);
+        return (Bitmap) methodGetBitmap.invoke(wallpaperManager, null);
+    }
+
+    public static ByteArrayInputStream bitmapToInputStream(Bitmap bmp) {
+        final ByteArrayOutputStream bos = new ByteArrayOutputStream();
+        bmp.compress(CompressFormat.PNG, 0 /*ignored for PNG*/, bos);
+        byte[] bitmapData = bos.toByteArray();
+        return new ByteArrayInputStream(bitmapData);
+    }
+
+    private static void logIfBitmapSolidColor(String fileName, Bitmap bitmap) {
+        int firstColor = bitmap.getPixel(0, 0);
+        for (int x = 0; x < bitmap.getWidth(); x++) {
+            for (int y = 0; y < bitmap.getHeight(); y++) {
+                if (bitmap.getPixel(x, y) != firstColor) {
+                    return;
+                }
+            }
+        }
+
+        Log.w(TAG, String.format("%s entire bitmap color is %x", fileName, firstColor));
+    }
+
+    public static void saveBitmap(Bitmap bitmap, String directoryName, String fileName) {
+        new File(directoryName).mkdirs(); // create dirs if needed
+
+        Log.d(TAG, "Saving file: " + fileName + " in directory: " + directoryName);
+
+        if (bitmap == null) {
+            Log.d(TAG, "File not saved, bitmap was null");
+            return;
+        }
+
+        logIfBitmapSolidColor(fileName, bitmap);
+
+        File file = new File(directoryName, fileName);
+        try (FileOutputStream fileStream = new FileOutputStream(file)) {
+            bitmap.compress(Bitmap.CompressFormat.PNG, 0 /* ignored for PNG */, fileStream);
+            fileStream.flush();
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+    }
+}
diff --git a/common/device-side/util-axt/src/com/android/compatibility/common/util/BlockedNumberService.java b/common/device-side/util-axt/src/com/android/compatibility/common/util/BlockedNumberService.java
new file mode 100644
index 0000000..360c078
--- /dev/null
+++ b/common/device-side/util-axt/src/com/android/compatibility/common/util/BlockedNumberService.java
@@ -0,0 +1,96 @@
+/*
+ * Copyright (C) 2019 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.compatibility.common.util;
+
+import static android.provider.BlockedNumberContract.BlockedNumbers.COLUMN_ORIGINAL_NUMBER;
+import static android.provider.BlockedNumberContract.BlockedNumbers.CONTENT_URI;
+
+import android.app.IntentService;
+import android.content.ContentResolver;
+import android.content.ContentValues;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.ResultReceiver;
+import android.util.Log;
+
+/**
+ * A service to handle interactions with the BlockedNumberProvider. The BlockedNumberProvider
+ * can only be accessed by the primary user. This service can be run as a singleton service
+ * which will then be able to access the BlockedNumberProvider from a test running in a
+ * secondary user.
+ */
+public class BlockedNumberService extends IntentService {
+
+    static final String INSERT_ACTION = "android.telecom.cts.InsertBlockedNumber";
+    static final String DELETE_ACTION = "android.telecom.cts.DeleteBlockedNumber";
+    static final String PHONE_NUMBER_EXTRA = "number";
+    static final String URI_EXTRA = "uri";
+    static final String ROWS_EXTRA = "rows";
+    static final String RESULT_RECEIVER_EXTRA = "resultReceiver";
+
+    private static final String TAG = "CtsBlockNumberSvc";
+
+    private ContentResolver mContentResolver;
+
+    public BlockedNumberService() {
+        super(BlockedNumberService.class.getName());
+    }
+
+    @Override
+    public void onHandleIntent(Intent intent) {
+        Log.i(TAG, "Starting BlockedNumberService service: " + intent);
+        if (intent == null) {
+            return;
+        }
+        Bundle bundle;
+        mContentResolver = getContentResolver();
+        switch (intent.getAction()) {
+            case INSERT_ACTION:
+                bundle = insertBlockedNumber(intent.getStringExtra(PHONE_NUMBER_EXTRA));
+                break;
+            case DELETE_ACTION:
+                bundle = deleteBlockedNumber(Uri.parse(intent.getStringExtra(URI_EXTRA)));
+                break;
+            default:
+                bundle = new Bundle();
+                break;
+        }
+        ResultReceiver receiver = intent.getParcelableExtra(RESULT_RECEIVER_EXTRA);
+        receiver.send(0, bundle);
+    }
+
+    private Bundle insertBlockedNumber(String number) {
+        Log.i(TAG, "insertBlockedNumber: " + number);
+
+        ContentValues cv = new ContentValues();
+        cv.put(COLUMN_ORIGINAL_NUMBER, number);
+        Uri uri = mContentResolver.insert(CONTENT_URI, cv);
+        Bundle bundle = new Bundle();
+        bundle.putString(URI_EXTRA, uri.toString());
+        return bundle;
+    }
+
+    private Bundle deleteBlockedNumber(Uri uri) {
+        Log.i(TAG, "deleteBlockedNumber: " + uri);
+
+        int rows = mContentResolver.delete(uri, null, null);
+        Bundle bundle = new Bundle();
+        bundle.putInt(ROWS_EXTRA, rows);
+        return bundle;
+    }
+}
diff --git a/common/device-side/util-axt/src/com/android/compatibility/common/util/BlockedNumberUtil.java b/common/device-side/util-axt/src/com/android/compatibility/common/util/BlockedNumberUtil.java
new file mode 100644
index 0000000..e5a0ce4
--- /dev/null
+++ b/common/device-side/util-axt/src/com/android/compatibility/common/util/BlockedNumberUtil.java
@@ -0,0 +1,92 @@
+/*
+ * Copyright (C) 2019 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.compatibility.common.util;
+
+import static com.android.compatibility.common.util.BlockedNumberService.DELETE_ACTION;
+import static com.android.compatibility.common.util.BlockedNumberService.INSERT_ACTION;
+import static com.android.compatibility.common.util.BlockedNumberService.PHONE_NUMBER_EXTRA;
+import static com.android.compatibility.common.util.BlockedNumberService.RESULT_RECEIVER_EXTRA;
+import static com.android.compatibility.common.util.BlockedNumberService.ROWS_EXTRA;
+import static com.android.compatibility.common.util.BlockedNumberService.URI_EXTRA;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.ResultReceiver;
+
+import junit.framework.TestCase;
+
+import java.util.concurrent.Semaphore;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Utility for starting the blocked number service.
+ */
+public class BlockedNumberUtil {
+
+    private static final int TIMEOUT = 2;
+
+    private BlockedNumberUtil() {}
+
+    /** Insert a phone number into the blocked number provider and returns the resulting Uri. */
+    public static Uri insertBlockedNumber(Context context, String phoneNumber) {
+        Intent intent = new Intent(INSERT_ACTION);
+        intent.putExtra(PHONE_NUMBER_EXTRA, phoneNumber);
+
+        return Uri.parse(runBlockedNumberService(context, intent).getString(URI_EXTRA));
+    }
+
+    /** Remove a number from the blocked number provider and returns the number of rows deleted. */
+    public static int deleteBlockedNumber(Context context, Uri uri) {
+        Intent intent = new Intent(DELETE_ACTION);
+        intent.putExtra(URI_EXTRA, uri.toString());
+
+        return runBlockedNumberService(context, intent).getInt(ROWS_EXTRA);
+    }
+
+    /** Start the blocked number service. */
+    static Bundle runBlockedNumberService(Context context, Intent intent) {
+        // Temporarily allow background service
+        SystemUtil.runShellCommand("cmd deviceidle tempwhitelist " + context.getPackageName());
+
+        final Semaphore semaphore = new Semaphore(0);
+        final Bundle result = new Bundle();
+
+        ResultReceiver receiver = new ResultReceiver(new Handler(Looper.getMainLooper())) {
+            @Override
+            protected void onReceiveResult(int resultCode, Bundle resultData) {
+                result.putAll(resultData);
+                semaphore.release();
+            }
+        };
+        intent.putExtra(RESULT_RECEIVER_EXTRA, receiver);
+        intent.setComponent(new ComponentName(context, BlockedNumberService.class));
+
+        context.startService(intent);
+
+        try {
+            TestCase.assertTrue(semaphore.tryAcquire(TIMEOUT, TimeUnit.SECONDS));
+        } catch (InterruptedException e) {
+            TestCase.fail("Timed out waiting for result from BlockedNumberService");
+        }
+        return result;
+    }
+}
diff --git a/common/device-side/util-axt/src/com/android/compatibility/common/util/BlockingBroadcastReceiver.java b/common/device-side/util-axt/src/com/android/compatibility/common/util/BlockingBroadcastReceiver.java
new file mode 100644
index 0000000..301a626
--- /dev/null
+++ b/common/device-side/util-axt/src/com/android/compatibility/common/util/BlockingBroadcastReceiver.java
@@ -0,0 +1,102 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.compatibility.common.util;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import androidx.annotation.Nullable;
+import android.util.Log;
+
+import java.util.concurrent.ArrayBlockingQueue;
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * A receiver that allows caller to wait for the broadcast synchronously. Notice that you should not
+ * reuse the instance. Usage is typically like this:
+ * <pre>
+ *     BlockingBroadcastReceiver receiver = new BlockingBroadcastReceiver(context, "action");
+ *     try {
+ *         receiver.register();
+ *         Intent intent = receiver.awaitForBroadcast();
+ *         // assert the intent
+ *     } finally {
+ *         receiver.unregisterQuietly();
+ *     }
+ * </pre>
+ */
+public class BlockingBroadcastReceiver extends BroadcastReceiver {
+    private static final String TAG = "BlockingBroadcast";
+
+    private static final int DEFAULT_TIMEOUT_SECONDS = 10;
+
+    private final BlockingQueue<Intent> mBlockingQueue;
+    private final String mExpectedAction;
+    private final Context mContext;
+
+    public BlockingBroadcastReceiver(Context context, String expectedAction) {
+        mContext = context;
+        mExpectedAction = expectedAction;
+        mBlockingQueue = new ArrayBlockingQueue<>(1);
+    }
+
+    @Override
+    public void onReceive(Context context, Intent intent) {
+        if (mExpectedAction.equals(intent.getAction())) {
+            mBlockingQueue.add(intent);
+        }
+    }
+
+    public void register() {
+        mContext.registerReceiver(this, new IntentFilter(mExpectedAction));
+    }
+
+    /**
+     * Wait until the broadcast and return the received broadcast intent. {@code null} is returned
+     * if no broadcast with expected action is received within 10 seconds.
+     */
+    public @Nullable Intent awaitForBroadcast() {
+        try {
+            return mBlockingQueue.poll(DEFAULT_TIMEOUT_SECONDS, TimeUnit.SECONDS);
+        } catch (InterruptedException e) {
+            Log.e(TAG, "waitForBroadcast get interrupted: ", e);
+        }
+        return null;
+    }
+
+    /**
+     * Wait until the broadcast and return the received broadcast intent. {@code null} is returned
+     * if no broadcast with expected action is received within the given timeout.
+     */
+    public @Nullable Intent awaitForBroadcast(long timeoutMillis) {
+        try {
+            return mBlockingQueue.poll(timeoutMillis, TimeUnit.MILLISECONDS);
+        } catch (InterruptedException e) {
+            Log.e(TAG, "waitForBroadcast get interrupted: ", e);
+        }
+        return null;
+    }
+
+    public void unregisterQuietly() {
+        try {
+            mContext.unregisterReceiver(this);
+        } catch (Exception ex) {
+            Log.e(TAG, "Failed to unregister BlockingBroadcastReceiver: ", ex);
+        }
+    }
+}
\ No newline at end of file
diff --git a/common/device-side/util-axt/src/com/android/compatibility/common/util/BroadcastRpcBase.java b/common/device-side/util-axt/src/com/android/compatibility/common/util/BroadcastRpcBase.java
new file mode 100644
index 0000000..e4d9c22
--- /dev/null
+++ b/common/device-side/util-axt/src/com/android/compatibility/common/util/BroadcastRpcBase.java
@@ -0,0 +1,148 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.compatibility.common.util;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import android.content.BroadcastReceiver;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Looper;
+import androidx.test.InstrumentationRegistry;
+import android.util.Log;
+
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicReference;
+
+/**
+ * Base class to help broadcast-based RPC.
+ */
+public abstract class BroadcastRpcBase<TRequest, TResponse> {
+    private static final String TAG = "BroadcastRpc";
+
+    private static final boolean VERBOSE = Log.isLoggable(TAG, Log.VERBOSE);
+
+    static final String ACTION_REQUEST = "ACTION_REQUEST";
+    static final String EXTRA_PAYLOAD = "EXTRA_PAYLOAD";
+    static final String EXTRA_EXCEPTION = "EXTRA_EXCEPTION";
+
+    static Handler sMainHandler = new Handler(Looper.getMainLooper());
+
+    /** Implement in a subclass */
+    protected abstract byte[] requestToBytes(TRequest request);
+
+    /** Implement in a subclass */
+    protected abstract TResponse bytesToResponse(byte[] bytes);
+
+    public TResponse invoke(ComponentName targetReceiver, TRequest request) throws Exception {
+        // Create a request intent.
+        Log.i(TAG, "Sending to: " + targetReceiver + (VERBOSE ? "\nRequest: " + request : ""));
+
+        final Intent requestIntent = new Intent(ACTION_REQUEST)
+                .setComponent(targetReceiver)
+                .addFlags(Intent.FLAG_RECEIVER_FOREGROUND)
+                .putExtra(EXTRA_PAYLOAD, requestToBytes(request));
+
+        // Send it.
+        final CountDownLatch latch = new CountDownLatch(1);
+        final AtomicReference<Bundle> responseBundle = new AtomicReference<>();
+
+        InstrumentationRegistry.getContext().sendOrderedBroadcast(
+                requestIntent, null, new BroadcastReceiver() {
+                    @Override
+                    public void onReceive(Context context, Intent intent) {
+                        responseBundle.set(getResultExtras(false));
+                        latch.countDown();
+                    }
+                }, sMainHandler, 0, null, null);
+
+        // Wait for a reply and check it.
+        final boolean responseArrived = latch.await(60, TimeUnit.SECONDS);
+        assertTrue("Didn't receive broadcast result.", responseArrived);
+
+        // TODO If responseArrived is false, print if the package / component is installed?
+
+        assertNotNull("Didn't receive result extras", responseBundle.get());
+
+        final String exception = responseBundle.get().getString(EXTRA_EXCEPTION);
+        if (exception != null) {
+            fail("Target throw exception: receiver=" + targetReceiver
+                    + "\nException: " + exception);
+        }
+
+        final byte[] resultPayload = responseBundle.get().getByteArray(EXTRA_PAYLOAD);
+        assertNotNull("Didn't receive result payload", resultPayload);
+
+        Log.i(TAG, "Response received: " + (VERBOSE ? resultPayload.toString() : ""));
+
+        return bytesToResponse(resultPayload);
+    }
+
+    /**
+     * Base class for a receiver for a broadcast-based RPC.
+     */
+    public abstract static class ReceiverBase<TRequest, TResponse> extends BroadcastReceiver {
+        @Override
+        public final void onReceive(Context context, Intent intent) {
+            assertEquals(ACTION_REQUEST, intent.getAction());
+
+            // Parse the request.
+            final TRequest request = bytesToRequest(intent.getByteArrayExtra(EXTRA_PAYLOAD));
+
+            Log.i(TAG, "Request received: " + (VERBOSE ? request.toString() : ""));
+
+            Throwable exception = null;
+
+            // Handle it and generate a response.
+            TResponse response = null;
+            try {
+                response = handleRequest(context, request);
+                Log.i(TAG, "Response generated: " + (VERBOSE ? response.toString() : ""));
+            } catch (Throwable e) {
+                exception = e;
+                Log.e(TAG, "Exception thrown: " + e.getMessage(), e);
+            }
+
+            // Send back.
+            final Bundle extras = new Bundle();
+            if (response != null) {
+                extras.putByteArray(EXTRA_PAYLOAD, responseToBytes(response));
+            }
+            if (exception != null) {
+                extras.putString(EXTRA_EXCEPTION,
+                        exception.toString() + "\n" + Log.getStackTraceString(exception));
+            }
+            setResultExtras(extras);
+        }
+
+        /** Implement in a subclass */
+        protected abstract TResponse handleRequest(Context context, TRequest request)
+                throws Exception;
+
+        /** Implement in a subclass */
+        protected abstract byte[] responseToBytes(TResponse response);
+
+        /** Implement in a subclass */
+        protected abstract TRequest bytesToRequest(byte[] bytes);
+    }
+}
diff --git a/common/device-side/util-axt/src/com/android/compatibility/common/util/BroadcastTestBase.java b/common/device-side/util-axt/src/com/android/compatibility/common/util/BroadcastTestBase.java
new file mode 100644
index 0000000..bf5dc39
--- /dev/null
+++ b/common/device-side/util-axt/src/com/android/compatibility/common/util/BroadcastTestBase.java
@@ -0,0 +1,123 @@
+/*
+ * Copyright (C) 2015 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.compatibility.common.util;
+
+import android.content.BroadcastReceiver;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.pm.PackageManager;
+import android.os.Bundle;
+import android.test.ActivityInstrumentationTestCase2;
+import android.util.Log;
+
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+public class BroadcastTestBase extends ActivityInstrumentationTestCase2<
+                                       BroadcastTestStartActivity> {
+    static final String TAG = "BroadcastTestBase";
+    protected static final int TIMEOUT_MS = 20 * 1000;
+
+    protected Context mContext;
+    protected Bundle mResultExtras;
+    private CountDownLatch mLatch;
+    protected ActivityDoneReceiver mActivityDoneReceiver = null;
+    private BroadcastTestStartActivity mActivity;
+    private BroadcastUtils.TestcaseType mTestCaseType;
+    protected boolean mHasFeature;
+
+    public BroadcastTestBase() {
+        super(BroadcastTestStartActivity.class);
+    }
+
+    @Override
+    protected void setUp() throws Exception {
+        super.setUp();
+        mHasFeature = false;
+    }
+
+    @Override
+    protected void tearDown() throws Exception {
+        if (mHasFeature && mActivityDoneReceiver != null) {
+            try {
+                mContext.unregisterReceiver(mActivityDoneReceiver);
+            } catch (IllegalArgumentException e) {
+                // This exception is thrown if mActivityDoneReceiver in
+                // the above call to unregisterReceiver is never registered.
+                // If so, no harm done by ignoring this exception.
+            }
+            mActivityDoneReceiver = null;
+        }
+        super.tearDown();
+    }
+
+    protected boolean isIntentSupported(String intentStr) {
+        Intent intent = new Intent(intentStr);
+        final PackageManager manager = mContext.getPackageManager();
+        assertNotNull(manager);
+        if (manager.resolveActivity(intent, 0) == null) {
+            Log.i(TAG, "No Activity found for the intent: " + intentStr);
+            return false;
+        }
+        return true;
+    }
+
+    protected void startTestActivity(String intentSuffix) {
+        Intent intent = new Intent();
+        intent.setAction("android.intent.action.TEST_START_ACTIVITY_" + intentSuffix);
+        intent.setComponent(new ComponentName(getInstrumentation().getContext(),
+                BroadcastTestStartActivity.class));
+        setActivityIntent(intent);
+        mActivity = getActivity();
+    }
+
+    protected void registerBroadcastReceiver(BroadcastUtils.TestcaseType testCaseType) throws Exception {
+        mTestCaseType = testCaseType;
+        mLatch = new CountDownLatch(1);
+        mActivityDoneReceiver = new ActivityDoneReceiver();
+        mContext.registerReceiver(mActivityDoneReceiver,
+                new IntentFilter(BroadcastUtils.BROADCAST_INTENT + testCaseType.toString()));
+    }
+
+    protected boolean startTestAndWaitForBroadcast(BroadcastUtils.TestcaseType testCaseType,
+                                                   String pkg, String cls) throws Exception {
+        Log.i(TAG, "Begin Testing: " + testCaseType);
+        registerBroadcastReceiver(testCaseType);
+        mActivity.startTest(testCaseType.toString(), pkg, cls);
+        if (!mLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS)) {
+            fail("Failed to receive broadcast in " + TIMEOUT_MS + "msec");
+            return false;
+        }
+        return true;
+    }
+
+    class ActivityDoneReceiver extends BroadcastReceiver {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            if (intent.getAction().equals(
+                    BroadcastUtils.BROADCAST_INTENT +
+                        BroadcastTestBase.this.mTestCaseType.toString())) {
+                Bundle extras = intent.getExtras();
+                Log.i(TAG, "received_broadcast for " + BroadcastUtils.toBundleString(extras));
+                BroadcastTestBase.this.mResultExtras = extras;
+                mLatch.countDown();
+            }
+        }
+    }
+}
diff --git a/common/device-side/util-axt/src/com/android/compatibility/common/util/BroadcastTestStartActivity.java b/common/device-side/util-axt/src/com/android/compatibility/common/util/BroadcastTestStartActivity.java
new file mode 100644
index 0000000..4b3e85d
--- /dev/null
+++ b/common/device-side/util-axt/src/com/android/compatibility/common/util/BroadcastTestStartActivity.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright (C) 2015 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.compatibility.common.util;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.content.ComponentName;
+import android.os.Bundle;
+import android.util.Log;
+
+public class BroadcastTestStartActivity extends Activity {
+    static final String TAG = "BroadcastTestStartActivity";
+
+    @Override
+    public void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        Log.i(TAG, " in onCreate");
+    }
+
+    @Override
+    protected void onResume() {
+        super.onResume();
+        Log.i(TAG, " in onResume");
+    }
+
+    void startTest(String testCaseType, String pkg, String cls) {
+        Intent intent = new Intent();
+        Log.i(TAG, "received_testcasetype = " + testCaseType);
+        intent.putExtra(BroadcastUtils.TESTCASE_TYPE, testCaseType);
+        intent.setAction("android.intent.action.VIMAIN_" + testCaseType);
+        intent.setComponent(new ComponentName(pkg, cls));
+        startActivity(intent);
+    }
+
+    @Override
+    protected void onPause() {
+        Log.i(TAG, " in onPause");
+        super.onPause();
+    }
+
+    @Override
+    protected void onStart() {
+        super.onStart();
+        Log.i(TAG, " in onStart");
+    }
+
+    @Override
+    protected void onRestart() {
+        super.onRestart();
+        Log.i(TAG, " in onRestart");
+    }
+
+    @Override
+    protected void onStop() {
+        Log.i(TAG, " in onStop");
+        super.onStop();
+    }
+
+    @Override
+    protected void onDestroy() {
+        Log.i(TAG, " in onDestroy");
+        super.onDestroy();
+    }
+}
diff --git a/common/device-side/util-axt/src/com/android/compatibility/common/util/BroadcastUtils.java b/common/device-side/util-axt/src/com/android/compatibility/common/util/BroadcastUtils.java
new file mode 100644
index 0000000..a4661fc
--- /dev/null
+++ b/common/device-side/util-axt/src/com/android/compatibility/common/util/BroadcastUtils.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2015 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.compatibility.common.util;
+
+import android.os.Bundle;
+
+public class BroadcastUtils {
+    public enum TestcaseType {
+        ZEN_MODE_ON,
+        ZEN_MODE_OFF,
+        AIRPLANE_MODE_ON,
+        AIRPLANE_MODE_OFF,
+        BATTERYSAVER_MODE_ON,
+        BATTERYSAVER_MODE_OFF,
+        THEATER_MODE_ON,
+        THEATER_MODE_OFF
+    }
+    public static final String TESTCASE_TYPE = "Testcase_type";
+    public static final String BROADCAST_INTENT =
+            "android.intent.action.FROM_UTIL_CTS_TEST_";
+    public static final int NUM_MINUTES_FOR_ZENMODE = 10;
+
+    public static final String toBundleString(Bundle bundle) {
+        if (bundle == null) {
+            return "*** Bundle is null ****";
+        }
+        StringBuilder buf = new StringBuilder();
+        if (bundle != null) {
+            buf.append("extras: ");
+            for (String s : bundle.keySet()) {
+                buf.append("(" + s + " = " + bundle.get(s) + "), ");
+            }
+        }
+        return buf.toString();
+    }
+}
diff --git a/common/device-side/util-axt/src/com/android/compatibility/common/util/BundleUtils.java b/common/device-side/util-axt/src/com/android/compatibility/common/util/BundleUtils.java
new file mode 100644
index 0000000..eda641d
--- /dev/null
+++ b/common/device-side/util-axt/src/com/android/compatibility/common/util/BundleUtils.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.compatibility.common.util;
+
+import android.os.Bundle;
+
+public class BundleUtils {
+    private BundleUtils() {
+    }
+
+    public static Bundle makeBundle(Object... keysAndValues) {
+        if ((keysAndValues.length % 2) != 0) {
+            throw new IllegalArgumentException("Argument count not even.");
+        }
+
+        if (keysAndValues.length == 0) {
+            return null;
+        }
+        final Bundle ret = new Bundle();
+
+        for (int i = keysAndValues.length - 2; i >= 0; i -= 2) {
+            final String key = keysAndValues[i].toString();
+            final Object value = keysAndValues[i + 1];
+
+            if (value == null) {
+                ret.putString(key, null);
+
+            } else if (value instanceof Boolean) {
+                ret.putBoolean(key, (Boolean) value);
+
+            } else if (value instanceof Integer) {
+                ret.putInt(key, (Integer) value);
+
+            } else if (value instanceof String) {
+                ret.putString(key, (String) value);
+
+            } else if (value instanceof Bundle) {
+                ret.putBundle(key, (Bundle) value);
+            } else {
+                throw new IllegalArgumentException(
+                        "Type not supported yet: " + value.getClass().getName());
+            }
+        }
+        return ret;
+    }
+}
diff --git a/common/device-side/util-axt/src/com/android/compatibility/common/util/BusinessLogicConditionalTestCase.java b/common/device-side/util-axt/src/com/android/compatibility/common/util/BusinessLogicConditionalTestCase.java
new file mode 100644
index 0000000..d12caa8
--- /dev/null
+++ b/common/device-side/util-axt/src/com/android/compatibility/common/util/BusinessLogicConditionalTestCase.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.compatibility.common.util;
+
+import org.junit.Before;
+
+/**
+ *  Device-side base class for tests leveraging the Business Logic service for rules that are
+ *  conditionally added based on the device characteristics.
+ */
+public class BusinessLogicConditionalTestCase extends BusinessLogicTestCase {
+
+    @Override
+    @Before
+    public void handleBusinessLogic() {
+        super.loadBusinessLogic();
+        ensureAuthenticated();
+        super.executeBusinessLogic();
+    }
+
+    protected void ensureAuthenticated() {
+        if (!mCanReadBusinessLogic) {
+            // super class handles the condition that the service is unavailable.
+            return;
+        }
+
+        if (!mBusinessLogic.mConditionalTestsEnabled) {
+            skipTest("Execution of device specific tests is not enabled. "
+                    + "Enable with '--conditional-business-logic-tests-enabled'");
+        }
+
+        if (mBusinessLogic.isAuthorized()) {
+            // Run test as normal.
+            return;
+        }
+        String message = mBusinessLogic.getAuthenticationStatusMessage();
+
+        // Fail test since request was not authorized.
+        failTest(String.format("Unable to execute because %s.", message));
+    }
+}
diff --git a/common/device-side/util-axt/src/com/android/compatibility/common/util/BusinessLogicDeviceExecutor.java b/common/device-side/util-axt/src/com/android/compatibility/common/util/BusinessLogicDeviceExecutor.java
new file mode 100644
index 0000000..7d7aaf0
--- /dev/null
+++ b/common/device-side/util-axt/src/com/android/compatibility/common/util/BusinessLogicDeviceExecutor.java
@@ -0,0 +1,117 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.compatibility.common.util;
+
+import android.content.Context;
+import android.text.TextUtils;
+import android.util.Log;
+
+import java.lang.reflect.Method;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * Execute business logic methods for device side test cases
+ */
+public class BusinessLogicDeviceExecutor extends BusinessLogicExecutor {
+
+    private Context mContext;
+    private Object mTestObj;
+
+    public BusinessLogicDeviceExecutor(Context context, Object testObj) {
+        mContext = context;
+        mTestObj = testObj;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    protected Object getTestObject() {
+        return mTestObj;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void logInfo(String format, Object... args) {
+        Log.i(LOG_TAG, String.format(format, args));
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void logDebug(String format, Object... args) {
+        Log.d(LOG_TAG, String.format(format, args));
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    protected String formatExecutionString(String method, String... args) {
+        return String.format("%s(%s)", method, TextUtils.join(", ", args));
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    protected ResolvedMethod getResolvedMethod(Class cls, String methodName, String... args)
+            throws ClassNotFoundException {
+        List<Method> nameMatches = getMethodsWithName(cls, methodName);
+        for (Method m : nameMatches) {
+            ResolvedMethod rm = new ResolvedMethod(m);
+            int paramTypesMatched = 0;
+            int argsUsed = 0;
+            Class[] paramTypes = m.getParameterTypes();
+            for (Class paramType : paramTypes) {
+                if (argsUsed == args.length && paramType.equals(String.class)) {
+                    // We've used up all supplied string args, so this method will not match.
+                    // If paramType is the Context class, we can match a paramType without needing
+                    // more string args. similarly, paramType "String[]" can be matched with zero
+                    // string args. If we add support for more paramTypes, this logic may require
+                    // adjustment.
+                    break;
+                }
+                if (paramType.equals(String.class)) {
+                    // Type "String" -- supply the next available arg
+                    rm.addArg(args[argsUsed++]);
+                } else if (Context.class.isAssignableFrom(paramType)) {
+                    // Type "Context" -- supply the context from the test case
+                    rm.addArg(mContext);
+                } else if (paramType.equals(Class.forName(STRING_ARRAY_CLASS))) {
+                    // Type "String[]" (or "String...") -- supply all remaining args
+                    rm.addArg(Arrays.copyOfRange(args, argsUsed, args.length));
+                    argsUsed += (args.length - argsUsed);
+                } else {
+                    break; // Param type is unrecognized, this method will not match.
+                }
+                paramTypesMatched++; // A param type has been matched when reaching this point.
+            }
+            if (paramTypesMatched == paramTypes.length && argsUsed == args.length) {
+                return rm; // Args match, methods match, so return the first method-args pairing.
+            }
+            // Not a match, try args for next method that matches by name.
+        }
+        throw new RuntimeException(String.format(
+                "BusinessLogic: Failed to invoke action method %s with args: %s", methodName,
+                Arrays.toString(args)));
+    }
+}
diff --git a/common/device-side/util-axt/src/com/android/compatibility/common/util/BusinessLogicTestCase.java b/common/device-side/util-axt/src/com/android/compatibility/common/util/BusinessLogicTestCase.java
new file mode 100644
index 0000000..36e647b
--- /dev/null
+++ b/common/device-side/util-axt/src/com/android/compatibility/common/util/BusinessLogicTestCase.java
@@ -0,0 +1,117 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.compatibility.common.util;
+
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+import static org.junit.Assume.assumeTrue;
+
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Rule;
+import org.junit.rules.TestName;
+
+import android.app.Instrumentation;
+import android.content.Context;
+import androidx.test.InstrumentationRegistry;
+import android.util.Log;
+
+import java.lang.reflect.Field;
+import java.io.File;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Device-side base class for tests leveraging the Business Logic service.
+ */
+public class BusinessLogicTestCase {
+
+    /* String marking the beginning of the parameter in a test name */
+    private static final String PARAM_START = "[";
+
+    /* Test name rule that tracks the current test method under execution */
+    @Rule public TestName mTestCase = new TestName();
+
+    protected BusinessLogic mBusinessLogic;
+    protected boolean mCanReadBusinessLogic = true;
+
+    @Before
+    public void handleBusinessLogic() {
+        loadBusinessLogic();
+        executeBusinessLogic();
+    }
+
+    protected void executeBusinessLogic() {
+        String methodName = mTestCase.getMethodName();
+        assertTrue(String.format("Test \"%s\" is unable to execute as it depends on the missing "
+                + "remote configuration.", methodName), mCanReadBusinessLogic);
+        if (methodName.contains(PARAM_START)) {
+            // Strip parameter suffix (e.g. "[0]") from method name
+            methodName = methodName.substring(0, methodName.lastIndexOf(PARAM_START));
+        }
+        String testName = String.format("%s#%s", this.getClass().getName(), methodName);
+        if (mBusinessLogic.hasLogicFor(testName)) {
+            Log.i("Finding business logic for test case: ", testName);
+            BusinessLogicExecutor executor = new BusinessLogicDeviceExecutor(getContext(), this);
+            mBusinessLogic.applyLogicFor(testName, executor);
+        }
+    }
+
+    protected void loadBusinessLogic() {
+        File businessLogicFile = new File(BusinessLogic.DEVICE_FILE);
+        if (businessLogicFile.canRead()) {
+            mBusinessLogic = BusinessLogicFactory.createFromFile(businessLogicFile);
+        } else {
+            mCanReadBusinessLogic = false;
+        }
+    }
+
+    protected static Instrumentation getInstrumentation() {
+        return InstrumentationRegistry.getInstrumentation();
+    }
+
+    protected static Context getContext() {
+        return getInstrumentation().getTargetContext();
+    }
+
+    public static void skipTest(String message) {
+        assumeTrue(message, false);
+    }
+
+    public static void failTest(String message) {
+        fail(message);
+    }
+
+    public void mapPut(String mapName, String key, String value) {
+        boolean put = false;
+        for (Field f : getClass().getDeclaredFields()) {
+            if (f.getName().equalsIgnoreCase(mapName) && Map.class.isAssignableFrom(f.getType())) {
+                try {
+                    ((Map) f.get(this)).put(key, value);
+                    put = true;
+                } catch (IllegalAccessException e) {
+                    Log.w(String.format("failed to invoke mapPut on field \"%s\". Resuming...",
+                            f.getName()), e);
+                    // continue iterating through fields, throw exception if no other fields match
+                }
+            }
+        }
+        if (!put) {
+            throw new RuntimeException(String.format("Failed to find map %s in class %s", mapName,
+                    getClass().getName()));
+        }
+    }
+}
diff --git a/common/device-side/util-axt/src/com/android/compatibility/common/util/CTSResult.java b/common/device-side/util-axt/src/com/android/compatibility/common/util/CTSResult.java
new file mode 100644
index 0000000..d60155a
--- /dev/null
+++ b/common/device-side/util-axt/src/com/android/compatibility/common/util/CTSResult.java
@@ -0,0 +1,23 @@
+/*
+ * Copyright (C) 2008 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.compatibility.common.util;
+
+public interface CTSResult {
+    public static final int RESULT_OK = 1;
+    public static final int RESULT_FAIL = 2;
+    public void setResult(int resultCode);
+}
diff --git a/common/device-side/util-axt/src/com/android/compatibility/common/util/CallbackAsserter.java b/common/device-side/util-axt/src/com/android/compatibility/common/util/CallbackAsserter.java
new file mode 100644
index 0000000..5a9a2a6
--- /dev/null
+++ b/common/device-side/util-axt/src/com/android/compatibility/common/util/CallbackAsserter.java
@@ -0,0 +1,136 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.compatibility.common.util;
+
+import static junit.framework.Assert.fail;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.database.ContentObserver;
+import android.net.Uri;
+import androidx.test.InstrumentationRegistry;
+import android.util.Log;
+
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.function.Predicate;
+
+/**
+ * CallbackAsserter helps wait until a callback is called.
+ */
+public class CallbackAsserter {
+    private static final String TAG = "CallbackAsserter";
+
+    final CountDownLatch mLatch = new CountDownLatch(1);
+
+    CallbackAsserter() {
+    }
+
+    /**
+     * Call this to assert a callback be called within the given timeout.
+     */
+    public final void assertCalled(String message, int timeoutSeconds) throws Exception {
+        try {
+            if (mLatch.await(timeoutSeconds, TimeUnit.SECONDS)) {
+                return;
+            }
+            fail("Didn't receive callback: " + message);
+        } finally {
+            cleanUp();
+        }
+    }
+
+    void cleanUp() {
+    }
+
+    /**
+     * Create an instance for a broadcast.
+     */
+    public static CallbackAsserter forBroadcast(IntentFilter filter) {
+        return forBroadcast(filter, null);
+    }
+
+    /**
+     * Create an instance for a broadcast.
+     */
+    public static CallbackAsserter forBroadcast(IntentFilter filter, Predicate<Intent> checker) {
+        return new BroadcastAsserter(filter, checker);
+    }
+
+    /**
+     * Create an instance for a content changed notification.
+     */
+    public static CallbackAsserter forContentUri(Uri watchUri) {
+        return forContentUri(watchUri, null);
+    }
+
+    /**
+     * Create an instance for a content changed notification.
+     */
+    public static CallbackAsserter forContentUri(Uri watchUri, Predicate<Uri> checker) {
+        return new ContentObserverAsserter(watchUri, checker);
+    }
+
+    private static class BroadcastAsserter extends CallbackAsserter {
+        private final BroadcastReceiver mReceiver;
+
+        BroadcastAsserter(IntentFilter filter, Predicate<Intent> checker) {
+            mReceiver = new BroadcastReceiver() {
+                @Override
+                public void onReceive(Context context, Intent intent) {
+                    if (checker != null && !checker.test(intent)) {
+                        Log.v(TAG, "Ignoring intent: " + intent);
+                        return;
+                    }
+                    mLatch.countDown();
+                }
+            };
+            InstrumentationRegistry.getContext().registerReceiver(mReceiver, filter);
+        }
+
+        @Override
+        void cleanUp() {
+            InstrumentationRegistry.getContext().unregisterReceiver(mReceiver);
+        }
+    }
+
+    private static class ContentObserverAsserter extends CallbackAsserter {
+        private final ContentObserver mObserver;
+
+        ContentObserverAsserter(Uri watchUri, Predicate<Uri> checker) {
+            mObserver = new ContentObserver(null) {
+                @Override
+                public void onChange(boolean selfChange, Uri uri) {
+                    if (checker != null && !checker.test(uri)) {
+                        Log.v(TAG, "Ignoring notification on URI: " + uri);
+                        return;
+                    }
+                    mLatch.countDown();
+                }
+            };
+            InstrumentationRegistry.getContext().getContentResolver().registerContentObserver(
+                    watchUri, true, mObserver);
+        }
+
+        @Override
+        void cleanUp() {
+            InstrumentationRegistry.getContext().getContentResolver().unregisterContentObserver(
+                    mObserver);
+        }
+    }
+}
diff --git a/common/device-side/util-axt/src/com/android/compatibility/common/util/ColorUtils.java b/common/device-side/util-axt/src/com/android/compatibility/common/util/ColorUtils.java
new file mode 100644
index 0000000..a2439c7
--- /dev/null
+++ b/common/device-side/util-axt/src/com/android/compatibility/common/util/ColorUtils.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.compatibility.common.util;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.fail;
+
+import android.graphics.Color;
+
+public class ColorUtils {
+    public static void verifyColor(int expected, int observed) {
+        verifyColor(expected, observed, 0);
+    }
+
+    public static void verifyColor(int expected, int observed, int tolerance) {
+        String s = "expected " + Integer.toHexString(expected)
+                + ", observed " + Integer.toHexString(observed)
+                + ", tolerated channel error " + tolerance;
+        assertEquals(s, Color.red(expected), Color.red(observed), tolerance);
+        assertEquals(s, Color.green(expected), Color.green(observed), tolerance);
+        assertEquals(s, Color.blue(expected), Color.blue(observed), tolerance);
+        assertEquals(s, Color.alpha(expected), Color.alpha(observed), tolerance);
+    }
+}
diff --git a/common/device-side/util-axt/src/com/android/compatibility/common/util/ConnectivityUtils.java b/common/device-side/util-axt/src/com/android/compatibility/common/util/ConnectivityUtils.java
new file mode 100644
index 0000000..09a0a85
--- /dev/null
+++ b/common/device-side/util-axt/src/com/android/compatibility/common/util/ConnectivityUtils.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.compatibility.common.util;
+
+import static org.junit.Assert.assertTrue;
+
+import android.content.Context;
+import android.net.ConnectivityManager;
+import android.net.NetworkInfo;
+
+public class ConnectivityUtils {
+    private ConnectivityUtils() {
+    }
+
+    /** @return true when the device has a network connection. */
+    public static boolean isNetworkConnected(Context context) {
+        final NetworkInfo networkInfo = context.getSystemService(ConnectivityManager.class)
+                .getActiveNetworkInfo();
+        return (networkInfo != null) && networkInfo.isConnected();
+    }
+
+    /** Assert that the device has a network connection. */
+    public static void assertNetworkConnected(Context context) {
+        assertTrue("Network must be connected", isNetworkConnected(context));
+    }
+}
diff --git a/common/device-side/util-axt/src/com/android/compatibility/common/util/CpuFeatures.java b/common/device-side/util-axt/src/com/android/compatibility/common/util/CpuFeatures.java
new file mode 100644
index 0000000..6cf1414
--- /dev/null
+++ b/common/device-side/util-axt/src/com/android/compatibility/common/util/CpuFeatures.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright (C) 2010 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.compatibility.common.util;
+
+import android.os.Build;
+
+public class CpuFeatures {
+
+    public static final String ARMEABI_V7 = "armeabi-v7a";
+
+    public static final String ARMEABI = "armeabi";
+
+    public static final String MIPSABI = "mips";
+
+    public static final  String X86ABI = "x86";
+
+    public static final int HWCAP_VFP = (1 << 6);
+
+    public static final int HWCAP_NEON = (1 << 12);
+
+    public static final int HWCAP_VFPv3 = (1 << 13);
+
+    public static final int HWCAP_VFPv4 = (1 << 16);
+
+    public static final int HWCAP_IDIVA = (1 << 17);
+
+    public static final int HWCAP_IDIVT = (1 << 18);
+
+    static {
+        System.loadLibrary("cts_jni");
+    }
+
+    public static native boolean isArmCpu();
+
+    public static native boolean isArm7Compatible();
+
+    public static native boolean isMipsCpu();
+
+    public static native boolean isX86Cpu();
+
+    public static native boolean isArm64Cpu();
+
+    public static native boolean isMips64Cpu();
+
+    public static native boolean isX86_64Cpu();
+
+    public static native int getHwCaps();
+
+    public static boolean isArm64CpuIn32BitMode() {
+        if (!isArmCpu()) {
+            return false;
+        }
+
+        for (String abi : Build.SUPPORTED_64_BIT_ABIS) {
+            if (abi.equals("arm64-v8a")) {
+                return true;
+            }
+        }
+
+        return false;
+    }
+}
diff --git a/common/device-side/util-axt/src/com/android/compatibility/common/util/CtsAndroidTestCase.java b/common/device-side/util-axt/src/com/android/compatibility/common/util/CtsAndroidTestCase.java
new file mode 100644
index 0000000..1ffad1d
--- /dev/null
+++ b/common/device-side/util-axt/src/com/android/compatibility/common/util/CtsAndroidTestCase.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2012 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.compatibility.common.util;
+
+import android.content.Context;
+import android.test.ActivityInstrumentationTestCase2;
+
+/**
+ *  This class emulates AndroidTestCase, but internally it is ActivityInstrumentationTestCase2
+ *  to access Instrumentation.
+ *  DummyActivity is not supposed to be accessed.
+ */
+public class CtsAndroidTestCase extends ActivityInstrumentationTestCase2<DummyActivity> {
+    public CtsAndroidTestCase() {
+        super(DummyActivity.class);
+    }
+
+    public Context getContext() {
+        return getInstrumentation().getContext();
+    }
+}
diff --git a/common/device-side/util-axt/src/com/android/compatibility/common/util/CtsKeyEventUtil.java b/common/device-side/util-axt/src/com/android/compatibility/common/util/CtsKeyEventUtil.java
new file mode 100644
index 0000000..97e4310
--- /dev/null
+++ b/common/device-side/util-axt/src/com/android/compatibility/common/util/CtsKeyEventUtil.java
@@ -0,0 +1,293 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.compatibility.common.util;
+
+import android.app.Instrumentation;
+import android.os.Looper;
+import android.os.SystemClock;
+import android.util.Log;
+import android.view.InputDevice;
+import android.view.KeyCharacterMap;
+import android.view.KeyEvent;
+import android.view.View;
+import android.view.inputmethod.InputMethodManager;
+
+import java.lang.reflect.Field;
+
+/**
+ * Utility class to send KeyEvents bypassing the IME. The code is similar to functions in
+ * {@link Instrumentation} and {@link android.test.InstrumentationTestCase} classes. It uses
+ * {@link InputMethodManager#dispatchKeyEventFromInputMethod(View, KeyEvent)} to send the events.
+ * After sending the events waits for idle.
+ */
+public final class CtsKeyEventUtil {
+
+    private CtsKeyEventUtil() {}
+
+    /**
+     * Sends the key events corresponding to the text to the app being instrumented.
+     *
+     * @param instrumentation the instrumentation used to run the test.
+     * @param targetView View to find the ViewRootImpl and dispatch.
+     * @param text The text to be sent. Null value returns immediately.
+     */
+    public static void sendString(final Instrumentation instrumentation, final View targetView,
+            final String text) {
+        if (text == null) {
+            return;
+        }
+
+        KeyEvent[] events = getKeyEvents(text);
+
+        if (events != null) {
+            for (int i = 0; i < events.length; i++) {
+                // We have to change the time of an event before injecting it because
+                // all KeyEvents returned by KeyCharacterMap.getEvents() have the same
+                // time stamp and the system rejects too old events. Hence, it is
+                // possible for an event to become stale before it is injected if it
+                // takes too long to inject the preceding ones.
+                sendKey(instrumentation, targetView, KeyEvent.changeTimeRepeat(
+                        events[i], SystemClock.uptimeMillis(), 0 /* newRepeat */));
+            }
+        }
+    }
+
+    /**
+     * Sends a series of key events through instrumentation. For instance:
+     * sendKeys(view, KEYCODE_DPAD_LEFT, KEYCODE_DPAD_CENTER).
+     *
+     * @param instrumentation the instrumentation used to run the test.
+     * @param targetView View to find the ViewRootImpl and dispatch.
+     * @param keys The series of key codes.
+     */
+    public static void sendKeys(final Instrumentation instrumentation, final View targetView,
+            final int...keys) {
+        final int count = keys.length;
+
+        for (int i = 0; i < count; i++) {
+            try {
+                sendKeyDownUp(instrumentation, targetView, keys[i]);
+            } catch (SecurityException e) {
+                // Ignore security exceptions that are now thrown
+                // when trying to send to another app, to retain
+                // compatibility with existing tests.
+            }
+        }
+    }
+
+    /**
+     * Sends a series of key events through instrumentation. The sequence of keys is a string
+     * containing the key names as specified in KeyEvent, without the KEYCODE_ prefix. For
+     * instance: sendKeys(view, "DPAD_LEFT A B C DPAD_CENTER"). Each key can be repeated by using
+     * the N* prefix. For instance, to send two KEYCODE_DPAD_LEFT, use the following:
+     * sendKeys(view, "2*DPAD_LEFT").
+     *
+     * @param instrumentation the instrumentation used to run the test.
+     * @param targetView View to find the ViewRootImpl and dispatch.
+     * @param keysSequence The sequence of keys.
+     */
+    public static void sendKeys(final Instrumentation instrumentation, final View targetView,
+            final String keysSequence) {
+        final String[] keys = keysSequence.split(" ");
+        final int count = keys.length;
+
+        for (int i = 0; i < count; i++) {
+            String key = keys[i];
+            int repeater = key.indexOf('*');
+
+            int keyCount;
+            try {
+                keyCount = repeater == -1 ? 1 : Integer.parseInt(key.substring(0, repeater));
+            } catch (NumberFormatException e) {
+                Log.w("ActivityTestCase", "Invalid repeat count: " + key);
+                continue;
+            }
+
+            if (repeater != -1) {
+                key = key.substring(repeater + 1);
+            }
+
+            for (int j = 0; j < keyCount; j++) {
+                try {
+                    final Field keyCodeField = KeyEvent.class.getField("KEYCODE_" + key);
+                    final int keyCode = keyCodeField.getInt(null);
+                    try {
+                        sendKeyDownUp(instrumentation, targetView, keyCode);
+                    } catch (SecurityException e) {
+                        // Ignore security exceptions that are now thrown
+                        // when trying to send to another app, to retain
+                        // compatibility with existing tests.
+                    }
+                } catch (NoSuchFieldException e) {
+                    Log.w("ActivityTestCase", "Unknown keycode: KEYCODE_" + key);
+                    break;
+                } catch (IllegalAccessException e) {
+                    Log.w("ActivityTestCase", "Unknown keycode: KEYCODE_" + key);
+                    break;
+                }
+            }
+        }
+    }
+
+    /**
+     * Sends an up and down key events.
+     *
+     * @param instrumentation the instrumentation used to run the test.
+     * @param targetView View to find the ViewRootImpl and dispatch.
+     * @param key The integer keycode for the event to be sent.
+     */
+    public static void sendKeyDownUp(final Instrumentation instrumentation, final View targetView,
+            final int key) {
+        sendKey(instrumentation, targetView, new KeyEvent(KeyEvent.ACTION_DOWN, key));
+        sendKey(instrumentation, targetView, new KeyEvent(KeyEvent.ACTION_UP, key));
+    }
+
+    /**
+     * Sends a key event.
+     *
+     * @param instrumentation the instrumentation used to run the test.
+     * @param targetView View to find the ViewRootImpl and dispatch.
+     * @param event KeyEvent to be send.
+     */
+    public static void sendKey(final Instrumentation instrumentation, final View targetView,
+            final KeyEvent event) {
+        validateNotAppThread();
+
+        long downTime = event.getDownTime();
+        long eventTime = event.getEventTime();
+        int action = event.getAction();
+        int code = event.getKeyCode();
+        int repeatCount = event.getRepeatCount();
+        int metaState = event.getMetaState();
+        int deviceId = event.getDeviceId();
+        int scanCode = event.getScanCode();
+        int source = event.getSource();
+        int flags = event.getFlags();
+        if (source == InputDevice.SOURCE_UNKNOWN) {
+            source = InputDevice.SOURCE_KEYBOARD;
+        }
+        if (eventTime == 0) {
+            eventTime = SystemClock.uptimeMillis();
+        }
+        if (downTime == 0) {
+            downTime = eventTime;
+        }
+
+        final KeyEvent newEvent = new KeyEvent(downTime, eventTime, action, code, repeatCount,
+                metaState, deviceId, scanCode, flags, source);
+
+        InputMethodManager imm = targetView.getContext().getSystemService(InputMethodManager.class);
+        imm.dispatchKeyEventFromInputMethod(null, newEvent);
+        instrumentation.waitForIdleSync();
+    }
+
+    /**
+     * Sends a key event while holding another modifier key down, then releases both keys and
+     * waits for idle sync. Useful for sending combinations like shift + tab.
+     *
+     * @param instrumentation the instrumentation used to run the test.
+     * @param targetView View to find the ViewRootImpl and dispatch.
+     * @param keyCodeToSend The integer keycode for the event to be sent.
+     * @param modifierKeyCodeToHold The integer keycode of the modifier to be held.
+     */
+    public static void sendKeyWhileHoldingModifier(final Instrumentation instrumentation,
+            final View targetView, final int keyCodeToSend,
+            final int modifierKeyCodeToHold) {
+        final int metaState = getMetaStateForModifierKeyCode(modifierKeyCodeToHold);
+        final long downTime = SystemClock.uptimeMillis();
+
+        final KeyEvent holdKeyDown = new KeyEvent(downTime, downTime, KeyEvent.ACTION_DOWN,
+                modifierKeyCodeToHold, 0 /* repeat */);
+        sendKey(instrumentation ,targetView, holdKeyDown);
+
+        final KeyEvent keyDown = new KeyEvent(downTime, downTime, KeyEvent.ACTION_DOWN,
+                keyCodeToSend, 0 /* repeat */, metaState);
+        sendKey(instrumentation, targetView, keyDown);
+
+        final KeyEvent keyUp = new KeyEvent(downTime, downTime, KeyEvent.ACTION_UP,
+                keyCodeToSend, 0 /* repeat */, metaState);
+        sendKey(instrumentation, targetView, keyUp);
+
+        final KeyEvent holdKeyUp = new KeyEvent(downTime, downTime, KeyEvent.ACTION_UP,
+                modifierKeyCodeToHold, 0 /* repeat */);
+        sendKey(instrumentation, targetView, holdKeyUp);
+
+        instrumentation.waitForIdleSync();
+    }
+
+    private static int getMetaStateForModifierKeyCode(int modifierKeyCode) {
+        if (!KeyEvent.isModifierKey(modifierKeyCode)) {
+            throw new IllegalArgumentException("Modifier key expected, but got: "
+                    + KeyEvent.keyCodeToString(modifierKeyCode));
+        }
+
+        int metaState;
+        switch (modifierKeyCode) {
+            case KeyEvent.KEYCODE_SHIFT_LEFT:
+                metaState = KeyEvent.META_SHIFT_LEFT_ON;
+                break;
+            case KeyEvent.KEYCODE_SHIFT_RIGHT:
+                metaState = KeyEvent.META_SHIFT_RIGHT_ON;
+                break;
+            case KeyEvent.KEYCODE_ALT_LEFT:
+                metaState = KeyEvent.META_ALT_LEFT_ON;
+                break;
+            case KeyEvent.KEYCODE_ALT_RIGHT:
+                metaState = KeyEvent.META_ALT_RIGHT_ON;
+                break;
+            case KeyEvent.KEYCODE_CTRL_LEFT:
+                metaState = KeyEvent.META_CTRL_LEFT_ON;
+                break;
+            case KeyEvent.KEYCODE_CTRL_RIGHT:
+                metaState = KeyEvent.META_CTRL_RIGHT_ON;
+                break;
+            case KeyEvent.KEYCODE_META_LEFT:
+                metaState = KeyEvent.META_META_LEFT_ON;
+                break;
+            case KeyEvent.KEYCODE_META_RIGHT:
+                metaState = KeyEvent.META_META_RIGHT_ON;
+                break;
+            case KeyEvent.KEYCODE_SYM:
+                metaState = KeyEvent.META_SYM_ON;
+                break;
+            case KeyEvent.KEYCODE_NUM:
+                metaState = KeyEvent.META_NUM_LOCK_ON;
+                break;
+            case KeyEvent.KEYCODE_FUNCTION:
+                metaState = KeyEvent.META_FUNCTION_ON;
+                break;
+            default:
+                // Safety net: all modifier keys need to have at least one meta state associated.
+                throw new UnsupportedOperationException("No meta state associated with "
+                        + "modifier key: " + KeyEvent.keyCodeToString(modifierKeyCode));
+        }
+
+        return KeyEvent.normalizeMetaState(metaState);
+    }
+
+    private static KeyEvent[] getKeyEvents(final String text) {
+        KeyCharacterMap keyCharacterMap = KeyCharacterMap.load(KeyCharacterMap.VIRTUAL_KEYBOARD);
+        return keyCharacterMap.getEvents(text.toCharArray());
+    }
+
+    private static void validateNotAppThread() {
+        if (Looper.myLooper() == Looper.getMainLooper()) {
+            throw new RuntimeException(
+                    "This method can not be called from the main application thread");
+        }
+    }
+}
diff --git a/common/device-side/util-axt/src/com/android/compatibility/common/util/CtsMockitoUtils.java b/common/device-side/util-axt/src/com/android/compatibility/common/util/CtsMockitoUtils.java
new file mode 100644
index 0000000..54985dc
--- /dev/null
+++ b/common/device-side/util-axt/src/com/android/compatibility/common/util/CtsMockitoUtils.java
@@ -0,0 +1,26 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.compatibility.common.util;
+
+import org.mockito.verification.VerificationMode;
+
+public class CtsMockitoUtils {
+    private CtsMockitoUtils() {}
+
+    public static VerificationMode within(long timeout) {
+        return new Within(timeout);
+    }
+}
diff --git a/common/device-side/util-axt/src/com/android/compatibility/common/util/CtsMouseUtil.java b/common/device-side/util-axt/src/com/android/compatibility/common/util/CtsMouseUtil.java
new file mode 100644
index 0000000..99228fe
--- /dev/null
+++ b/common/device-side/util-axt/src/com/android/compatibility/common/util/CtsMouseUtil.java
@@ -0,0 +1,130 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.compatibility.common.util;
+
+import static org.mockito.Matchers.argThat;
+import static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.inOrder;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+
+import android.os.SystemClock;
+import android.view.InputDevice;
+import android.view.MotionEvent;
+import android.view.View;
+
+import org.mockito.ArgumentMatcher;
+import org.mockito.InOrder;
+
+public final class CtsMouseUtil {
+
+    private CtsMouseUtil() {}
+
+    public static View.OnHoverListener installHoverListener(View view) {
+        return installHoverListener(view, true);
+    }
+
+    public static View.OnHoverListener installHoverListener(View view, boolean result) {
+        final View.OnHoverListener mockListener = mock(View.OnHoverListener.class);
+        view.setOnHoverListener((v, event) -> {
+            // Clone the event to work around event instance reuse in the framework.
+            mockListener.onHover(v, MotionEvent.obtain(event));
+            return result;
+        });
+        return mockListener;
+    }
+
+    public static void clearHoverListener(View view) {
+        view.setOnHoverListener(null);
+    }
+
+    public static MotionEvent obtainMouseEvent(int action, View anchor, int offsetX, int offsetY) {
+        final long eventTime = SystemClock.uptimeMillis();
+        final int[] screenPos = new int[2];
+        anchor.getLocationOnScreen(screenPos);
+        final int x = screenPos[0] + offsetX;
+        final int y = screenPos[1] + offsetY;
+        MotionEvent event = MotionEvent.obtain(eventTime, eventTime, action, x, y, 0);
+        event.setSource(InputDevice.SOURCE_MOUSE);
+        return event;
+    }
+
+    public static class ActionMatcher implements ArgumentMatcher<MotionEvent> {
+        private final int mAction;
+
+        public ActionMatcher(int action) {
+            mAction = action;
+        }
+
+        @Override
+        public boolean matches(MotionEvent actual) {
+            return actual.getAction() == mAction;
+        }
+
+        @Override
+        public String toString() {
+            return "action=" + MotionEvent.actionToString(mAction);
+        }
+    }
+
+    public static class PositionMatcher extends ActionMatcher {
+        private final int mX;
+        private final int mY;
+
+        public PositionMatcher(int action, int x, int y) {
+            super(action);
+            mX = x;
+            mY = y;
+        }
+
+        @Override
+        public boolean matches(MotionEvent actual) {
+            return super.matches(actual)
+                    && ((int) actual.getX()) == mX
+                    && ((int) actual.getY()) == mY;
+        }
+
+        @Override
+        public String toString() {
+            return super.toString() + "@(" + mX + "," + mY + ")";
+        }
+    }
+
+    public static void verifyEnterMove(View.OnHoverListener listener, View view, int moveCount) {
+        final InOrder inOrder = inOrder(listener);
+        verifyEnterMoveInternal(listener, view, moveCount, inOrder);
+        inOrder.verifyNoMoreInteractions();
+    }
+
+    public static void verifyEnterMoveExit(
+            View.OnHoverListener listener, View view, int moveCount) {
+        final InOrder inOrder = inOrder(listener);
+        verifyEnterMoveInternal(listener, view, moveCount, inOrder);
+        inOrder.verify(listener, times(1)).onHover(eq(view),
+                argThat(new ActionMatcher(MotionEvent.ACTION_HOVER_EXIT)));
+        inOrder.verifyNoMoreInteractions();
+    }
+
+    private static void verifyEnterMoveInternal(
+            View.OnHoverListener listener, View view, int moveCount, InOrder inOrder) {
+        inOrder.verify(listener, times(1)).onHover(eq(view),
+                argThat(new ActionMatcher(MotionEvent.ACTION_HOVER_ENTER)));
+        inOrder.verify(listener, times(moveCount)).onHover(eq(view),
+                argThat(new ActionMatcher(MotionEvent.ACTION_HOVER_MOVE)));
+    }
+}
+
diff --git a/common/device-side/util-axt/src/com/android/compatibility/common/util/CtsTouchUtils.java b/common/device-side/util-axt/src/com/android/compatibility/common/util/CtsTouchUtils.java
new file mode 100644
index 0000000..16796a8
--- /dev/null
+++ b/common/device-side/util-axt/src/com/android/compatibility/common/util/CtsTouchUtils.java
@@ -0,0 +1,568 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.compatibility.common.util;
+
+import android.app.Instrumentation;
+import android.app.UiAutomation;
+import android.graphics.Point;
+import android.os.SystemClock;
+import android.util.SparseArray;
+import android.view.InputDevice;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewConfiguration;
+import android.view.ViewGroup;
+
+/**
+ * Test utilities for touch emulation.
+ */
+public final class CtsTouchUtils {
+    /**
+     * Interface definition for a callback to be invoked when an event has been injected.
+     */
+    public interface EventInjectionListener {
+        /**
+         * Callback method to be invoked when a {MotionEvent#ACTION_DOWN} has been injected.
+         * @param xOnScreen X coordinate of the injected event.
+         * @param yOnScreen Y coordinate of the injected event.
+         */
+        public void onDownInjected(int xOnScreen, int yOnScreen);
+
+        /**
+         * Callback method to be invoked when a {MotionEvent#ACTION_MOVE} has been injected.
+         * @param xOnScreen X coordinates of the injected event.
+         * @param yOnScreen Y coordinates of the injected event.
+         */
+        public void onMoveInjected(int[] xOnScreen, int[] yOnScreen);
+
+        /**
+         * Callback method to be invoked when a {MotionEvent#ACTION_UP} has been injected.
+         * @param xOnScreen X coordinate of the injected event.
+         * @param yOnScreen Y coordinate of the injected event.
+         */
+        public void onUpInjected(int xOnScreen, int yOnScreen);
+    }
+
+    private CtsTouchUtils() {}
+
+    /**
+     * Emulates a tap in the center of the passed {@link View}.
+     *
+     * @param instrumentation the instrumentation used to run the test
+     * @param view the view to "tap"
+     */
+    public static void emulateTapOnViewCenter(Instrumentation instrumentation, View view) {
+        emulateTapOnView(instrumentation, view, view.getWidth() / 2, view.getHeight() / 2);
+    }
+
+    /**
+     * Emulates a tap on a point relative to the top-left corner of the passed {@link View}. Offset
+     * parameters are used to compute the final screen coordinates of the tap point.
+     *
+     * @param instrumentation the instrumentation used to run the test
+     * @param anchorView the anchor view to determine the tap location on the screen
+     * @param offsetX extra X offset for the tap
+     * @param offsetY extra Y offset for the tap
+     */
+    public static void emulateTapOnView(Instrumentation instrumentation, View anchorView,
+            int offsetX, int offsetY) {
+        final int touchSlop = ViewConfiguration.get(anchorView.getContext()).getScaledTouchSlop();
+        // Get anchor coordinates on the screen
+        final int[] viewOnScreenXY = new int[2];
+        anchorView.getLocationOnScreen(viewOnScreenXY);
+        int xOnScreen = viewOnScreenXY[0] + offsetX;
+        int yOnScreen = viewOnScreenXY[1] + offsetY;
+        final UiAutomation uiAutomation = instrumentation.getUiAutomation();
+        final long downTime = SystemClock.uptimeMillis();
+
+        injectDownEvent(uiAutomation, downTime, xOnScreen, yOnScreen, null);
+        injectMoveEventForTap(uiAutomation, downTime, touchSlop, xOnScreen, yOnScreen);
+        injectUpEvent(uiAutomation, downTime, false, xOnScreen, yOnScreen, null);
+
+        // Wait for the system to process all events in the queue
+        instrumentation.waitForIdleSync();
+    }
+
+    /**
+     * Emulates a double tap in the center of the passed {@link View}.
+     *
+     * @param instrumentation the instrumentation used to run the test
+     * @param view the view to "double tap"
+     */
+    public static void emulateDoubleTapOnViewCenter(Instrumentation instrumentation, View view) {
+        emulateDoubleTapOnView(instrumentation, view, view.getWidth() / 2, view.getHeight() / 2);
+    }
+
+    /**
+     * Emulates a double tap on a point relative to the top-left corner of the passed {@link View}.
+     * Offset parameters are used to compute the final screen coordinates of the tap points.
+     *
+     * @param instrumentation the instrumentation used to run the test
+     * @param anchorView the anchor view to determine the tap location on the screen
+     * @param offsetX extra X offset for the taps
+     * @param offsetY extra Y offset for the taps
+     */
+    public static void emulateDoubleTapOnView(Instrumentation instrumentation, View anchorView,
+            int offsetX, int offsetY) {
+        final int touchSlop = ViewConfiguration.get(anchorView.getContext()).getScaledTouchSlop();
+        // Get anchor coordinates on the screen
+        final int[] viewOnScreenXY = new int[2];
+        anchorView.getLocationOnScreen(viewOnScreenXY);
+        int xOnScreen = viewOnScreenXY[0] + offsetX;
+        int yOnScreen = viewOnScreenXY[1] + offsetY;
+        final UiAutomation uiAutomation = instrumentation.getUiAutomation();
+        final long downTime = SystemClock.uptimeMillis();
+
+        injectDownEvent(uiAutomation, downTime, xOnScreen, yOnScreen, null);
+        injectMoveEventForTap(uiAutomation, downTime, touchSlop, xOnScreen, yOnScreen);
+        injectUpEvent(uiAutomation, downTime, false, xOnScreen, yOnScreen, null);
+        injectDownEvent(uiAutomation, downTime, xOnScreen, yOnScreen, null);
+        injectMoveEventForTap(uiAutomation, downTime, touchSlop, xOnScreen, yOnScreen);
+        injectUpEvent(uiAutomation, downTime, false, xOnScreen, yOnScreen, null);
+
+        // Wait for the system to process all events in the queue
+        instrumentation.waitForIdleSync();
+    }
+
+    /**
+     * Emulates a linear drag gesture between 2 points across the screen.
+     *
+     * @param instrumentation the instrumentation used to run the test
+     * @param dragStartX Start X of the emulated drag gesture
+     * @param dragStartY Start Y of the emulated drag gesture
+     * @param dragAmountX X amount of the emulated drag gesture
+     * @param dragAmountY Y amount of the emulated drag gesture
+     */
+    public static void emulateDragGesture(Instrumentation instrumentation,
+            int dragStartX, int dragStartY, int dragAmountX, int dragAmountY) {
+        emulateDragGesture(instrumentation, dragStartX, dragStartY, dragAmountX, dragAmountY,
+                2000, 20, null);
+    }
+
+    private static void emulateDragGesture(Instrumentation instrumentation,
+            int dragStartX, int dragStartY, int dragAmountX, int dragAmountY,
+            int dragDurationMs, int moveEventCount) {
+        emulateDragGesture(instrumentation, dragStartX, dragStartY, dragAmountX, dragAmountY,
+                dragDurationMs, moveEventCount, null);
+    }
+
+    private static void emulateDragGesture(Instrumentation instrumentation,
+            int dragStartX, int dragStartY, int dragAmountX, int dragAmountY,
+            int dragDurationMs, int moveEventCount,
+            EventInjectionListener eventInjectionListener) {
+        // We are using the UiAutomation object to inject events so that drag works
+        // across view / window boundaries (such as for the emulated drag and drop
+        // sequences)
+        final UiAutomation uiAutomation = instrumentation.getUiAutomation();
+        final long downTime = SystemClock.uptimeMillis();
+
+        injectDownEvent(uiAutomation, downTime, dragStartX, dragStartY, eventInjectionListener);
+
+        // Inject a sequence of MOVE events that emulate the "move" part of the gesture
+        injectMoveEventsForDrag(uiAutomation, downTime, true, dragStartX, dragStartY,
+                dragStartX + dragAmountX, dragStartY + dragAmountY, moveEventCount, dragDurationMs,
+            eventInjectionListener);
+
+        injectUpEvent(uiAutomation, downTime, true, dragStartX + dragAmountX,
+                dragStartY + dragAmountY, eventInjectionListener);
+
+        // Wait for the system to process all events in the queue
+        instrumentation.waitForIdleSync();
+    }
+
+    /**
+     * Emulates a series of linear drag gestures across the screen between multiple points without
+     * lifting the finger. Note that this function does not support curve movements between the
+     * points.
+     *
+     * @param instrumentation the instrumentation used to run the test
+     * @param coordinates the ordered list of points for the drag gesture
+     */
+    public static void emulateDragGesture(Instrumentation instrumentation,
+            SparseArray<Point> coordinates) {
+        emulateDragGesture(instrumentation, coordinates, 2000, 20);
+    }
+
+    private static void emulateDragGesture(Instrumentation instrumentation,
+            SparseArray<Point> coordinates, int dragDurationMs, int moveEventCount) {
+        final int coordinatesSize = coordinates.size();
+        if (coordinatesSize < 2) {
+            throw new IllegalArgumentException("Need at least 2 points for emulating drag");
+        }
+        // We are using the UiAutomation object to inject events so that drag works
+        // across view / window boundaries (such as for the emulated drag and drop
+        // sequences)
+        final UiAutomation uiAutomation = instrumentation.getUiAutomation();
+        final long downTime = SystemClock.uptimeMillis();
+
+        injectDownEvent(uiAutomation, downTime, coordinates.get(0).x, coordinates.get(0).y, null);
+
+        // Move to each coordinate.
+        for (int i = 0; i < coordinatesSize - 1; i++) {
+            // Inject a sequence of MOVE events that emulate the "move" part of the gesture.
+            injectMoveEventsForDrag(uiAutomation,
+                    downTime,
+                    true,
+                    coordinates.get(i).x,
+                    coordinates.get(i).y,
+                    coordinates.get(i + 1).x,
+                    coordinates.get(i + 1).y,
+                    moveEventCount,
+                    dragDurationMs,
+                    null);
+        }
+
+        injectUpEvent(uiAutomation,
+                downTime,
+                true,
+                coordinates.get(coordinatesSize - 1).x,
+                coordinates.get(coordinatesSize - 1).y,
+                null);
+
+        // Wait for the system to process all events in the queue
+        instrumentation.waitForIdleSync();
+    }
+
+    private static long injectDownEvent(UiAutomation uiAutomation, long downTime, int xOnScreen,
+            int yOnScreen, EventInjectionListener eventInjectionListener) {
+        MotionEvent eventDown = MotionEvent.obtain(
+                downTime, downTime, MotionEvent.ACTION_DOWN, xOnScreen, yOnScreen, 1);
+        eventDown.setSource(InputDevice.SOURCE_TOUCHSCREEN);
+        uiAutomation.injectInputEvent(eventDown, true);
+        if (eventInjectionListener != null) {
+            eventInjectionListener.onDownInjected(xOnScreen, yOnScreen);
+        }
+        eventDown.recycle();
+        return downTime;
+    }
+
+    private static void injectMoveEventForTap(UiAutomation uiAutomation, long downTime,
+            int touchSlop, int xOnScreen, int yOnScreen) {
+        MotionEvent eventMove = MotionEvent.obtain(downTime, downTime, MotionEvent.ACTION_MOVE,
+                xOnScreen + (touchSlop / 2.0f), yOnScreen + (touchSlop / 2.0f), 1);
+        eventMove.setSource(InputDevice.SOURCE_TOUCHSCREEN);
+        uiAutomation.injectInputEvent(eventMove, true);
+        eventMove.recycle();
+    }
+
+    private static void injectMoveEventsForDrag(UiAutomation uiAutomation, long downTime,
+            boolean useCurrentEventTime, int dragStartX, int dragStartY, int dragEndX, int dragEndY,
+            int moveEventCount, int dragDurationMs, EventInjectionListener eventInjectionListener) {
+        final int dragAmountX = dragEndX - dragStartX;
+        final int dragAmountY = dragEndY - dragStartY;
+        final int sleepTime = dragDurationMs / moveEventCount;
+
+        // sleep for a bit to emulate the overall drag gesture.
+        long prevEventTime = downTime;
+        SystemClock.sleep(sleepTime);
+        for (int i = 0; i < moveEventCount; i++) {
+            // Note that the first MOVE event is generated "away" from the coordinates
+            // of the start / DOWN event, and the last MOVE event is generated
+            // at the same coordinates as the subsequent UP event.
+            final int moveX = dragStartX + dragAmountX * (i  + 1) / moveEventCount;
+            final int moveY = dragStartY + dragAmountY * (i  + 1) / moveEventCount;
+            long eventTime = useCurrentEventTime ? SystemClock.uptimeMillis() : downTime;
+
+            // If necessary, generate history for our next MOVE event. The history is generated
+            // to be spaced at 10 millisecond intervals, interpolating the coordinates from the
+            // last generated MOVE event to our current one.
+            int historyEventCount = (int) ((eventTime - prevEventTime) / 10);
+            int[] xCoordsForListener = (eventInjectionListener == null) ? null :
+                    new int[Math.max(1, historyEventCount)];
+            int[] yCoordsForListener = (eventInjectionListener == null) ? null :
+                    new int[Math.max(1, historyEventCount)];
+            MotionEvent eventMove = null;
+            if (historyEventCount == 0) {
+                eventMove = MotionEvent.obtain(
+                        downTime, eventTime, MotionEvent.ACTION_MOVE, moveX, moveY, 1);
+                if (eventInjectionListener != null) {
+                    xCoordsForListener[0] = moveX;
+                    yCoordsForListener[0] = moveY;
+                }
+            } else {
+                final int prevMoveX = dragStartX + dragAmountX * i / moveEventCount;
+                final int prevMoveY = dragStartY + dragAmountY * i / moveEventCount;
+                final int deltaMoveX = moveX - prevMoveX;
+                final int deltaMoveY = moveY - prevMoveY;
+                final long deltaTime = (eventTime - prevEventTime);
+                for (int historyIndex = 0; historyIndex < historyEventCount; historyIndex++) {
+                    int stepMoveX = prevMoveX + deltaMoveX * (historyIndex + 1) / historyEventCount;
+                    int stepMoveY = prevMoveY + deltaMoveY * (historyIndex + 1) / historyEventCount;
+                    long stepEventTime = useCurrentEventTime
+                            ? prevEventTime + deltaTime * (historyIndex + 1) / historyEventCount
+                            : downTime;
+                    if (historyIndex == 0) {
+                        // Generate the first event in our sequence
+                        eventMove = MotionEvent.obtain(downTime, stepEventTime,
+                                MotionEvent.ACTION_MOVE, stepMoveX, stepMoveY, 1);
+                    } else {
+                        // and then add to it
+                        eventMove.addBatch(stepEventTime, stepMoveX, stepMoveY, 1.0f, 1.0f, 1);
+                    }
+                    if (eventInjectionListener != null) {
+                        xCoordsForListener[historyIndex] = stepMoveX;
+                        yCoordsForListener[historyIndex] = stepMoveY;
+                    }
+                }
+            }
+
+            eventMove.setSource(InputDevice.SOURCE_TOUCHSCREEN);
+            uiAutomation.injectInputEvent(eventMove, true);
+            if (eventInjectionListener != null) {
+                eventInjectionListener.onMoveInjected(xCoordsForListener, yCoordsForListener);
+            }
+            eventMove.recycle();
+            prevEventTime = eventTime;
+
+            // sleep for a bit to emulate the overall drag gesture.
+            SystemClock.sleep(sleepTime);
+        }
+    }
+
+    private static void injectUpEvent(UiAutomation uiAutomation, long downTime,
+            boolean useCurrentEventTime, int xOnScreen, int yOnScreen,
+            EventInjectionListener eventInjectionListener) {
+        long eventTime = useCurrentEventTime ? SystemClock.uptimeMillis() : downTime;
+        MotionEvent eventUp = MotionEvent.obtain(
+                downTime, eventTime, MotionEvent.ACTION_UP, xOnScreen, yOnScreen, 1);
+        eventUp.setSource(InputDevice.SOURCE_TOUCHSCREEN);
+        uiAutomation.injectInputEvent(eventUp, true);
+        if (eventInjectionListener != null) {
+            eventInjectionListener.onUpInjected(xOnScreen, yOnScreen);
+        }
+        eventUp.recycle();
+    }
+
+    /**
+     * Emulates a fling gesture across the horizontal center of the passed view.
+     *
+     * @param instrumentation the instrumentation used to run the test
+     * @param view the view to fling
+     * @param isDownwardsFlingGesture if <code>true</code>, the emulated fling will
+     *      be a downwards gesture
+     * @return The vertical amount of emulated fling in pixels
+     */
+    public static int emulateFlingGesture(Instrumentation instrumentation,
+            View view, boolean isDownwardsFlingGesture) {
+        return emulateFlingGesture(instrumentation, view, isDownwardsFlingGesture, null);
+    }
+
+    /**
+     * Emulates a fling gesture across the horizontal center of the passed view.
+     *
+     * @param instrumentation the instrumentation used to run the test
+     * @param view the view to fling
+     * @param isDownwardsFlingGesture if <code>true</code>, the emulated fling will
+     *      be a downwards gesture
+     * @param eventInjectionListener optional listener to notify about the injected events
+     * @return The vertical amount of emulated fling in pixels
+     */
+    public static int emulateFlingGesture(Instrumentation instrumentation,
+            View view, boolean isDownwardsFlingGesture,
+            EventInjectionListener eventInjectionListener) {
+        final ViewConfiguration configuration = ViewConfiguration.get(view.getContext());
+        final int flingVelocity = (configuration.getScaledMinimumFlingVelocity() +
+                configuration.getScaledMaximumFlingVelocity()) / 2;
+        // Get view coordinates on the screen
+        final int[] viewOnScreenXY = new int[2];
+        view.getLocationOnScreen(viewOnScreenXY);
+
+        // Our fling gesture will be from 25% height of the view to 75% height of the view
+        // for downwards fling gesture, and the other way around for upwards fling gesture
+        final int viewHeight = view.getHeight();
+        final int x = viewOnScreenXY[0] + view.getWidth() / 2;
+        final int startY = isDownwardsFlingGesture ? viewOnScreenXY[1] + viewHeight / 4
+                : viewOnScreenXY[1] + 3 * viewHeight / 4;
+        final int amountY = isDownwardsFlingGesture ? viewHeight / 2 : -viewHeight / 2;
+
+        // Compute fling gesture duration based on the distance (50% height of the view) and
+        // fling velocity
+        final int durationMs = (1000 * viewHeight) / (2 * flingVelocity);
+
+        // And do the same event injection sequence as our generic drag gesture
+        emulateDragGesture(instrumentation, x, startY, 0, amountY, durationMs, durationMs / 16,
+            eventInjectionListener);
+
+        return amountY;
+    }
+
+    private static class ViewStateSnapshot {
+        final View mFirst;
+        final View mLast;
+        final int mFirstTop;
+        final int mLastBottom;
+        final int mChildCount;
+        private ViewStateSnapshot(ViewGroup viewGroup) {
+            mChildCount = viewGroup.getChildCount();
+            if (mChildCount == 0) {
+                mFirst = mLast = null;
+                mFirstTop = mLastBottom = Integer.MIN_VALUE;
+            } else {
+                mFirst = viewGroup.getChildAt(0);
+                mLast = viewGroup.getChildAt(mChildCount - 1);
+                mFirstTop = mFirst.getTop();
+                mLastBottom = mLast.getBottom();
+            }
+        }
+
+        @Override
+        public boolean equals(Object o) {
+            if (this == o) {
+                return true;
+            }
+            if (o == null || getClass() != o.getClass()) {
+                return false;
+            }
+
+            final ViewStateSnapshot that = (ViewStateSnapshot) o;
+            return mFirstTop == that.mFirstTop &&
+                    mLastBottom == that.mLastBottom &&
+                    mFirst == that.mFirst &&
+                    mLast == that.mLast &&
+                    mChildCount == that.mChildCount;
+        }
+
+        @Override
+        public int hashCode() {
+            int result = mFirst != null ? mFirst.hashCode() : 0;
+            result = 31 * result + (mLast != null ? mLast.hashCode() : 0);
+            result = 31 * result + mFirstTop;
+            result = 31 * result + mLastBottom;
+            result = 31 * result + mChildCount;
+            return result;
+        }
+    }
+
+    /**
+     * Emulates a scroll to the bottom of the specified {@link ViewGroup}.
+     *
+     * @param instrumentation the instrumentation used to run the test
+     * @param viewGroup View group
+     */
+    public static void emulateScrollToBottom(Instrumentation instrumentation, ViewGroup viewGroup) {
+        final int[] viewGroupOnScreenXY = new int[2];
+        viewGroup.getLocationOnScreen(viewGroupOnScreenXY);
+
+        final int emulatedX = viewGroupOnScreenXY[0] + viewGroup.getWidth() / 2;
+        final int emulatedStartY = viewGroupOnScreenXY[1] + 3 * viewGroup.getHeight() / 4;
+        final int swipeAmount = viewGroup.getHeight() / 2;
+
+        ViewStateSnapshot prev;
+        ViewStateSnapshot next = new ViewStateSnapshot(viewGroup);
+        do {
+            prev = next;
+            emulateDragGesture(instrumentation, emulatedX, emulatedStartY, 0, -swipeAmount,
+                    300, 10);
+            next = new ViewStateSnapshot(viewGroup);
+        } while (!prev.equals(next));
+    }
+
+    /**
+     * Emulates a long press in the center of the passed {@link View}.
+     *
+     * @param instrumentation the instrumentation used to run the test
+     * @param view the view to "long press"
+     */
+    public static void emulateLongPressOnViewCenter(Instrumentation instrumentation, View view) {
+        emulateLongPressOnViewCenter(instrumentation, view, 0);
+    }
+
+    /**
+     * Emulates a long press in the center of the passed {@link View}.
+     *
+     * @param instrumentation the instrumentation used to run the test
+     * @param view the view to "long press"
+     * @param extraWaitMs the duration of emulated "long press" in milliseconds starting
+     *      after system-level long press timeout.
+     */
+    public static void emulateLongPressOnViewCenter(Instrumentation instrumentation, View view,
+            long extraWaitMs) {
+        final int touchSlop = ViewConfiguration.get(view.getContext()).getScaledTouchSlop();
+        // Use instrumentation to emulate a tap on the spinner to bring down its popup
+        final int[] viewOnScreenXY = new int[2];
+        view.getLocationOnScreen(viewOnScreenXY);
+        int xOnScreen = viewOnScreenXY[0] + view.getWidth() / 2;
+        int yOnScreen = viewOnScreenXY[1] + view.getHeight() / 2;
+
+        emulateLongPressOnScreen(
+                instrumentation, xOnScreen, yOnScreen, touchSlop, extraWaitMs, true);
+    }
+
+    /**
+     * Emulates a long press confirmed on a point relative to the top-left corner of the passed
+     * {@link View}. Offset parameters are used to compute the final screen coordinates of the
+     * press point.
+     *
+     * @param instrumentation the instrumentation used to run the test
+     * @param view the view to "long press"
+     * @param offsetX extra X offset for the tap
+     * @param offsetY extra Y offset for the tap
+     */
+    public static void emulateLongPressOnView(Instrumentation instrumentation, View view,
+            int offsetX, int offsetY) {
+        final int touchSlop = ViewConfiguration.get(view.getContext()).getScaledTouchSlop();
+        final int[] viewOnScreenXY = new int[2];
+        view.getLocationOnScreen(viewOnScreenXY);
+        int xOnScreen = viewOnScreenXY[0] + offsetX;
+        int yOnScreen = viewOnScreenXY[1] + offsetY;
+
+        emulateLongPressOnScreen(instrumentation, xOnScreen, yOnScreen, touchSlop, 0, true);
+    }
+
+    /**
+     * Emulates a long press then a linear drag gesture between 2 points across the screen.
+     * This is used for drag selection.
+     *
+     * @param instrumentation the instrumentation used to run the test
+     * @param dragStartX Start X of the emulated drag gesture
+     * @param dragStartY Start Y of the emulated drag gesture
+     * @param dragAmountX X amount of the emulated drag gesture
+     * @param dragAmountY Y amount of the emulated drag gesture
+     */
+    public static void emulateLongPressAndDragGesture(Instrumentation instrumentation,
+            int dragStartX, int dragStartY, int dragAmountX, int dragAmountY) {
+        emulateLongPressOnScreen(instrumentation, dragStartX, dragStartY,
+                0 /* touchSlop */, 0 /* extraWaitMs */, false /* upGesture */);
+        emulateDragGesture(instrumentation, dragStartX, dragStartY, dragAmountX, dragAmountY);
+    }
+
+    /**
+     * Emulates a long press on the screen.
+     *
+     * @param instrumentation the instrumentation used to run the test
+     * @param xOnScreen X position on screen for the "long press"
+     * @param yOnScreen Y position on screen for the "long press"
+     * @param extraWaitMs extra duration of emulated long press in milliseconds added
+     *        after the system-level "long press" timeout.
+     * @param upGesture whether to include an up event.
+     */
+    private static void emulateLongPressOnScreen(Instrumentation instrumentation,
+            int xOnScreen, int yOnScreen, int touchSlop, long extraWaitMs, boolean upGesture) {
+        final UiAutomation uiAutomation = instrumentation.getUiAutomation();
+        final long downTime = SystemClock.uptimeMillis();
+
+        injectDownEvent(uiAutomation, downTime, xOnScreen, yOnScreen, null);
+        injectMoveEventForTap(uiAutomation, downTime, touchSlop, xOnScreen, yOnScreen);
+        SystemClock.sleep((long) (ViewConfiguration.getLongPressTimeout() * 1.5f) + extraWaitMs);
+        if (upGesture) {
+            injectUpEvent(uiAutomation, downTime, false, xOnScreen, yOnScreen, null);
+        }
+
+        // Wait for the system to process all events in the queue
+        instrumentation.waitForIdleSync();
+    }
+}
diff --git a/common/device-side/util-axt/src/com/android/compatibility/common/util/DeviceInfoStore.java b/common/device-side/util-axt/src/com/android/compatibility/common/util/DeviceInfoStore.java
new file mode 100644
index 0000000..966ac1a
--- /dev/null
+++ b/common/device-side/util-axt/src/com/android/compatibility/common/util/DeviceInfoStore.java
@@ -0,0 +1,260 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.compatibility.common.util;
+
+import android.util.JsonWriter;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.OutputStreamWriter;
+import java.nio.charset.StandardCharsets;
+import java.util.Arrays;
+import java.util.List;
+
+public class DeviceInfoStore extends InfoStore {
+
+    protected File mJsonFile;
+    protected JsonWriter mJsonWriter = null;
+
+    public DeviceInfoStore() {
+        mJsonFile = null;
+    }
+
+    public DeviceInfoStore(File file) throws Exception {
+        mJsonFile = file;
+    }
+
+    /**
+     * Opens the file for storage and creates the writer.
+     */
+    @Override
+    public void open() throws IOException {
+        FileOutputStream out = new FileOutputStream(mJsonFile);
+        mJsonWriter = new JsonWriter(new OutputStreamWriter(out, StandardCharsets.UTF_8));
+        // TODO(agathaman): remove to make json output less pretty
+        mJsonWriter.setIndent("  ");
+        mJsonWriter.beginObject();
+    }
+
+    /**
+     * Closes the writer.
+     */
+    @Override
+    public void close() throws IOException {
+        mJsonWriter.endObject();
+        mJsonWriter.flush();
+        mJsonWriter.close();
+    }
+
+    /**
+     * Start a new group of result.
+     */
+    @Override
+    public void startGroup() throws IOException {
+        mJsonWriter.beginObject();
+    }
+
+    /**
+     * Start a new group of result with specified name.
+     */
+    @Override
+    public void startGroup(String name) throws IOException {
+        mJsonWriter.name(name);
+        mJsonWriter.beginObject();
+    }
+
+    /**
+     * Complete adding result to the last started group.
+     */
+    @Override
+    public void endGroup() throws IOException {
+        mJsonWriter.endObject();
+    }
+
+    /**
+     * Start a new array of result.
+     */
+    @Override
+    public void startArray() throws IOException {
+        mJsonWriter.beginArray();
+    }
+
+    /**
+     * Start a new array of result with specified name.
+     */
+    @Override
+    public void startArray(String name) throws IOException {
+        checkName(name);
+        mJsonWriter.name(name);
+        mJsonWriter.beginArray();
+    }
+
+    /**
+     * Complete adding result to the last started array.
+     */
+    @Override
+    public void endArray() throws IOException {
+        mJsonWriter.endArray();
+    }
+
+    /**
+     * Adds a int value to the InfoStore
+     */
+    @Override
+    public void addResult(String name, int value) throws IOException {
+        checkName(name);
+        mJsonWriter.name(name);
+        mJsonWriter.value(value);
+    }
+
+    /**
+     * Adds a long value to the InfoStore
+     */
+    @Override
+    public void addResult(String name, long value) throws IOException {
+        checkName(name);
+        mJsonWriter.name(name);
+        mJsonWriter.value(value);
+    }
+
+    /**
+     * Adds a float value to the InfoStore
+     */
+    @Override
+    public void addResult(String name, float value) throws IOException {
+        addResult(name, (double) value);
+    }
+
+    /**
+     * Adds a double value to the InfoStore
+     */
+    @Override
+    public void addResult(String name, double value) throws IOException {
+        checkName(name);
+        if (isDoubleNaNOrInfinite(value)) {
+            return;
+        } else {
+            mJsonWriter.name(name);
+            mJsonWriter.value(value);
+        }
+    }
+
+    /**
+     * Adds a boolean value to the InfoStore
+     */
+    @Override
+    public void addResult(String name, boolean value) throws IOException {
+        checkName(name);
+        mJsonWriter.name(name);
+        mJsonWriter.value(value);
+    }
+
+    /**
+     * Adds a String value to the InfoStore
+     */
+    @Override
+    public void addResult(String name, String value) throws IOException {
+        checkName(name);
+        mJsonWriter.name(name);
+        mJsonWriter.value(checkString(value));
+    }
+
+    /**
+     * Adds a int array to the InfoStore
+     */
+    @Override
+    public void addArrayResult(String name, int[] array) throws IOException {
+        checkName(name);
+        mJsonWriter.name(name);
+        mJsonWriter.beginArray();
+        for (int value : checkArray(array)) {
+            mJsonWriter.value(value);
+        }
+        mJsonWriter.endArray();
+    }
+
+    /**
+     * Adds a long array to the InfoStore
+     */
+    @Override
+    public void addArrayResult(String name, long[] array) throws IOException {
+        checkName(name);
+        mJsonWriter.name(name);
+        mJsonWriter.beginArray();
+        for (long value : checkArray(array)) {
+            mJsonWriter.value(value);
+        }
+        mJsonWriter.endArray();
+    }
+
+    /**
+     * Adds a float array to the InfoStore
+     */
+    @Override
+    public void addArrayResult(String name, float[] array) throws IOException {
+        double[] doubleArray = new double[array.length];
+        for (int i = 0; i < array.length; i++) {
+            doubleArray[i] = array[i];
+        }
+        addArrayResult(name, doubleArray);
+    }
+
+    /**
+     * Adds a double array to the InfoStore
+     */
+    @Override
+    public void addArrayResult(String name, double[] array) throws IOException {
+        checkName(name);
+        mJsonWriter.name(name);
+        mJsonWriter.beginArray();
+        for (double value : checkArray(array)) {
+            if (isDoubleNaNOrInfinite(value)) {
+                continue;
+            }
+            mJsonWriter.value(value);
+        }
+        mJsonWriter.endArray();
+    }
+
+    /**
+     * Adds a boolean array to the InfoStore
+     */
+    @Override
+    public void addArrayResult(String name, boolean[] array) throws IOException {
+        checkName(name);
+        mJsonWriter.name(name);
+        mJsonWriter.beginArray();
+        for (boolean value : checkArray(array)) {
+            mJsonWriter.value(value);
+        }
+        mJsonWriter.endArray();
+    }
+
+    /**
+     * Adds a List of String to the InfoStore
+     */
+    @Override
+    public void addListResult(String name, List<String> list) throws IOException {
+        checkName(name);
+        mJsonWriter.name(name);
+        mJsonWriter.beginArray();
+        for (String value : checkStringList(list)) {
+            mJsonWriter.value(checkString(value));
+        }
+        mJsonWriter.endArray();
+    }
+}
diff --git a/common/device-side/util-axt/src/com/android/compatibility/common/util/DeviceReportLog.java b/common/device-side/util-axt/src/com/android/compatibility/common/util/DeviceReportLog.java
new file mode 100644
index 0000000..d170263
--- /dev/null
+++ b/common/device-side/util-axt/src/com/android/compatibility/common/util/DeviceReportLog.java
@@ -0,0 +1,283 @@
+/*
+ * Copyright (C) 2014 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.compatibility.common.util;
+
+import android.app.Instrumentation;
+import android.os.Bundle;
+import android.os.Environment;
+import android.util.Log;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.List;
+
+/**
+ * Handles adding results to the report for device side tests.
+ *
+ * NOTE: tests MUST call {@link #submit(Instrumentation)} if and only if the test passes in order to
+ * send the results to the runner.
+ */
+public class DeviceReportLog extends ReportLog {
+    private static final String TAG = DeviceReportLog.class.getSimpleName();
+    private static final String RESULT = "COMPATIBILITY_TEST_RESULT";
+    private static final int INST_STATUS_ERROR = -1;
+    private static final int INST_STATUS_IN_PROGRESS = 2;
+
+    private ReportLogDeviceInfoStore store;
+
+    public DeviceReportLog(String reportLogName, String streamName) {
+        this(reportLogName, streamName,
+                new File(Environment.getExternalStorageDirectory(), "report-log-files"));
+    }
+
+    public DeviceReportLog(String reportLogName, String streamName, File logDirectory) {
+        super(reportLogName, streamName);
+        try {
+            // dir value must match the src-dir value configured in ReportLogCollector target
+            // preparer in cts/harness/tools/cts-tradefed/res/config/cts-preconditions.xml
+            if (!Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
+                throw new IOException("External storage is not mounted");
+            } else if ((!logDirectory.exists() && !logDirectory.mkdirs())
+                    || (logDirectory.exists() && !logDirectory.isDirectory())) {
+                throw new IOException("Cannot create directory for device info files");
+            } else {
+                File jsonFile = new File(logDirectory, mReportLogName + ".reportlog.json");
+                store = new ReportLogDeviceInfoStore(jsonFile, mStreamName);
+                store.open();
+            }
+        } catch (Exception e) {
+            Log.e(TAG, "Could not create report log file.", e);
+        }
+    }
+
+    /**
+     * Adds a double metric to the report.
+     */
+    @Override
+    public void addValue(String source, String message, double value, ResultType type,
+            ResultUnit unit) {
+        super.addValue(source, message, value, type, unit);
+        try {
+            store.addResult(message, value);
+        } catch (Exception e) {
+            Log.e(TAG, "Could not log metric.", e);
+        }
+    }
+
+    /**
+     * Adds a double metric to the report.
+     */
+    @Override
+    public void addValue(String message, double value, ResultType type, ResultUnit unit) {
+        super.addValue(message, value, type, unit);
+        try {
+            store.addResult(message, value);
+        } catch (Exception e) {
+            Log.e(TAG, "Could not log metric.", e);
+        }
+    }
+
+    /**
+     * Adds a double array of metrics to the report.
+     */
+    @Override
+    public void addValues(String source, String message, double[] values, ResultType type,
+            ResultUnit unit) {
+        super.addValues(source, message, values, type, unit);
+        try {
+            store.addArrayResult(message, values);
+        } catch (Exception e) {
+            Log.e(TAG, "Could not log metric.", e);
+        }
+    }
+
+    /**
+     * Adds a double array of metrics to the report.
+     */
+    @Override
+    public void addValues(String message, double[] values, ResultType type, ResultUnit unit) {
+        super.addValues(message, values, type, unit);
+        try {
+            store.addArrayResult(message, values);
+        } catch (Exception e) {
+            Log.e(TAG, "Could not log metric.", e);
+        }
+    }
+
+    /**
+     * Adds an int metric to the report.
+     */
+    @Override
+    public void addValue(String message, int value, ResultType type, ResultUnit unit) {
+        try {
+            store.addResult(message, value);
+        } catch (Exception e) {
+            Log.e(TAG, "Could not log metric.", e);
+        }
+    }
+
+    /**
+     * Adds a long metric to the report.
+     */
+    @Override
+    public void addValue(String message, long value, ResultType type, ResultUnit unit) {
+        try {
+            store.addResult(message, value);
+        } catch (Exception e) {
+            Log.e(TAG, "Could not log metric.", e);
+        }
+    }
+
+    /**
+     * Adds a float metric to the report.
+     */
+    @Override
+    public void addValue(String message, float value, ResultType type, ResultUnit unit) {
+        try {
+            store.addResult(message, value);
+        } catch (Exception e) {
+            Log.e(TAG, "Could not log metric.", e);
+        }
+    }
+
+    /**
+     * Adds a boolean metric to the report.
+     */
+    @Override
+    public void addValue(String message, boolean value, ResultType type, ResultUnit unit) {
+        try {
+            store.addResult(message, value);
+        } catch (Exception e) {
+            Log.e(TAG, "Could not log metric.", e);
+        }
+    }
+
+    /**
+     * Adds a String metric to the report.
+     */
+    @Override
+    public void addValue(String message, String value, ResultType type, ResultUnit unit) {
+        try {
+            store.addResult(message, value);
+        } catch (Exception e) {
+            Log.e(TAG, "Could not log metric.", e);
+        }
+    }
+
+    /**
+     * Adds an int array of metrics to the report.
+     */
+    @Override
+    public void addValues(String message, int[] values, ResultType type, ResultUnit unit) {
+        try {
+            store.addArrayResult(message, values);
+        } catch (Exception e) {
+            Log.e(TAG, "Could not log metric.", e);
+        }
+    }
+
+    /**
+     * Adds a long array of metrics to the report.
+     */
+    @Override
+    public void addValues(String message, long[] values, ResultType type, ResultUnit unit) {
+        try {
+            store.addArrayResult(message, values);
+        } catch (Exception e) {
+            Log.e(TAG, "Could not log metric.", e);
+        }
+    }
+
+    /**
+     * Adds a float array of metrics to the report.
+     */
+    @Override
+    public void addValues(String message, float[] values, ResultType type, ResultUnit unit) {
+        try {
+            store.addArrayResult(message, values);
+        } catch (Exception e) {
+            Log.e(TAG, "Could not log metric.", e);
+        }
+    }
+
+    /**
+     * Adds a boolean array of metrics to the report.
+     */
+    @Override
+    public void addValues(String message, boolean[] values, ResultType type, ResultUnit unit) {
+        try {
+            store.addArrayResult(message, values);
+        } catch (Exception e) {
+            Log.e(TAG, "Could not log metric.", e);
+        }
+    }
+
+    /**
+     * Adds a String List of metrics to the report.
+     */
+    @Override
+    public void addValues(String message, List<String> values, ResultType type, ResultUnit unit) {
+        try {
+            store.addListResult(message, values);
+        } catch (Exception e) {
+            Log.e(TAG, "Could not log metric.", e);
+        }
+    }
+
+    /**
+     * Sets the summary double metric of the report.
+     *
+     * NOTE: messages over {@value Metric#MAX_MESSAGE_LENGTH} chars will be trimmed.
+     */
+    @Override
+    public void setSummary(String message, double value, ResultType type, ResultUnit unit) {
+        super.setSummary(message, value, type, unit);
+        try {
+            store.addResult(message, value);
+        } catch (Exception e) {
+            Log.e(TAG, "Could not log metric.", e);
+        }
+    }
+
+    /**
+     * Closes report file and submits report to instrumentation.
+     */
+    public void submit(Instrumentation instrumentation) {
+        try {
+            store.close();
+            Bundle output = new Bundle();
+            output.putString(RESULT, serialize(this));
+            instrumentation.sendStatus(INST_STATUS_IN_PROGRESS, output);
+        } catch (Exception e) {
+            Log.e(TAG, "ReportLog Submit Failed", e);
+            instrumentation.sendStatus(INST_STATUS_ERROR, null);
+        }
+    }
+
+    /**
+     * Closes report file. Static functions that do not have access to instrumentation can
+     * use this to close report logs. Summary, if present, is not reported to instrumentation, hence
+     * does not appear in the result XML.
+     */
+    public void submit() {
+        try {
+            store.close();
+        } catch (Exception e) {
+            Log.e(TAG, "ReportLog Submit Failed", e);
+        }
+    }
+}
diff --git a/common/device-side/util-axt/src/com/android/compatibility/common/util/DummyActivity.java b/common/device-side/util-axt/src/com/android/compatibility/common/util/DummyActivity.java
new file mode 100644
index 0000000..672106c
--- /dev/null
+++ b/common/device-side/util-axt/src/com/android/compatibility/common/util/DummyActivity.java
@@ -0,0 +1,24 @@
+/*
+ * Copyright (C) 2013 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.compatibility.common.util;
+
+import android.app.Activity;
+
+public class DummyActivity extends Activity {
+
+}
diff --git a/common/device-side/util-axt/src/com/android/compatibility/common/util/DynamicConfigDeviceSide.java b/common/device-side/util-axt/src/com/android/compatibility/common/util/DynamicConfigDeviceSide.java
new file mode 100644
index 0000000..e7ee499
--- /dev/null
+++ b/common/device-side/util-axt/src/com/android/compatibility/common/util/DynamicConfigDeviceSide.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2015 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.compatibility.common.util;
+
+import android.os.Environment;
+
+import org.xmlpull.v1.XmlPullParserException;
+
+import java.io.File;
+import java.io.IOException;
+
+/**
+ * Load dynamic config for device side test cases
+ */
+public class DynamicConfigDeviceSide extends DynamicConfig {
+    public DynamicConfigDeviceSide(String moduleName) throws XmlPullParserException, IOException {
+        this(moduleName, new File(CONFIG_FOLDER_ON_DEVICE));
+    }
+
+    public DynamicConfigDeviceSide(String moduleName, File configFolder)
+            throws XmlPullParserException, IOException {
+        if (!Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
+            throw new IOException("External storage is not mounted");
+        }
+        File configFile = getConfigFile(configFolder, moduleName);
+        initializeConfig(configFile);
+    }
+}
diff --git a/common/device-side/util-axt/src/com/android/compatibility/common/util/EvaluateJsResultPollingCheck.java b/common/device-side/util-axt/src/com/android/compatibility/common/util/EvaluateJsResultPollingCheck.java
new file mode 100644
index 0000000..5567ec6
--- /dev/null
+++ b/common/device-side/util-axt/src/com/android/compatibility/common/util/EvaluateJsResultPollingCheck.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2013 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.compatibility.common.util;
+
+import android.webkit.ValueCallback;
+
+import junit.framework.Assert;
+
+public class EvaluateJsResultPollingCheck extends PollingCheck
+        implements ValueCallback<String> {
+    private String mActualResult;
+    private String mExpectedResult;
+    private boolean mGotResult;
+
+    public EvaluateJsResultPollingCheck(String expected) {
+        mExpectedResult = expected;
+    }
+
+    @Override
+    public synchronized boolean check() {
+        return mGotResult;
+    }
+
+    @Override
+    public void run() {
+        super.run();
+        synchronized (this) {
+            Assert.assertEquals(mExpectedResult, mActualResult);
+        }
+    }
+
+    @Override
+    public synchronized void onReceiveValue(String result) {
+        mGotResult = true;
+        mActualResult = result;
+    }
+}
diff --git a/common/device-side/util-axt/src/com/android/compatibility/common/util/FakeKeys.java b/common/device-side/util-axt/src/com/android/compatibility/common/util/FakeKeys.java
new file mode 100644
index 0000000..85e06ea
--- /dev/null
+++ b/common/device-side/util-axt/src/com/android/compatibility/common/util/FakeKeys.java
@@ -0,0 +1,469 @@
+/*
+ * Copyright (C) 2014 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.compatibility.common.util;
+
+// Copied from cts/tests/tests/keystore/src/android/keystore/cts/AndroidKeyStoreTest.java
+
+public class FakeKeys {
+    /*
+     * The keys and certificates below are generated with:
+     *
+     * openssl req -new -x509 -days 3650 -extensions v3_ca -keyout cakey.pem -out cacert.pem
+     * openssl req -newkey rsa:1024 -keyout userkey.pem -nodes -days 3650 -out userkey.req
+     * mkdir -p demoCA/newcerts
+     * touch demoCA/index.txt
+     * echo "01" > demoCA/serial
+     * openssl ca -out usercert.pem -in userkey.req -cert cacert.pem -keyfile cakey.pem -days 3650
+     */
+    public static class FAKE_RSA_1 {
+        /**
+         * Generated from above and converted with:
+         *
+         * openssl pkcs8 -topk8 -outform d -in userkey.pem -nocrypt | xxd -i | sed 's/0x/(byte) 0x/g'
+         */
+        public static final byte[] privateKey = {
+            (byte) 0x30, (byte) 0x82, (byte) 0x02, (byte) 0x78, (byte) 0x02, (byte) 0x01,
+            (byte) 0x00, (byte) 0x30, (byte) 0x0d, (byte) 0x06, (byte) 0x09, (byte) 0x2a,
+            (byte) 0x86, (byte) 0x48, (byte) 0x86, (byte) 0xf7, (byte) 0x0d, (byte) 0x01,
+            (byte) 0x01, (byte) 0x01, (byte) 0x05, (byte) 0x00, (byte) 0x04, (byte) 0x82,
+            (byte) 0x02, (byte) 0x62, (byte) 0x30, (byte) 0x82, (byte) 0x02, (byte) 0x5e,
+            (byte) 0x02, (byte) 0x01, (byte) 0x00, (byte) 0x02, (byte) 0x81, (byte) 0x81,
+            (byte) 0x00, (byte) 0xce, (byte) 0x29, (byte) 0xeb, (byte) 0xf6, (byte) 0x5b,
+            (byte) 0x25, (byte) 0xdc, (byte) 0xa1, (byte) 0xa6, (byte) 0x2c, (byte) 0x66,
+            (byte) 0xcb, (byte) 0x20, (byte) 0x90, (byte) 0x27, (byte) 0x86, (byte) 0x8a,
+            (byte) 0x44, (byte) 0x71, (byte) 0x50, (byte) 0xda, (byte) 0xd3, (byte) 0x02,
+            (byte) 0x77, (byte) 0x55, (byte) 0xe9, (byte) 0xe8, (byte) 0x08, (byte) 0xf3,
+            (byte) 0x36, (byte) 0x9a, (byte) 0xae, (byte) 0xab, (byte) 0x04, (byte) 0x6d,
+            (byte) 0x00, (byte) 0x99, (byte) 0xbf, (byte) 0x7d, (byte) 0x0f, (byte) 0x67,
+            (byte) 0x8b, (byte) 0x1d, (byte) 0xd4, (byte) 0x2b, (byte) 0x7c, (byte) 0xcb,
+            (byte) 0xcd, (byte) 0x33, (byte) 0xc7, (byte) 0x84, (byte) 0x30, (byte) 0xe2,
+            (byte) 0x45, (byte) 0x21, (byte) 0xb3, (byte) 0x75, (byte) 0xf5, (byte) 0x79,
+            (byte) 0x02, (byte) 0xda, (byte) 0x50, (byte) 0xa3, (byte) 0x8b, (byte) 0xce,
+            (byte) 0xc3, (byte) 0x8e, (byte) 0x0f, (byte) 0x25, (byte) 0xeb, (byte) 0x08,
+            (byte) 0x2c, (byte) 0xdd, (byte) 0x1c, (byte) 0xcf, (byte) 0xff, (byte) 0x3b,
+            (byte) 0xde, (byte) 0xb6, (byte) 0xaa, (byte) 0x2a, (byte) 0xa9, (byte) 0xc4,
+            (byte) 0x8a, (byte) 0x24, (byte) 0x24, (byte) 0xe6, (byte) 0x29, (byte) 0x0d,
+            (byte) 0x98, (byte) 0x4c, (byte) 0x32, (byte) 0xa1, (byte) 0x7b, (byte) 0x23,
+            (byte) 0x2b, (byte) 0x42, (byte) 0x30, (byte) 0xee, (byte) 0x78, (byte) 0x08,
+            (byte) 0x47, (byte) 0xad, (byte) 0xf2, (byte) 0x96, (byte) 0xd5, (byte) 0xf1,
+            (byte) 0x62, (byte) 0x42, (byte) 0x2d, (byte) 0x35, (byte) 0x19, (byte) 0xb4,
+            (byte) 0x3c, (byte) 0xc9, (byte) 0xc3, (byte) 0x5f, (byte) 0x03, (byte) 0x16,
+            (byte) 0x3a, (byte) 0x23, (byte) 0xac, (byte) 0xcb, (byte) 0xce, (byte) 0x9e,
+            (byte) 0x51, (byte) 0x2e, (byte) 0x6d, (byte) 0x02, (byte) 0x03, (byte) 0x01,
+            (byte) 0x00, (byte) 0x01, (byte) 0x02, (byte) 0x81, (byte) 0x80, (byte) 0x16,
+            (byte) 0x59, (byte) 0xc3, (byte) 0x24, (byte) 0x1d, (byte) 0x33, (byte) 0x98,
+            (byte) 0x9c, (byte) 0xc9, (byte) 0xc8, (byte) 0x2c, (byte) 0x88, (byte) 0xbf,
+            (byte) 0x0a, (byte) 0x01, (byte) 0xce, (byte) 0xfb, (byte) 0x34, (byte) 0x7a,
+            (byte) 0x58, (byte) 0x7a, (byte) 0xb0, (byte) 0xbf, (byte) 0xa6, (byte) 0xb2,
+            (byte) 0x60, (byte) 0xbe, (byte) 0x70, (byte) 0x21, (byte) 0xf5, (byte) 0xfc,
+            (byte) 0x85, (byte) 0x0d, (byte) 0x33, (byte) 0x58, (byte) 0xa1, (byte) 0xe5,
+            (byte) 0x09, (byte) 0x36, (byte) 0x84, (byte) 0xb2, (byte) 0x04, (byte) 0x0a,
+            (byte) 0x02, (byte) 0xd3, (byte) 0x88, (byte) 0x1f, (byte) 0x0c, (byte) 0x2b,
+            (byte) 0x1d, (byte) 0xe9, (byte) 0x3d, (byte) 0xe7, (byte) 0x79, (byte) 0xf9,
+            (byte) 0x32, (byte) 0x5c, (byte) 0x8a, (byte) 0x75, (byte) 0x49, (byte) 0x12,
+            (byte) 0xe4, (byte) 0x05, (byte) 0x26, (byte) 0xd4, (byte) 0x2e, (byte) 0x9e,
+            (byte) 0x1f, (byte) 0xcc, (byte) 0x54, (byte) 0xad, (byte) 0x33, (byte) 0x8d,
+            (byte) 0x99, (byte) 0x00, (byte) 0xdc, (byte) 0xf5, (byte) 0xb4, (byte) 0xa2,
+            (byte) 0x2f, (byte) 0xba, (byte) 0xe5, (byte) 0x62, (byte) 0x30, (byte) 0x6d,
+            (byte) 0xe6, (byte) 0x3d, (byte) 0xeb, (byte) 0x24, (byte) 0xc2, (byte) 0xdc,
+            (byte) 0x5f, (byte) 0xb7, (byte) 0x16, (byte) 0x35, (byte) 0xa3, (byte) 0x98,
+            (byte) 0x98, (byte) 0xa8, (byte) 0xef, (byte) 0xe8, (byte) 0xc4, (byte) 0x96,
+            (byte) 0x6d, (byte) 0x38, (byte) 0xab, (byte) 0x26, (byte) 0x6d, (byte) 0x30,
+            (byte) 0xc2, (byte) 0xa0, (byte) 0x44, (byte) 0xe4, (byte) 0xff, (byte) 0x7e,
+            (byte) 0xbe, (byte) 0x7c, (byte) 0x33, (byte) 0xa5, (byte) 0x10, (byte) 0xad,
+            (byte) 0xd7, (byte) 0x1e, (byte) 0x13, (byte) 0x20, (byte) 0xb3, (byte) 0x1f,
+            (byte) 0x41, (byte) 0x02, (byte) 0x41, (byte) 0x00, (byte) 0xf1, (byte) 0x89,
+            (byte) 0x07, (byte) 0x0f, (byte) 0xe8, (byte) 0xcf, (byte) 0xab, (byte) 0x13,
+            (byte) 0x2a, (byte) 0x8f, (byte) 0x88, (byte) 0x80, (byte) 0x11, (byte) 0x9a,
+            (byte) 0x79, (byte) 0xb6, (byte) 0x59, (byte) 0x3a, (byte) 0x50, (byte) 0x6e,
+            (byte) 0x57, (byte) 0x37, (byte) 0xab, (byte) 0x2a, (byte) 0xd2, (byte) 0xaa,
+            (byte) 0xd9, (byte) 0x72, (byte) 0x73, (byte) 0xff, (byte) 0x8b, (byte) 0x47,
+            (byte) 0x76, (byte) 0xdd, (byte) 0xdc, (byte) 0xf5, (byte) 0x97, (byte) 0x44,
+            (byte) 0x3a, (byte) 0x78, (byte) 0xbe, (byte) 0x17, (byte) 0xb4, (byte) 0x22,
+            (byte) 0x6f, (byte) 0xe5, (byte) 0x23, (byte) 0x70, (byte) 0x1d, (byte) 0x10,
+            (byte) 0x5d, (byte) 0xba, (byte) 0x16, (byte) 0x81, (byte) 0xf1, (byte) 0x45,
+            (byte) 0xce, (byte) 0x30, (byte) 0xb4, (byte) 0xab, (byte) 0x80, (byte) 0xe4,
+            (byte) 0x98, (byte) 0x31, (byte) 0x02, (byte) 0x41, (byte) 0x00, (byte) 0xda,
+            (byte) 0x82, (byte) 0x9d, (byte) 0x3f, (byte) 0xca, (byte) 0x2f, (byte) 0xe1,
+            (byte) 0xd4, (byte) 0x86, (byte) 0x77, (byte) 0x48, (byte) 0xa6, (byte) 0xab,
+            (byte) 0xab, (byte) 0x1c, (byte) 0x42, (byte) 0x5c, (byte) 0xd5, (byte) 0xc7,
+            (byte) 0x46, (byte) 0x59, (byte) 0x91, (byte) 0x3f, (byte) 0xfc, (byte) 0xcc,
+            (byte) 0xec, (byte) 0xc2, (byte) 0x40, (byte) 0x12, (byte) 0x2c, (byte) 0x8d,
+            (byte) 0x1f, (byte) 0xa2, (byte) 0x18, (byte) 0x88, (byte) 0xee, (byte) 0x82,
+            (byte) 0x4a, (byte) 0x5a, (byte) 0x5e, (byte) 0x88, (byte) 0x20, (byte) 0xe3,
+            (byte) 0x7b, (byte) 0xe0, (byte) 0xd8, (byte) 0x3a, (byte) 0x52, (byte) 0x9a,
+            (byte) 0x26, (byte) 0x6a, (byte) 0x04, (byte) 0xec, (byte) 0xe8, (byte) 0xb9,
+            (byte) 0x48, (byte) 0x40, (byte) 0xe1, (byte) 0xe1, (byte) 0x83, (byte) 0xa6,
+            (byte) 0x67, (byte) 0xa6, (byte) 0xfd, (byte) 0x02, (byte) 0x41, (byte) 0x00,
+            (byte) 0x89, (byte) 0x72, (byte) 0x3e, (byte) 0xb0, (byte) 0x90, (byte) 0xfd,
+            (byte) 0x4c, (byte) 0x0e, (byte) 0xd6, (byte) 0x13, (byte) 0x63, (byte) 0xcb,
+            (byte) 0xed, (byte) 0x38, (byte) 0x88, (byte) 0xb6, (byte) 0x79, (byte) 0xc4,
+            (byte) 0x33, (byte) 0x6c, (byte) 0xf6, (byte) 0xf8, (byte) 0xd8, (byte) 0xd0,
+            (byte) 0xbf, (byte) 0x9d, (byte) 0x35, (byte) 0xac, (byte) 0x69, (byte) 0xd2,
+            (byte) 0x2b, (byte) 0xc1, (byte) 0xf9, (byte) 0x24, (byte) 0x7b, (byte) 0xce,
+            (byte) 0xcd, (byte) 0xcb, (byte) 0xa7, (byte) 0xb2, (byte) 0x7a, (byte) 0x0a,
+            (byte) 0x27, (byte) 0x19, (byte) 0xc9, (byte) 0xaf, (byte) 0x0d, (byte) 0x21,
+            (byte) 0x89, (byte) 0x88, (byte) 0x7c, (byte) 0xad, (byte) 0x9e, (byte) 0x8d,
+            (byte) 0x47, (byte) 0x6d, (byte) 0x3f, (byte) 0xce, (byte) 0x7b, (byte) 0xa1,
+            (byte) 0x74, (byte) 0xf1, (byte) 0xa0, (byte) 0xa1, (byte) 0x02, (byte) 0x41,
+            (byte) 0x00, (byte) 0xd9, (byte) 0xa8, (byte) 0xf5, (byte) 0xfe, (byte) 0xce,
+            (byte) 0xe6, (byte) 0x77, (byte) 0x6b, (byte) 0xfe, (byte) 0x2d, (byte) 0xe0,
+            (byte) 0x1e, (byte) 0xb6, (byte) 0x2e, (byte) 0x12, (byte) 0x4e, (byte) 0x40,
+            (byte) 0xaf, (byte) 0x6a, (byte) 0x7b, (byte) 0x37, (byte) 0x49, (byte) 0x2a,
+            (byte) 0x96, (byte) 0x25, (byte) 0x83, (byte) 0x49, (byte) 0xd4, (byte) 0x0c,
+            (byte) 0xc6, (byte) 0x78, (byte) 0x25, (byte) 0x24, (byte) 0x90, (byte) 0x90,
+            (byte) 0x06, (byte) 0x15, (byte) 0x9e, (byte) 0xfe, (byte) 0xf9, (byte) 0xdf,
+            (byte) 0x5b, (byte) 0xf3, (byte) 0x7e, (byte) 0x38, (byte) 0x70, (byte) 0xeb,
+            (byte) 0x57, (byte) 0xd0, (byte) 0xd9, (byte) 0xa7, (byte) 0x0e, (byte) 0x14,
+            (byte) 0xf7, (byte) 0x95, (byte) 0x68, (byte) 0xd5, (byte) 0xc8, (byte) 0xab,
+            (byte) 0x9d, (byte) 0x3a, (byte) 0x2b, (byte) 0x51, (byte) 0xf9, (byte) 0x02,
+            (byte) 0x41, (byte) 0x00, (byte) 0x96, (byte) 0xdf, (byte) 0xe9, (byte) 0x67,
+            (byte) 0x6c, (byte) 0xdc, (byte) 0x90, (byte) 0x14, (byte) 0xb4, (byte) 0x1d,
+            (byte) 0x22, (byte) 0x33, (byte) 0x4a, (byte) 0x31, (byte) 0xc1, (byte) 0x9d,
+            (byte) 0x2e, (byte) 0xff, (byte) 0x9a, (byte) 0x2a, (byte) 0x95, (byte) 0x4b,
+            (byte) 0x27, (byte) 0x74, (byte) 0xcb, (byte) 0x21, (byte) 0xc3, (byte) 0xd2,
+            (byte) 0x0b, (byte) 0xb2, (byte) 0x46, (byte) 0x87, (byte) 0xf8, (byte) 0x28,
+            (byte) 0x01, (byte) 0x8b, (byte) 0xd8, (byte) 0xb9, (byte) 0x4b, (byte) 0xcd,
+            (byte) 0x9a, (byte) 0x96, (byte) 0x41, (byte) 0x0e, (byte) 0x36, (byte) 0x6d,
+            (byte) 0x40, (byte) 0x42, (byte) 0xbc, (byte) 0xd9, (byte) 0xd3, (byte) 0x7b,
+            (byte) 0xbc, (byte) 0xa7, (byte) 0x92, (byte) 0x90, (byte) 0xdd, (byte) 0xa1,
+            (byte) 0x9c, (byte) 0xce, (byte) 0xa1, (byte) 0x87, (byte) 0x11, (byte) 0x51
+        };
+
+        /**
+         * Generated from above and converted with:
+         *
+         * openssl x509 -outform d -in cacert.pem | xxd -i | sed 's/0x/(byte) 0x/g'
+         */
+        public static final byte[] caCertificate = {
+            (byte) 0x30, (byte) 0x82, (byte) 0x02, (byte) 0xce, (byte) 0x30, (byte) 0x82,
+            (byte) 0x02, (byte) 0x37, (byte) 0xa0, (byte) 0x03, (byte) 0x02, (byte) 0x01,
+            (byte) 0x02, (byte) 0x02, (byte) 0x09, (byte) 0x00, (byte) 0xe1, (byte) 0x6a,
+            (byte) 0xa2, (byte) 0xf4, (byte) 0x2e, (byte) 0x55, (byte) 0x48, (byte) 0x0a,
+            (byte) 0x30, (byte) 0x0d, (byte) 0x06, (byte) 0x09, (byte) 0x2a, (byte) 0x86,
+            (byte) 0x48, (byte) 0x86, (byte) 0xf7, (byte) 0x0d, (byte) 0x01, (byte) 0x01,
+            (byte) 0x05, (byte) 0x05, (byte) 0x00, (byte) 0x30, (byte) 0x4f, (byte) 0x31,
+            (byte) 0x0b, (byte) 0x30, (byte) 0x09, (byte) 0x06, (byte) 0x03, (byte) 0x55,
+            (byte) 0x04, (byte) 0x06, (byte) 0x13, (byte) 0x02, (byte) 0x55, (byte) 0x53,
+            (byte) 0x31, (byte) 0x0b, (byte) 0x30, (byte) 0x09, (byte) 0x06, (byte) 0x03,
+            (byte) 0x55, (byte) 0x04, (byte) 0x08, (byte) 0x13, (byte) 0x02, (byte) 0x43,
+            (byte) 0x41, (byte) 0x31, (byte) 0x16, (byte) 0x30, (byte) 0x14, (byte) 0x06,
+            (byte) 0x03, (byte) 0x55, (byte) 0x04, (byte) 0x07, (byte) 0x13, (byte) 0x0d,
+            (byte) 0x4d, (byte) 0x6f, (byte) 0x75, (byte) 0x6e, (byte) 0x74, (byte) 0x61,
+            (byte) 0x69, (byte) 0x6e, (byte) 0x20, (byte) 0x56, (byte) 0x69, (byte) 0x65,
+            (byte) 0x77, (byte) 0x31, (byte) 0x1b, (byte) 0x30, (byte) 0x19, (byte) 0x06,
+            (byte) 0x03, (byte) 0x55, (byte) 0x04, (byte) 0x0a, (byte) 0x13, (byte) 0x12,
+            (byte) 0x41, (byte) 0x6e, (byte) 0x64, (byte) 0x72, (byte) 0x6f, (byte) 0x69,
+            (byte) 0x64, (byte) 0x20, (byte) 0x54, (byte) 0x65, (byte) 0x73, (byte) 0x74,
+            (byte) 0x20, (byte) 0x43, (byte) 0x61, (byte) 0x73, (byte) 0x65, (byte) 0x73,
+            (byte) 0x30, (byte) 0x1e, (byte) 0x17, (byte) 0x0d, (byte) 0x31, (byte) 0x32,
+            (byte) 0x30, (byte) 0x38, (byte) 0x31, (byte) 0x34, (byte) 0x31, (byte) 0x36,
+            (byte) 0x35, (byte) 0x35, (byte) 0x34, (byte) 0x34, (byte) 0x5a, (byte) 0x17,
+            (byte) 0x0d, (byte) 0x32, (byte) 0x32, (byte) 0x30, (byte) 0x38, (byte) 0x31,
+            (byte) 0x32, (byte) 0x31, (byte) 0x36, (byte) 0x35, (byte) 0x35, (byte) 0x34,
+            (byte) 0x34, (byte) 0x5a, (byte) 0x30, (byte) 0x4f, (byte) 0x31, (byte) 0x0b,
+            (byte) 0x30, (byte) 0x09, (byte) 0x06, (byte) 0x03, (byte) 0x55, (byte) 0x04,
+            (byte) 0x06, (byte) 0x13, (byte) 0x02, (byte) 0x55, (byte) 0x53, (byte) 0x31,
+            (byte) 0x0b, (byte) 0x30, (byte) 0x09, (byte) 0x06, (byte) 0x03, (byte) 0x55,
+            (byte) 0x04, (byte) 0x08, (byte) 0x13, (byte) 0x02, (byte) 0x43, (byte) 0x41,
+            (byte) 0x31, (byte) 0x16, (byte) 0x30, (byte) 0x14, (byte) 0x06, (byte) 0x03,
+            (byte) 0x55, (byte) 0x04, (byte) 0x07, (byte) 0x13, (byte) 0x0d, (byte) 0x4d,
+            (byte) 0x6f, (byte) 0x75, (byte) 0x6e, (byte) 0x74, (byte) 0x61, (byte) 0x69,
+            (byte) 0x6e, (byte) 0x20, (byte) 0x56, (byte) 0x69, (byte) 0x65, (byte) 0x77,
+            (byte) 0x31, (byte) 0x1b, (byte) 0x30, (byte) 0x19, (byte) 0x06, (byte) 0x03,
+            (byte) 0x55, (byte) 0x04, (byte) 0x0a, (byte) 0x13, (byte) 0x12, (byte) 0x41,
+            (byte) 0x6e, (byte) 0x64, (byte) 0x72, (byte) 0x6f, (byte) 0x69, (byte) 0x64,
+            (byte) 0x20, (byte) 0x54, (byte) 0x65, (byte) 0x73, (byte) 0x74, (byte) 0x20,
+            (byte) 0x43, (byte) 0x61, (byte) 0x73, (byte) 0x65, (byte) 0x73, (byte) 0x30,
+            (byte) 0x81, (byte) 0x9f, (byte) 0x30, (byte) 0x0d, (byte) 0x06, (byte) 0x09,
+            (byte) 0x2a, (byte) 0x86, (byte) 0x48, (byte) 0x86, (byte) 0xf7, (byte) 0x0d,
+            (byte) 0x01, (byte) 0x01, (byte) 0x01, (byte) 0x05, (byte) 0x00, (byte) 0x03,
+            (byte) 0x81, (byte) 0x8d, (byte) 0x00, (byte) 0x30, (byte) 0x81, (byte) 0x89,
+            (byte) 0x02, (byte) 0x81, (byte) 0x81, (byte) 0x00, (byte) 0xa3, (byte) 0x72,
+            (byte) 0xab, (byte) 0xd0, (byte) 0xe4, (byte) 0xad, (byte) 0x2f, (byte) 0xe7,
+            (byte) 0xe2, (byte) 0x79, (byte) 0x07, (byte) 0x36, (byte) 0x3d, (byte) 0x0c,
+            (byte) 0x8d, (byte) 0x42, (byte) 0x9a, (byte) 0x0a, (byte) 0x33, (byte) 0x64,
+            (byte) 0xb3, (byte) 0xcd, (byte) 0xb2, (byte) 0xd7, (byte) 0x3a, (byte) 0x42,
+            (byte) 0x06, (byte) 0x77, (byte) 0x45, (byte) 0x29, (byte) 0xe9, (byte) 0xcb,
+            (byte) 0xb7, (byte) 0x4a, (byte) 0xd6, (byte) 0xee, (byte) 0xad, (byte) 0x01,
+            (byte) 0x91, (byte) 0x9b, (byte) 0x0c, (byte) 0x59, (byte) 0xa1, (byte) 0x03,
+            (byte) 0xfa, (byte) 0xf0, (byte) 0x5a, (byte) 0x7c, (byte) 0x4f, (byte) 0xf7,
+            (byte) 0x8d, (byte) 0x36, (byte) 0x0f, (byte) 0x1f, (byte) 0x45, (byte) 0x7d,
+            (byte) 0x1b, (byte) 0x31, (byte) 0xa1, (byte) 0x35, (byte) 0x0b, (byte) 0x00,
+            (byte) 0xed, (byte) 0x7a, (byte) 0xb6, (byte) 0xc8, (byte) 0x4e, (byte) 0xa9,
+            (byte) 0x86, (byte) 0x4c, (byte) 0x7b, (byte) 0x99, (byte) 0x57, (byte) 0x41,
+            (byte) 0x12, (byte) 0xef, (byte) 0x6b, (byte) 0xbc, (byte) 0x3d, (byte) 0x60,
+            (byte) 0xf2, (byte) 0x99, (byte) 0x1a, (byte) 0xcd, (byte) 0xed, (byte) 0x56,
+            (byte) 0xa4, (byte) 0xe5, (byte) 0x36, (byte) 0x9f, (byte) 0x24, (byte) 0x1f,
+            (byte) 0xdc, (byte) 0x89, (byte) 0x40, (byte) 0xc8, (byte) 0x99, (byte) 0x92,
+            (byte) 0xab, (byte) 0x4a, (byte) 0xb5, (byte) 0x61, (byte) 0x45, (byte) 0x62,
+            (byte) 0xff, (byte) 0xa3, (byte) 0x45, (byte) 0x65, (byte) 0xaf, (byte) 0xf6,
+            (byte) 0x27, (byte) 0x30, (byte) 0x51, (byte) 0x0e, (byte) 0x0e, (byte) 0xeb,
+            (byte) 0x79, (byte) 0x0c, (byte) 0xbe, (byte) 0xb3, (byte) 0x0a, (byte) 0x6f,
+            (byte) 0x29, (byte) 0x06, (byte) 0xdc, (byte) 0x2f, (byte) 0x6b, (byte) 0x51,
+            (byte) 0x02, (byte) 0x03, (byte) 0x01, (byte) 0x00, (byte) 0x01, (byte) 0xa3,
+            (byte) 0x81, (byte) 0xb1, (byte) 0x30, (byte) 0x81, (byte) 0xae, (byte) 0x30,
+            (byte) 0x1d, (byte) 0x06, (byte) 0x03, (byte) 0x55, (byte) 0x1d, (byte) 0x0e,
+            (byte) 0x04, (byte) 0x16, (byte) 0x04, (byte) 0x14, (byte) 0x33, (byte) 0x05,
+            (byte) 0xee, (byte) 0xfe, (byte) 0x6f, (byte) 0x60, (byte) 0xc7, (byte) 0xf9,
+            (byte) 0xa9, (byte) 0xd2, (byte) 0x73, (byte) 0x5c, (byte) 0x8f, (byte) 0x6d,
+            (byte) 0xa2, (byte) 0x2f, (byte) 0x97, (byte) 0x8e, (byte) 0x5d, (byte) 0x51,
+            (byte) 0x30, (byte) 0x7f, (byte) 0x06, (byte) 0x03, (byte) 0x55, (byte) 0x1d,
+            (byte) 0x23, (byte) 0x04, (byte) 0x78, (byte) 0x30, (byte) 0x76, (byte) 0x80,
+            (byte) 0x14, (byte) 0x33, (byte) 0x05, (byte) 0xee, (byte) 0xfe, (byte) 0x6f,
+            (byte) 0x60, (byte) 0xc7, (byte) 0xf9, (byte) 0xa9, (byte) 0xd2, (byte) 0x73,
+            (byte) 0x5c, (byte) 0x8f, (byte) 0x6d, (byte) 0xa2, (byte) 0x2f, (byte) 0x97,
+            (byte) 0x8e, (byte) 0x5d, (byte) 0x51, (byte) 0xa1, (byte) 0x53, (byte) 0xa4,
+            (byte) 0x51, (byte) 0x30, (byte) 0x4f, (byte) 0x31, (byte) 0x0b, (byte) 0x30,
+            (byte) 0x09, (byte) 0x06, (byte) 0x03, (byte) 0x55, (byte) 0x04, (byte) 0x06,
+            (byte) 0x13, (byte) 0x02, (byte) 0x55, (byte) 0x53, (byte) 0x31, (byte) 0x0b,
+            (byte) 0x30, (byte) 0x09, (byte) 0x06, (byte) 0x03, (byte) 0x55, (byte) 0x04,
+            (byte) 0x08, (byte) 0x13, (byte) 0x02, (byte) 0x43, (byte) 0x41, (byte) 0x31,
+            (byte) 0x16, (byte) 0x30, (byte) 0x14, (byte) 0x06, (byte) 0x03, (byte) 0x55,
+            (byte) 0x04, (byte) 0x07, (byte) 0x13, (byte) 0x0d, (byte) 0x4d, (byte) 0x6f,
+            (byte) 0x75, (byte) 0x6e, (byte) 0x74, (byte) 0x61, (byte) 0x69, (byte) 0x6e,
+            (byte) 0x20, (byte) 0x56, (byte) 0x69, (byte) 0x65, (byte) 0x77, (byte) 0x31,
+            (byte) 0x1b, (byte) 0x30, (byte) 0x19, (byte) 0x06, (byte) 0x03, (byte) 0x55,
+            (byte) 0x04, (byte) 0x0a, (byte) 0x13, (byte) 0x12, (byte) 0x41, (byte) 0x6e,
+            (byte) 0x64, (byte) 0x72, (byte) 0x6f, (byte) 0x69, (byte) 0x64, (byte) 0x20,
+            (byte) 0x54, (byte) 0x65, (byte) 0x73, (byte) 0x74, (byte) 0x20, (byte) 0x43,
+            (byte) 0x61, (byte) 0x73, (byte) 0x65, (byte) 0x73, (byte) 0x82, (byte) 0x09,
+            (byte) 0x00, (byte) 0xe1, (byte) 0x6a, (byte) 0xa2, (byte) 0xf4, (byte) 0x2e,
+            (byte) 0x55, (byte) 0x48, (byte) 0x0a, (byte) 0x30, (byte) 0x0c, (byte) 0x06,
+            (byte) 0x03, (byte) 0x55, (byte) 0x1d, (byte) 0x13, (byte) 0x04, (byte) 0x05,
+            (byte) 0x30, (byte) 0x03, (byte) 0x01, (byte) 0x01, (byte) 0xff, (byte) 0x30,
+            (byte) 0x0d, (byte) 0x06, (byte) 0x09, (byte) 0x2a, (byte) 0x86, (byte) 0x48,
+            (byte) 0x86, (byte) 0xf7, (byte) 0x0d, (byte) 0x01, (byte) 0x01, (byte) 0x05,
+            (byte) 0x05, (byte) 0x00, (byte) 0x03, (byte) 0x81, (byte) 0x81, (byte) 0x00,
+            (byte) 0x8c, (byte) 0x30, (byte) 0x42, (byte) 0xfa, (byte) 0xeb, (byte) 0x1a,
+            (byte) 0x26, (byte) 0xeb, (byte) 0xda, (byte) 0x56, (byte) 0x32, (byte) 0xf2,
+            (byte) 0x9d, (byte) 0xa5, (byte) 0x24, (byte) 0xd8, (byte) 0x3a, (byte) 0xda,
+            (byte) 0x30, (byte) 0xa6, (byte) 0x8b, (byte) 0x46, (byte) 0xfe, (byte) 0xfe,
+            (byte) 0xdb, (byte) 0xf1, (byte) 0xe6, (byte) 0xe1, (byte) 0x7c, (byte) 0x1b,
+            (byte) 0xe7, (byte) 0x77, (byte) 0x00, (byte) 0xa1, (byte) 0x1c, (byte) 0x19,
+            (byte) 0x17, (byte) 0x73, (byte) 0xb0, (byte) 0xf0, (byte) 0x9d, (byte) 0xf3,
+            (byte) 0x4f, (byte) 0xb6, (byte) 0xbc, (byte) 0xc7, (byte) 0x47, (byte) 0x85,
+            (byte) 0x2a, (byte) 0x4a, (byte) 0xa1, (byte) 0xa5, (byte) 0x58, (byte) 0xf5,
+            (byte) 0xc5, (byte) 0x1a, (byte) 0x51, (byte) 0xb1, (byte) 0x04, (byte) 0x80,
+            (byte) 0xee, (byte) 0x3a, (byte) 0xec, (byte) 0x2f, (byte) 0xe1, (byte) 0xfd,
+            (byte) 0x58, (byte) 0xeb, (byte) 0xed, (byte) 0x82, (byte) 0x9e, (byte) 0x38,
+            (byte) 0xa3, (byte) 0x24, (byte) 0x75, (byte) 0xf7, (byte) 0x3e, (byte) 0xc2,
+            (byte) 0xc5, (byte) 0x27, (byte) 0xeb, (byte) 0x6f, (byte) 0x7b, (byte) 0x50,
+            (byte) 0xda, (byte) 0x43, (byte) 0xdc, (byte) 0x3b, (byte) 0x0b, (byte) 0x6f,
+            (byte) 0x78, (byte) 0x8f, (byte) 0xb0, (byte) 0x66, (byte) 0xe1, (byte) 0x12,
+            (byte) 0x87, (byte) 0x5f, (byte) 0x97, (byte) 0x7b, (byte) 0xca, (byte) 0x14,
+            (byte) 0x79, (byte) 0xf7, (byte) 0xe8, (byte) 0x6c, (byte) 0x72, (byte) 0xdb,
+            (byte) 0x91, (byte) 0x65, (byte) 0x17, (byte) 0x54, (byte) 0xe0, (byte) 0x74,
+            (byte) 0x1d, (byte) 0xac, (byte) 0x47, (byte) 0x04, (byte) 0x12, (byte) 0xe0,
+            (byte) 0xc3, (byte) 0x66, (byte) 0x19, (byte) 0x05, (byte) 0x2e, (byte) 0x7e,
+            (byte) 0xf1, (byte) 0x61
+        };
+    }
+
+    /*
+     * The keys and certificates below are generated with:
+     *
+     * openssl req -new -x509 -days 3650 -extensions v3_ca -keyout cakey.pem -out cacert.pem
+     * openssl dsaparam -out dsaparam.pem 1024
+     * openssl req -newkey dsa:dsaparam.pem -keyout userkey.pem -nodes -days 3650 -out userkey.req
+     * mkdir -p demoCA/newcerts
+     * touch demoCA/index.txt
+     * echo "01" > demoCA/serial
+     * openssl ca -out usercert.pem -in userkey.req -cert cacert.pem -keyfile cakey.pem -days 3650
+     */
+    public static class FAKE_DSA_1 {
+        /**
+         * Generated from above and converted with: openssl pkcs8 -topk8 -outform d
+         * -in userkey.pem -nocrypt | xxd -i | sed 's/0x/(byte) 0x/g'
+         */
+        public static final byte[] privateKey = {
+            (byte) 0x30, (byte) 0x82, (byte) 0x01, (byte) 0x4c, (byte) 0x02, (byte) 0x01,
+            (byte) 0x00, (byte) 0x30, (byte) 0x82, (byte) 0x01, (byte) 0x2c, (byte) 0x06,
+            (byte) 0x07, (byte) 0x2a, (byte) 0x86, (byte) 0x48, (byte) 0xce, (byte) 0x38,
+            (byte) 0x04, (byte) 0x01, (byte) 0x30, (byte) 0x82, (byte) 0x01, (byte) 0x1f,
+            (byte) 0x02, (byte) 0x81, (byte) 0x81, (byte) 0x00, (byte) 0xb3, (byte) 0x23,
+            (byte) 0xf7, (byte) 0x86, (byte) 0xbd, (byte) 0x3b, (byte) 0x86, (byte) 0xcc,
+            (byte) 0xc3, (byte) 0x91, (byte) 0xc0, (byte) 0x30, (byte) 0x32, (byte) 0x02,
+            (byte) 0x47, (byte) 0x35, (byte) 0x01, (byte) 0xef, (byte) 0xee, (byte) 0x98,
+            (byte) 0x13, (byte) 0x56, (byte) 0x49, (byte) 0x47, (byte) 0xb5, (byte) 0x20,
+            (byte) 0xa8, (byte) 0x60, (byte) 0xcb, (byte) 0xc0, (byte) 0xd5, (byte) 0x77,
+            (byte) 0xc1, (byte) 0x69, (byte) 0xcd, (byte) 0x18, (byte) 0x34, (byte) 0x92,
+            (byte) 0xf2, (byte) 0x6a, (byte) 0x2a, (byte) 0x10, (byte) 0x59, (byte) 0x1c,
+            (byte) 0x91, (byte) 0x20, (byte) 0x51, (byte) 0xca, (byte) 0x37, (byte) 0xb2,
+            (byte) 0x87, (byte) 0xa6, (byte) 0x8a, (byte) 0x02, (byte) 0xfd, (byte) 0x45,
+            (byte) 0x46, (byte) 0xf9, (byte) 0x76, (byte) 0xb1, (byte) 0x35, (byte) 0x38,
+            (byte) 0x8d, (byte) 0xff, (byte) 0x4c, (byte) 0x5d, (byte) 0x75, (byte) 0x8f,
+            (byte) 0x66, (byte) 0x15, (byte) 0x7d, (byte) 0x7b, (byte) 0xda, (byte) 0xdb,
+            (byte) 0x57, (byte) 0x39, (byte) 0xff, (byte) 0x91, (byte) 0x3f, (byte) 0xdd,
+            (byte) 0xe2, (byte) 0xb4, (byte) 0x22, (byte) 0x60, (byte) 0x4c, (byte) 0x32,
+            (byte) 0x3b, (byte) 0x9d, (byte) 0x34, (byte) 0x9f, (byte) 0xb9, (byte) 0x5d,
+            (byte) 0x75, (byte) 0xb9, (byte) 0xd3, (byte) 0x7f, (byte) 0x11, (byte) 0xba,
+            (byte) 0xb7, (byte) 0xc8, (byte) 0x32, (byte) 0xc6, (byte) 0xce, (byte) 0x71,
+            (byte) 0x91, (byte) 0xd3, (byte) 0x32, (byte) 0xaf, (byte) 0x4d, (byte) 0x7e,
+            (byte) 0x7c, (byte) 0x15, (byte) 0xf7, (byte) 0x71, (byte) 0x2c, (byte) 0x52,
+            (byte) 0x65, (byte) 0x4d, (byte) 0xa9, (byte) 0x81, (byte) 0x25, (byte) 0x35,
+            (byte) 0xce, (byte) 0x0b, (byte) 0x5b, (byte) 0x56, (byte) 0xfe, (byte) 0xf1,
+            (byte) 0x02, (byte) 0x15, (byte) 0x00, (byte) 0xeb, (byte) 0x4e, (byte) 0x7f,
+            (byte) 0x7a, (byte) 0x31, (byte) 0xb3, (byte) 0x7d, (byte) 0x8d, (byte) 0xb2,
+            (byte) 0xf7, (byte) 0xaf, (byte) 0xad, (byte) 0xb1, (byte) 0x42, (byte) 0x92,
+            (byte) 0xf3, (byte) 0x6c, (byte) 0xe4, (byte) 0xed, (byte) 0x8b, (byte) 0x02,
+            (byte) 0x81, (byte) 0x81, (byte) 0x00, (byte) 0x81, (byte) 0xc8, (byte) 0x36,
+            (byte) 0x48, (byte) 0xdb, (byte) 0x71, (byte) 0x2b, (byte) 0x91, (byte) 0xce,
+            (byte) 0x6d, (byte) 0xbc, (byte) 0xb8, (byte) 0xf9, (byte) 0xcb, (byte) 0x50,
+            (byte) 0x91, (byte) 0x10, (byte) 0x8a, (byte) 0xf8, (byte) 0x37, (byte) 0x50,
+            (byte) 0xda, (byte) 0x4f, (byte) 0xc8, (byte) 0x4d, (byte) 0x73, (byte) 0xcb,
+            (byte) 0x4d, (byte) 0xb0, (byte) 0x19, (byte) 0x54, (byte) 0x5a, (byte) 0xf3,
+            (byte) 0x6c, (byte) 0xc9, (byte) 0xd8, (byte) 0x96, (byte) 0xd9, (byte) 0xb0,
+            (byte) 0x54, (byte) 0x7e, (byte) 0x7d, (byte) 0xe2, (byte) 0x58, (byte) 0x0e,
+            (byte) 0x5f, (byte) 0xc0, (byte) 0xce, (byte) 0xb9, (byte) 0x5c, (byte) 0xe3,
+            (byte) 0xd3, (byte) 0xdf, (byte) 0xcf, (byte) 0x45, (byte) 0x74, (byte) 0xfb,
+            (byte) 0xe6, (byte) 0x20, (byte) 0xe7, (byte) 0xfc, (byte) 0x0f, (byte) 0xca,
+            (byte) 0xdb, (byte) 0xc0, (byte) 0x0b, (byte) 0xe1, (byte) 0x5a, (byte) 0x16,
+            (byte) 0x1d, (byte) 0xb3, (byte) 0x2e, (byte) 0xe5, (byte) 0x5f, (byte) 0x89,
+            (byte) 0x17, (byte) 0x73, (byte) 0x50, (byte) 0xd1, (byte) 0x4a, (byte) 0x60,
+            (byte) 0xb7, (byte) 0xaa, (byte) 0xf0, (byte) 0xc7, (byte) 0xc5, (byte) 0x03,
+            (byte) 0x4e, (byte) 0x36, (byte) 0x51, (byte) 0x9e, (byte) 0x2f, (byte) 0xfa,
+            (byte) 0xf3, (byte) 0xd6, (byte) 0x58, (byte) 0x14, (byte) 0x02, (byte) 0xb4,
+            (byte) 0x41, (byte) 0xd6, (byte) 0x72, (byte) 0x6f, (byte) 0x58, (byte) 0x5b,
+            (byte) 0x2d, (byte) 0x23, (byte) 0xc0, (byte) 0x75, (byte) 0x4f, (byte) 0x39,
+            (byte) 0xa8, (byte) 0x6a, (byte) 0xdf, (byte) 0x79, (byte) 0x21, (byte) 0xf2,
+            (byte) 0x77, (byte) 0x91, (byte) 0x3f, (byte) 0x1c, (byte) 0x4d, (byte) 0x48,
+            (byte) 0x78, (byte) 0xcd, (byte) 0xed, (byte) 0x79, (byte) 0x23, (byte) 0x04,
+            (byte) 0x17, (byte) 0x02, (byte) 0x15, (byte) 0x00, (byte) 0xc7, (byte) 0xe7,
+            (byte) 0xe2, (byte) 0x6b, (byte) 0x14, (byte) 0xe6, (byte) 0x31, (byte) 0x12,
+            (byte) 0xb2, (byte) 0x1e, (byte) 0xd4, (byte) 0xf2, (byte) 0x9b, (byte) 0x2c,
+            (byte) 0xf6, (byte) 0x54, (byte) 0x4c, (byte) 0x12, (byte) 0xe8, (byte) 0x22
+
+        };
+
+        /**
+         * Generated from above and converted with:
+         *
+         * openssl x509 -outform d -in cacert.pem | xxd -i | sed 's/0x/(byte) 0x/g'
+         */
+        public static final byte[] caCertificate = new byte[] {
+            (byte) 0x30, (byte) 0x82, (byte) 0x02, (byte) 0x8a, (byte) 0x30, (byte) 0x82,
+            (byte) 0x01, (byte) 0xf3, (byte) 0xa0, (byte) 0x03, (byte) 0x02, (byte) 0x01,
+            (byte) 0x02, (byte) 0x02, (byte) 0x09, (byte) 0x00, (byte) 0x87, (byte) 0xc0,
+            (byte) 0x68, (byte) 0x7f, (byte) 0x42, (byte) 0x92, (byte) 0x0b, (byte) 0x7a,
+            (byte) 0x30, (byte) 0x0d, (byte) 0x06, (byte) 0x09, (byte) 0x2a, (byte) 0x86,
+            (byte) 0x48, (byte) 0x86, (byte) 0xf7, (byte) 0x0d, (byte) 0x01, (byte) 0x01,
+            (byte) 0x05, (byte) 0x05, (byte) 0x00, (byte) 0x30, (byte) 0x5e, (byte) 0x31,
+            (byte) 0x0b, (byte) 0x30, (byte) 0x09, (byte) 0x06, (byte) 0x03, (byte) 0x55,
+            (byte) 0x04, (byte) 0x06, (byte) 0x13, (byte) 0x02, (byte) 0x41, (byte) 0x55,
+            (byte) 0x31, (byte) 0x13, (byte) 0x30, (byte) 0x11, (byte) 0x06, (byte) 0x03,
+            (byte) 0x55, (byte) 0x04, (byte) 0x08, (byte) 0x0c, (byte) 0x0a, (byte) 0x53,
+            (byte) 0x6f, (byte) 0x6d, (byte) 0x65, (byte) 0x2d, (byte) 0x53, (byte) 0x74,
+            (byte) 0x61, (byte) 0x74, (byte) 0x65, (byte) 0x31, (byte) 0x21, (byte) 0x30,
+            (byte) 0x1f, (byte) 0x06, (byte) 0x03, (byte) 0x55, (byte) 0x04, (byte) 0x0a,
+            (byte) 0x0c, (byte) 0x18, (byte) 0x49, (byte) 0x6e, (byte) 0x74, (byte) 0x65,
+            (byte) 0x72, (byte) 0x6e, (byte) 0x65, (byte) 0x74, (byte) 0x20, (byte) 0x57,
+            (byte) 0x69, (byte) 0x64, (byte) 0x67, (byte) 0x69, (byte) 0x74, (byte) 0x73,
+            (byte) 0x20, (byte) 0x50, (byte) 0x74, (byte) 0x79, (byte) 0x20, (byte) 0x4c,
+            (byte) 0x74, (byte) 0x64, (byte) 0x31, (byte) 0x17, (byte) 0x30, (byte) 0x15,
+            (byte) 0x06, (byte) 0x03, (byte) 0x55, (byte) 0x04, (byte) 0x03, (byte) 0x0c,
+            (byte) 0x0e, (byte) 0x63, (byte) 0x61, (byte) 0x2e, (byte) 0x65, (byte) 0x78,
+            (byte) 0x61, (byte) 0x6d, (byte) 0x70, (byte) 0x6c, (byte) 0x65, (byte) 0x2e,
+            (byte) 0x63, (byte) 0x6f, (byte) 0x6d, (byte) 0x30, (byte) 0x1e, (byte) 0x17,
+            (byte) 0x0d, (byte) 0x31, (byte) 0x33, (byte) 0x30, (byte) 0x38, (byte) 0x32,
+            (byte) 0x37, (byte) 0x32, (byte) 0x33, (byte) 0x33, (byte) 0x31, (byte) 0x32,
+            (byte) 0x39, (byte) 0x5a, (byte) 0x17, (byte) 0x0d, (byte) 0x32, (byte) 0x33,
+            (byte) 0x30, (byte) 0x38, (byte) 0x32, (byte) 0x35, (byte) 0x32, (byte) 0x33,
+            (byte) 0x33, (byte) 0x31, (byte) 0x32, (byte) 0x39, (byte) 0x5a, (byte) 0x30,
+            (byte) 0x5e, (byte) 0x31, (byte) 0x0b, (byte) 0x30, (byte) 0x09, (byte) 0x06,
+            (byte) 0x03, (byte) 0x55, (byte) 0x04, (byte) 0x06, (byte) 0x13, (byte) 0x02,
+            (byte) 0x41, (byte) 0x55, (byte) 0x31, (byte) 0x13, (byte) 0x30, (byte) 0x11,
+            (byte) 0x06, (byte) 0x03, (byte) 0x55, (byte) 0x04, (byte) 0x08, (byte) 0x0c,
+            (byte) 0x0a, (byte) 0x53, (byte) 0x6f, (byte) 0x6d, (byte) 0x65, (byte) 0x2d,
+            (byte) 0x53, (byte) 0x74, (byte) 0x61, (byte) 0x74, (byte) 0x65, (byte) 0x31,
+            (byte) 0x21, (byte) 0x30, (byte) 0x1f, (byte) 0x06, (byte) 0x03, (byte) 0x55,
+            (byte) 0x04, (byte) 0x0a, (byte) 0x0c, (byte) 0x18, (byte) 0x49, (byte) 0x6e,
+            (byte) 0x74, (byte) 0x65, (byte) 0x72, (byte) 0x6e, (byte) 0x65, (byte) 0x74,
+            (byte) 0x20, (byte) 0x57, (byte) 0x69, (byte) 0x64, (byte) 0x67, (byte) 0x69,
+            (byte) 0x74, (byte) 0x73, (byte) 0x20, (byte) 0x50, (byte) 0x74, (byte) 0x79,
+            (byte) 0x20, (byte) 0x4c, (byte) 0x74, (byte) 0x64, (byte) 0x31, (byte) 0x17,
+            (byte) 0x30, (byte) 0x15, (byte) 0x06, (byte) 0x03, (byte) 0x55, (byte) 0x04,
+            (byte) 0x03, (byte) 0x0c, (byte) 0x0e, (byte) 0x63, (byte) 0x61, (byte) 0x2e,
+            (byte) 0x65, (byte) 0x78, (byte) 0x61, (byte) 0x6d, (byte) 0x70, (byte) 0x6c,
+            (byte) 0x65, (byte) 0x2e, (byte) 0x63, (byte) 0x6f, (byte) 0x6d, (byte) 0x30,
+            (byte) 0x81, (byte) 0x9f, (byte) 0x30, (byte) 0x0d, (byte) 0x06, (byte) 0x09,
+            (byte) 0x2a, (byte) 0x86, (byte) 0x48, (byte) 0x86, (byte) 0xf7, (byte) 0x0d,
+            (byte) 0x01, (byte) 0x01, (byte) 0x01, (byte) 0x05, (byte) 0x00, (byte) 0x03,
+            (byte) 0x81, (byte) 0x8d, (byte) 0x00, (byte) 0x30, (byte) 0x81, (byte) 0x89,
+            (byte) 0x02, (byte) 0x81, (byte) 0x81, (byte) 0x00, (byte) 0xa4, (byte) 0xc7,
+            (byte) 0x06, (byte) 0xba, (byte) 0xdf, (byte) 0x2b, (byte) 0xee, (byte) 0xd2,
+            (byte) 0xb9, (byte) 0xe4, (byte) 0x52, (byte) 0x21, (byte) 0x68, (byte) 0x2b,
+            (byte) 0x83, (byte) 0xdf, (byte) 0xe3, (byte) 0x9c, (byte) 0x08, (byte) 0x73,
+            (byte) 0xdd, (byte) 0x90, (byte) 0xea, (byte) 0x97, (byte) 0x0c, (byte) 0x96,
+            (byte) 0x20, (byte) 0xb1, (byte) 0xee, (byte) 0x11, (byte) 0xd5, (byte) 0xd4,
+            (byte) 0x7c, (byte) 0x44, (byte) 0x96, (byte) 0x2e, (byte) 0x6e, (byte) 0xa2,
+            (byte) 0xb2, (byte) 0xa3, (byte) 0x4b, (byte) 0x0f, (byte) 0x32, (byte) 0x90,
+            (byte) 0xaf, (byte) 0x5c, (byte) 0x6f, (byte) 0x00, (byte) 0x88, (byte) 0x45,
+            (byte) 0x4e, (byte) 0x9b, (byte) 0x26, (byte) 0xc1, (byte) 0x94, (byte) 0x3c,
+            (byte) 0xfe, (byte) 0x10, (byte) 0xbd, (byte) 0xda, (byte) 0xf2, (byte) 0x8d,
+            (byte) 0x03, (byte) 0x52, (byte) 0x32, (byte) 0x11, (byte) 0xff, (byte) 0xf6,
+            (byte) 0xf9, (byte) 0x6e, (byte) 0x8f, (byte) 0x0f, (byte) 0xc8, (byte) 0x0a,
+            (byte) 0x48, (byte) 0x39, (byte) 0x33, (byte) 0xb9, (byte) 0x0c, (byte) 0xb3,
+            (byte) 0x2b, (byte) 0xab, (byte) 0x7d, (byte) 0x79, (byte) 0x6f, (byte) 0x57,
+            (byte) 0x5b, (byte) 0xb8, (byte) 0x84, (byte) 0xb6, (byte) 0xcc, (byte) 0xe8,
+            (byte) 0x30, (byte) 0x78, (byte) 0xff, (byte) 0x92, (byte) 0xe5, (byte) 0x43,
+            (byte) 0x2e, (byte) 0xef, (byte) 0x66, (byte) 0x98, (byte) 0xb4, (byte) 0xfe,
+            (byte) 0xa2, (byte) 0x40, (byte) 0xf2, (byte) 0x1f, (byte) 0xd0, (byte) 0x86,
+            (byte) 0x16, (byte) 0xc8, (byte) 0x45, (byte) 0xc4, (byte) 0x52, (byte) 0xcb,
+            (byte) 0x31, (byte) 0x5c, (byte) 0x9f, (byte) 0x32, (byte) 0x3b, (byte) 0xf7,
+            (byte) 0x19, (byte) 0x08, (byte) 0xc7, (byte) 0x00, (byte) 0x21, (byte) 0x7d,
+            (byte) 0x02, (byte) 0x03, (byte) 0x01, (byte) 0x00, (byte) 0x01, (byte) 0xa3,
+            (byte) 0x50, (byte) 0x30, (byte) 0x4e, (byte) 0x30, (byte) 0x1d, (byte) 0x06,
+            (byte) 0x03, (byte) 0x55, (byte) 0x1d, (byte) 0x0e, (byte) 0x04, (byte) 0x16,
+            (byte) 0x04, (byte) 0x14, (byte) 0x47, (byte) 0x82, (byte) 0xa3, (byte) 0xf1,
+            (byte) 0xc2, (byte) 0x7e, (byte) 0x3a, (byte) 0xde, (byte) 0x4f, (byte) 0x30,
+            (byte) 0x4c, (byte) 0x7f, (byte) 0x72, (byte) 0x81, (byte) 0x15, (byte) 0x32,
+            (byte) 0xda, (byte) 0x7f, (byte) 0x58, (byte) 0x18, (byte) 0x30, (byte) 0x1f,
+            (byte) 0x06, (byte) 0x03, (byte) 0x55, (byte) 0x1d, (byte) 0x23, (byte) 0x04,
+            (byte) 0x18, (byte) 0x30, (byte) 0x16, (byte) 0x80, (byte) 0x14, (byte) 0x47,
+            (byte) 0x82, (byte) 0xa3, (byte) 0xf1, (byte) 0xc2, (byte) 0x7e, (byte) 0x3a,
+            (byte) 0xde, (byte) 0x4f, (byte) 0x30, (byte) 0x4c, (byte) 0x7f, (byte) 0x72,
+            (byte) 0x81, (byte) 0x15, (byte) 0x32, (byte) 0xda, (byte) 0x7f, (byte) 0x58,
+            (byte) 0x18, (byte) 0x30, (byte) 0x0c, (byte) 0x06, (byte) 0x03, (byte) 0x55,
+            (byte) 0x1d, (byte) 0x13, (byte) 0x04, (byte) 0x05, (byte) 0x30, (byte) 0x03,
+            (byte) 0x01, (byte) 0x01, (byte) 0xff, (byte) 0x30, (byte) 0x0d, (byte) 0x06,
+            (byte) 0x09, (byte) 0x2a, (byte) 0x86, (byte) 0x48, (byte) 0x86, (byte) 0xf7,
+            (byte) 0x0d, (byte) 0x01, (byte) 0x01, (byte) 0x05, (byte) 0x05, (byte) 0x00,
+            (byte) 0x03, (byte) 0x81, (byte) 0x81, (byte) 0x00, (byte) 0x08, (byte) 0x7f,
+            (byte) 0x6a, (byte) 0x48, (byte) 0x90, (byte) 0x7b, (byte) 0x9b, (byte) 0x72,
+            (byte) 0x13, (byte) 0xa7, (byte) 0xef, (byte) 0x6b, (byte) 0x0b, (byte) 0x59,
+            (byte) 0xe5, (byte) 0x49, (byte) 0x72, (byte) 0x3a, (byte) 0xc8, (byte) 0x84,
+            (byte) 0xcc, (byte) 0x23, (byte) 0x18, (byte) 0x4c, (byte) 0xec, (byte) 0xc7,
+            (byte) 0xef, (byte) 0xcb, (byte) 0xa7, (byte) 0xbe, (byte) 0xe4, (byte) 0xef,
+            (byte) 0x8f, (byte) 0xc6, (byte) 0x06, (byte) 0x8c, (byte) 0xc0, (byte) 0xe4,
+            (byte) 0x2f, (byte) 0x2a, (byte) 0xc0, (byte) 0x35, (byte) 0x7d, (byte) 0x5e,
+            (byte) 0x19, (byte) 0x29, (byte) 0x8c, (byte) 0xb9, (byte) 0xf1, (byte) 0x1e,
+            (byte) 0xaf, (byte) 0x82, (byte) 0xd8, (byte) 0xe3, (byte) 0x88, (byte) 0xe1,
+            (byte) 0x31, (byte) 0xc8, (byte) 0x82, (byte) 0x1f, (byte) 0x83, (byte) 0xa9,
+            (byte) 0xde, (byte) 0xfe, (byte) 0x4b, (byte) 0xe2, (byte) 0x78, (byte) 0x64,
+            (byte) 0xed, (byte) 0xa4, (byte) 0x7b, (byte) 0xee, (byte) 0x8d, (byte) 0x71,
+            (byte) 0x1b, (byte) 0x44, (byte) 0xe6, (byte) 0xb7, (byte) 0xe8, (byte) 0xc5,
+            (byte) 0x9a, (byte) 0x93, (byte) 0x92, (byte) 0x6f, (byte) 0x6f, (byte) 0xdb,
+            (byte) 0xbd, (byte) 0xd7, (byte) 0x03, (byte) 0x85, (byte) 0xa9, (byte) 0x5f,
+            (byte) 0x53, (byte) 0x5f, (byte) 0x5d, (byte) 0x30, (byte) 0xc6, (byte) 0xd9,
+            (byte) 0xce, (byte) 0x34, (byte) 0xa8, (byte) 0xbe, (byte) 0x31, (byte) 0x47,
+            (byte) 0x1c, (byte) 0xa4, (byte) 0x7f, (byte) 0xc0, (byte) 0x2c, (byte) 0xbc,
+            (byte) 0xfe, (byte) 0x1a, (byte) 0x31, (byte) 0xd8, (byte) 0x77, (byte) 0x4d,
+            (byte) 0xfc, (byte) 0x45, (byte) 0x84, (byte) 0xfc, (byte) 0x45, (byte) 0x12,
+            (byte) 0xab, (byte) 0x50, (byte) 0xe4, (byte) 0x45, (byte) 0xe5, (byte) 0x11
+        };
+    }
+}
diff --git a/common/device-side/util-axt/src/com/android/compatibility/common/util/FeatureUtil.java b/common/device-side/util-axt/src/com/android/compatibility/common/util/FeatureUtil.java
new file mode 100644
index 0000000..b860b96
--- /dev/null
+++ b/common/device-side/util-axt/src/com/android/compatibility/common/util/FeatureUtil.java
@@ -0,0 +1,128 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.compatibility.common.util;
+
+import android.content.Context;
+import android.content.pm.FeatureInfo;
+import android.content.pm.PackageManager;
+import android.content.res.Configuration;
+import android.os.Build;
+import androidx.test.InstrumentationRegistry;
+
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * Device-side utility class for detecting system features
+ */
+public class FeatureUtil {
+
+    public static final String AUTOMOTIVE_FEATURE = "android.hardware.type.automotive";
+    public static final String LEANBACK_FEATURE = "android.software.leanback";
+    public static final String LOW_RAM_FEATURE = "android.hardware.ram.low";
+    public static final String TELEPHONY_FEATURE = "android.hardware.telephony";
+    public static final String TV_FEATURE = "android.hardware.type.television";
+    public static final String WATCH_FEATURE = "android.hardware.type.watch";
+
+
+    /** Returns true if the device has a given system feature */
+    public static boolean hasSystemFeature(String feature) {
+        return getPackageManager().hasSystemFeature(feature);
+    }
+
+    /** Returns true if the device has any feature in a given collection of system features */
+    public static boolean hasAnySystemFeature(String... features) {
+        PackageManager pm = getPackageManager();
+        for (String feature : features) {
+            if (pm.hasSystemFeature(feature)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /** Returns true if the device has all features in a given collection of system features */
+    public static boolean hasAllSystemFeatures(String... features) {
+        PackageManager pm = getPackageManager();
+        for (String feature : features) {
+            if (!pm.hasSystemFeature(feature)) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    /** Returns all system features of the device */
+    public static Set<String> getAllFeatures() {
+        Set<String> allFeatures = new HashSet<String>();
+        for (FeatureInfo fi : getPackageManager().getSystemAvailableFeatures()) {
+            allFeatures.add(fi.name);
+        }
+        return allFeatures;
+    }
+
+    /** Returns true if the device has feature TV_FEATURE or feature LEANBACK_FEATURE */
+    public static boolean isTV() {
+        return hasAnySystemFeature(TV_FEATURE, LEANBACK_FEATURE);
+    }
+
+    /** Returns true if the device has feature WATCH_FEATURE */
+    public static boolean isWatch() {
+        return hasSystemFeature(WATCH_FEATURE);
+    }
+
+    /** Returns true if the device has feature AUTOMOTIVE_FEATURE */
+    public static boolean isAutomotive() {
+        return hasSystemFeature(AUTOMOTIVE_FEATURE);
+    }
+
+    public static boolean isVrHeadset() {
+        int maskedUiMode = (getConfiguration().uiMode & Configuration.UI_MODE_TYPE_MASK);
+        return (maskedUiMode == Configuration.UI_MODE_TYPE_VR_HEADSET);
+    }
+
+    /** Returns true if the device is a low ram device:
+     *  1. API level &gt;= O_MR1
+     *  2. device has feature LOW_RAM_FEATURE
+     */
+    public static boolean isLowRam() {
+        return ApiLevelUtil.isAtLeast(Build.VERSION_CODES.O_MR1) &&
+                hasSystemFeature(LOW_RAM_FEATURE);
+    }
+
+    private static Context getContext() {
+        return InstrumentationRegistry.getInstrumentation().getTargetContext();
+    }
+
+    private static PackageManager getPackageManager() {
+        return getContext().getPackageManager();
+    }
+
+    private static Configuration getConfiguration() {
+        return getContext().getResources().getConfiguration();
+    }
+
+    /** Returns true if the device has feature TELEPHONY_FEATURE */
+    public static boolean hasTelephony() {
+        return hasSystemFeature(TELEPHONY_FEATURE);
+    }
+
+    /** Returns true if the device has feature FEATURE_MICROPHONE */
+    public static boolean hasMicrophone() {
+        return hasSystemFeature(getPackageManager().FEATURE_MICROPHONE);
+    }
+}
diff --git a/common/device-side/util-axt/src/com/android/compatibility/common/util/FileCopyHelper.java b/common/device-side/util-axt/src/com/android/compatibility/common/util/FileCopyHelper.java
new file mode 100644
index 0000000..f58dbd0
--- /dev/null
+++ b/common/device-side/util-axt/src/com/android/compatibility/common/util/FileCopyHelper.java
@@ -0,0 +1,100 @@
+/*
+ * Copyright (C) 2009 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.compatibility.common.util;
+
+import android.content.Context;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.ArrayList;
+
+/**
+ * FileCopyHelper is used to copy files from resources to the
+ * application directory and responsible for deleting the files.
+ *
+ * @see MediaStore_VideoTest
+ * @see MediaStore_Images_MediaTest
+ * @see MediaStore_Images_ThumbnailsTest
+ */
+public class FileCopyHelper {
+    /** The context. */
+    private Context mContext;
+
+    /** The files added. */
+    private ArrayList<String> mFilesList;
+
+    /**
+     * Instantiates a new file copy helper.
+     *
+     * @param context the context
+     */
+    public FileCopyHelper(Context context) {
+        mContext = context;
+        mFilesList = new ArrayList<String>();
+    }
+
+    /**
+     * Copy the file from the resources with a filename.
+     *
+     * @param resId the res id
+     * @param fileName the file name
+     *
+     * @return the absolute path of the destination file
+     * @throws IOException
+     */
+    public String copy(int resId, String fileName) throws IOException {
+        InputStream source = mContext.getResources().openRawResource(resId);
+        OutputStream target = mContext.openFileOutput(fileName, Context.MODE_WORLD_READABLE);
+        copyFile(source, target);
+        mFilesList.add(fileName);
+        return mContext.getFileStreamPath(fileName).getAbsolutePath();
+    }
+
+    public void copyToExternalStorage(int resId, File path) throws IOException {
+        InputStream source = mContext.getResources().openRawResource(resId);
+        OutputStream target = new FileOutputStream(path);
+        copyFile(source, target);
+    }
+
+    private void copyFile(InputStream source, OutputStream target) throws IOException {
+        try {
+            byte[] buffer = new byte[1024];
+            for (int len = source.read(buffer); len > 0; len = source.read(buffer)) {
+                target.write(buffer, 0, len);
+            }
+        } finally {
+            if (source != null) {
+                source.close();
+            }
+            if (target != null) {
+                target.close();
+            }
+        }
+    }
+
+    /**
+     * Delete all the files copied by the helper.
+     */
+    public void clear(){
+        for (String path : mFilesList) {
+            mContext.deleteFile(path);
+        }
+    }
+}
diff --git a/common/device-side/util-axt/src/com/android/compatibility/common/util/FileUtils.java b/common/device-side/util-axt/src/com/android/compatibility/common/util/FileUtils.java
new file mode 100644
index 0000000..ceada01
--- /dev/null
+++ b/common/device-side/util-axt/src/com/android/compatibility/common/util/FileUtils.java
@@ -0,0 +1,163 @@
+/*
+ * Copyright (C) 2011 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.compatibility.common.util;
+
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+
+/** Bits and pieces copied from hidden API of android.os.FileUtils. */
+public class FileUtils {
+
+    public static final int S_IFMT  = 0170000;
+    public static final int S_IFSOCK = 0140000;
+    public static final int S_IFLNK = 0120000;
+    public static final int S_IFREG = 0100000;
+    public static final int S_IFBLK = 0060000;
+    public static final int S_IFDIR = 0040000;
+    public static final int S_IFCHR = 0020000;
+    public static final int S_IFIFO = 0010000;
+
+    public static final int S_ISUID = 0004000;
+    public static final int S_ISGID = 0002000;
+    public static final int S_ISVTX = 0001000;
+
+    public static final int S_IRWXU = 00700;
+    public static final int S_IRUSR = 00400;
+    public static final int S_IWUSR = 00200;
+    public static final int S_IXUSR = 00100;
+
+    public static final int S_IRWXG = 00070;
+    public static final int S_IRGRP = 00040;
+    public static final int S_IWGRP = 00020;
+    public static final int S_IXGRP = 00010;
+
+    public static final int S_IRWXO = 00007;
+    public static final int S_IROTH = 00004;
+    public static final int S_IWOTH = 00002;
+    public static final int S_IXOTH = 00001;
+
+    static {
+        System.loadLibrary("cts_jni");
+    }
+
+    public static class FileStatus {
+
+        public int dev;
+        public int ino;
+        public int mode;
+        public int nlink;
+        public int uid;
+        public int gid;
+        public int rdev;
+        public long size;
+        public int blksize;
+        public long blocks;
+        public long atime;
+        public long mtime;
+        public long ctime;
+
+        public boolean hasModeFlag(int flag) {
+            if (((S_IRWXU | S_IRWXG | S_IRWXO) & flag) != flag) {
+                throw new IllegalArgumentException("Inappropriate flag " + flag);
+            }
+            return (mode & flag) == flag;
+        }
+
+        public boolean isOfType(int type) {
+            if ((type & S_IFMT) != type) {
+                throw new IllegalArgumentException("Unknown type " + type);
+            }
+            return (mode & S_IFMT) == type;
+        }
+    }
+
+    /**
+     * @param path of the file to stat
+     * @param status object to set the fields on
+     * @param statLinks or don't stat links (lstat vs stat)
+     * @return whether or not we were able to stat the file
+     */
+    public native static boolean getFileStatus(String path, FileStatus status, boolean statLinks);
+
+    public native static String getUserName(int uid);
+
+    public native static String getGroupName(int gid);
+
+    public native static int setPermissions(String file, int mode);
+
+    /**
+     * Copy data from a source stream to destFile.
+     * Return true if succeed, return false if failed.
+     */
+    public static boolean copyToFile(InputStream inputStream, File destFile) {
+        try {
+            if (destFile.exists()) {
+                destFile.delete();
+            }
+            FileOutputStream out = new FileOutputStream(destFile);
+            try {
+                byte[] buffer = new byte[4096];
+                int bytesRead;
+                while ((bytesRead = inputStream.read(buffer)) >= 0) {
+                    out.write(buffer, 0, bytesRead);
+                }
+            } finally {
+                out.flush();
+                try {
+                    out.getFD().sync();
+                } catch (IOException e) {
+                }
+                out.close();
+            }
+            return true;
+        } catch (IOException e) {
+            return false;
+        }
+    }
+
+    public static void createFile(File file, int numBytes) throws IOException {
+        File parentFile = file.getParentFile();
+        if (parentFile != null) {
+            parentFile.mkdirs();
+        }
+        byte[] buffer = new byte[numBytes];
+        FileOutputStream output = new FileOutputStream(file);
+        try {
+            output.write(buffer);
+        } finally {
+            output.close();
+        }
+    }
+
+    public static byte[] readInputStreamFully(InputStream is) {
+        ByteArrayOutputStream os = new ByteArrayOutputStream();
+        byte[] buffer = new byte[32768];
+        int count;
+        try {
+            while ((count = is.read(buffer)) != -1) {
+                os.write(buffer, 0, count);
+            }
+            is.close();
+        } catch (IOException e) {
+            throw new RuntimeException(e);
+        }
+        return os.toByteArray();
+    }
+}
diff --git a/common/device-side/util-axt/src/com/android/compatibility/common/util/IBinderParcelable.java b/common/device-side/util-axt/src/com/android/compatibility/common/util/IBinderParcelable.java
new file mode 100644
index 0000000..f3c53fe
--- /dev/null
+++ b/common/device-side/util-axt/src/com/android/compatibility/common/util/IBinderParcelable.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright (C) 2009 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.compatibility.common.util;
+
+import android.os.IBinder;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+public class IBinderParcelable implements Parcelable {
+    public IBinder binder;
+
+    public IBinderParcelable(IBinder source) {
+        binder = source;
+    }
+
+    public int describeContents() {
+        return 0;
+    }
+
+    public void writeToParcel(Parcel dest, int flags) {
+        dest.writeStrongBinder(binder);
+    }
+
+    public static final Parcelable.Creator<IBinderParcelable>
+        CREATOR = new Parcelable.Creator<IBinderParcelable>() {
+
+        public IBinderParcelable createFromParcel(Parcel source) {
+            return new IBinderParcelable(source);
+        }
+
+        public IBinderParcelable[] newArray(int size) {
+            return new IBinderParcelable[size];
+        }
+    };
+
+    private IBinderParcelable(Parcel source) {
+        binder = source.readStrongBinder();
+    }
+}
diff --git a/common/device-side/util-axt/src/com/android/compatibility/common/util/LocationUtils.java b/common/device-side/util-axt/src/com/android/compatibility/common/util/LocationUtils.java
new file mode 100644
index 0000000..f233851
--- /dev/null
+++ b/common/device-side/util-axt/src/com/android/compatibility/common/util/LocationUtils.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2015 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.compatibility.common.util;
+
+import android.app.Instrumentation;
+import android.util.Log;
+
+import java.io.IOException;
+
+public class LocationUtils {
+    private static String TAG = "LocationUtils";
+
+    public static void registerMockLocationProvider(Instrumentation instrumentation,
+            boolean enable) {
+        StringBuilder command = new StringBuilder();
+        command.append("appops set ");
+        command.append(instrumentation.getContext().getPackageName());
+        command.append(" android:mock_location ");
+        command.append(enable ? "allow" : "deny");
+        try {
+            SystemUtil.runShellCommand(instrumentation, command.toString());
+        } catch (IOException e) {
+            Log.e(TAG, "Error managing mock location app. Command: " + command, e);
+        }
+    }
+}
diff --git a/common/device-side/util-axt/src/com/android/compatibility/common/util/MediaPerfUtils.java b/common/device-side/util-axt/src/com/android/compatibility/common/util/MediaPerfUtils.java
new file mode 100644
index 0000000..469e99a
--- /dev/null
+++ b/common/device-side/util-axt/src/com/android/compatibility/common/util/MediaPerfUtils.java
@@ -0,0 +1,176 @@
+/*
+ * Copyright 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.compatibility.common.util;
+
+import android.media.MediaFormat;
+import android.util.Range;
+
+import com.android.compatibility.common.util.DeviceReportLog;
+import com.android.compatibility.common.util.ResultType;
+import com.android.compatibility.common.util.ResultUnit;
+
+import java.util.Arrays;
+import android.util.Log;
+
+public class MediaPerfUtils {
+    private static final String TAG = "MediaPerfUtils";
+
+    private static final int MOVING_AVERAGE_NUM_FRAMES = 10;
+    private static final int MOVING_AVERAGE_WINDOW_MS = 1000;
+
+    // allow a variance of 2x for measured frame rates (e.g. half of lower-limit to double of
+    // upper-limit of the published values). Also allow an extra 10% margin. This also acts as
+    // a limit for the size of the published rates (e.g. upper-limit / lower-limit <= tolerance).
+    private static final double FRAMERATE_TOLERANCE = 2.0 * 1.1;
+
+    /*
+     *  ------------------ HELPER METHODS FOR ACHIEVABLE FRAME RATES ------------------
+     */
+
+    /** removes brackets from format to be included in JSON. */
+    private static String formatForReport(MediaFormat format) {
+        String asString = "" + format;
+        return asString.substring(1, asString.length() - 1);
+    }
+
+    /**
+     * Adds performance header info to |log| for |codecName|, |round|, |configFormat|, |inputFormat|
+     * and |outputFormat|. Also appends same to |message| and returns the resulting base message
+     * for logging purposes.
+     */
+    public static String addPerformanceHeadersToLog(
+            DeviceReportLog log, String message, int round, String codecName,
+            MediaFormat configFormat, MediaFormat inputFormat, MediaFormat outputFormat) {
+        String mime = configFormat.getString(MediaFormat.KEY_MIME);
+        int width = configFormat.getInteger(MediaFormat.KEY_WIDTH);
+        int height = configFormat.getInteger(MediaFormat.KEY_HEIGHT);
+
+        log.addValue("round", round, ResultType.NEUTRAL, ResultUnit.NONE);
+        log.addValue("codec_name", codecName, ResultType.NEUTRAL, ResultUnit.NONE);
+        log.addValue("mime_type", mime, ResultType.NEUTRAL, ResultUnit.NONE);
+        log.addValue("width", width, ResultType.NEUTRAL, ResultUnit.NONE);
+        log.addValue("height", height, ResultType.NEUTRAL, ResultUnit.NONE);
+        log.addValue("config_format", formatForReport(configFormat),
+                ResultType.NEUTRAL, ResultUnit.NONE);
+        log.addValue("input_format", formatForReport(inputFormat),
+                ResultType.NEUTRAL, ResultUnit.NONE);
+        log.addValue("output_format", formatForReport(outputFormat),
+                ResultType.NEUTRAL, ResultUnit.NONE);
+
+        message += " codec=" + codecName + " round=" + round + " configFormat=" + configFormat
+                + " inputFormat=" + inputFormat + " outputFormat=" + outputFormat;
+
+        Range<Double> reported =
+            MediaUtils.getVideoCapabilities(codecName, mime)
+                    .getAchievableFrameRatesFor(width, height);
+        if (reported != null) {
+            log.addValue("reported_low", reported.getLower(), ResultType.NEUTRAL, ResultUnit.FPS);
+            log.addValue("reported_high", reported.getUpper(), ResultType.NEUTRAL, ResultUnit.FPS);
+            message += " reported=" + reported.getLower() + "-" + reported.getUpper();
+        }
+
+        return message;
+    }
+
+    /**
+     * Adds performance statistics based on the raw |stats| to |log|. Also prints the same into
+     * logcat. Returns the "final fps" value.
+     */
+    public static double addPerformanceStatsToLog(
+            DeviceReportLog log, MediaUtils.Stats durationsUsStats, String message) {
+
+        MediaUtils.Stats frameAvgUsStats =
+            durationsUsStats.movingAverage(MOVING_AVERAGE_NUM_FRAMES);
+        log.addValue(
+                "window_frames", MOVING_AVERAGE_NUM_FRAMES, ResultType.NEUTRAL, ResultUnit.COUNT);
+        logPerformanceStats(log, frameAvgUsStats, "frame_avg_stats",
+                message + " window=" + MOVING_AVERAGE_NUM_FRAMES);
+
+        MediaUtils.Stats timeAvgUsStats =
+            durationsUsStats.movingAverageOverSum(MOVING_AVERAGE_WINDOW_MS * 1000);
+        log.addValue("window_time", MOVING_AVERAGE_WINDOW_MS, ResultType.NEUTRAL, ResultUnit.MS);
+        double fps = logPerformanceStats(log, timeAvgUsStats, "time_avg_stats",
+                message + " windowMs=" + MOVING_AVERAGE_WINDOW_MS);
+
+        log.setSummary("fps", fps, ResultType.HIGHER_BETTER, ResultUnit.FPS);
+        return fps;
+    }
+
+    /**
+     * Adds performance statistics based on the processed |stats| to |log| using |prefix|.
+     * Also prints the same into logcat using |message| as the base message. Returns the fps value
+     * for |stats|. |prefix| must be lowercase alphanumeric underscored format.
+     */
+    private static double logPerformanceStats(
+            DeviceReportLog log, MediaUtils.Stats statsUs, String prefix, String message) {
+        final String[] labels = {
+            "min", "p5", "p10", "p20", "p30", "p40", "p50", "p60", "p70", "p80", "p90", "p95", "max"
+        };
+        final double[] points = {
+             0,     5,    10,    20,    30,    40,    50,    60,    70,    80,    90,    95,    100
+        };
+
+        int num = statsUs.getNum();
+        long avg = Math.round(statsUs.getAverage());
+        long stdev = Math.round(statsUs.getStdev());
+        log.addValue(prefix + "_num", num, ResultType.NEUTRAL, ResultUnit.COUNT);
+        log.addValue(prefix + "_avg", avg / 1000., ResultType.LOWER_BETTER, ResultUnit.MS);
+        log.addValue(prefix + "_stdev", stdev / 1000., ResultType.LOWER_BETTER, ResultUnit.MS);
+        message += " num=" + num + " avg=" + avg + " stdev=" + stdev;
+        final double[] percentiles = statsUs.getPercentiles(points);
+        for (int i = 0; i < labels.length; ++i) {
+            long p = Math.round(percentiles[i]);
+            message += " " + labels[i] + "=" + p;
+            log.addValue(prefix + "_" + labels[i], p / 1000., ResultType.NEUTRAL, ResultUnit.MS);
+        }
+
+        // print result to logcat in case test aborts before logs are written
+        Log.i(TAG, message);
+
+        return 1e6 / percentiles[points.length - 2];
+    }
+
+    /** Verifies |measuredFps| against reported achievable rates. Returns null if at least
+     *  one measurement falls within the margins of the reported range. Otherwise, returns
+     *  an error message to display.*/
+    public static String verifyAchievableFrameRates(
+            String name, String mime, int w, int h, double... measuredFps) {
+        Range<Double> reported =
+            MediaUtils.getVideoCapabilities(name, mime).getAchievableFrameRatesFor(w, h);
+        String kind = "achievable frame rates for " + name + " " + mime + " " + w + "x" + h;
+        if (reported == null) {
+            return "Failed to get " + kind;
+        }
+        double lowerBoundary1 = reported.getLower() / FRAMERATE_TOLERANCE;
+        double upperBoundary1 = reported.getUpper() * FRAMERATE_TOLERANCE;
+        double lowerBoundary2 = reported.getUpper() / Math.pow(FRAMERATE_TOLERANCE, 2);
+        double upperBoundary2 = reported.getLower() * Math.pow(FRAMERATE_TOLERANCE, 2);
+        Log.d(TAG, name + " " + mime + " " + w + "x" + h +
+                " lowerBoundary1 " + lowerBoundary1 + " upperBoundary1 " + upperBoundary1 +
+                " lowerBoundary2 " + lowerBoundary2 + " upperBoundary2 " + upperBoundary2 +
+                " measured " + Arrays.toString(measuredFps));
+
+        for (double measured : measuredFps) {
+            if (measured >= lowerBoundary1 && measured <= upperBoundary1
+                    && measured >= lowerBoundary2 && measured <= upperBoundary2) {
+                return null;
+            }
+        }
+
+        return "Expected " + kind + ": " + reported + ".\n"
+                + "Measured frame rate: " + Arrays.toString(measuredFps) + ".\n";
+    }
+}
diff --git a/common/device-side/util-axt/src/com/android/compatibility/common/util/MediaUtils.java b/common/device-side/util-axt/src/com/android/compatibility/common/util/MediaUtils.java
new file mode 100644
index 0000000..3ea6b63
--- /dev/null
+++ b/common/device-side/util-axt/src/com/android/compatibility/common/util/MediaUtils.java
@@ -0,0 +1,1232 @@
+/*
+ * Copyright 2014 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.compatibility.common.util;
+
+import android.content.Context;
+import android.content.res.AssetFileDescriptor;
+import android.drm.DrmConvertedStatus;
+import android.drm.DrmManagerClient;
+import android.graphics.ImageFormat;
+import android.media.Image;
+import android.media.Image.Plane;
+import android.media.MediaCodec;
+import android.media.MediaCodec.BufferInfo;
+import android.media.MediaCodecInfo;
+import android.media.MediaCodecInfo.CodecCapabilities;
+import android.media.MediaCodecInfo.VideoCapabilities;
+import android.media.MediaCodecList;
+import android.media.MediaExtractor;
+import android.media.MediaFormat;
+import android.net.Uri;
+import android.util.Log;
+import android.util.Range;
+
+import com.android.compatibility.common.util.DeviceReportLog;
+import com.android.compatibility.common.util.ResultType;
+import com.android.compatibility.common.util.ResultUnit;
+
+import java.lang.reflect.Method;
+import java.nio.ByteBuffer;
+import java.security.MessageDigest;
+
+import static java.lang.reflect.Modifier.isPublic;
+import static java.lang.reflect.Modifier.isStatic;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+
+import static junit.framework.Assert.assertTrue;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.RandomAccessFile;
+
+public class MediaUtils {
+    private static final String TAG = "MediaUtils";
+
+    /*
+     *  ----------------------- HELPER METHODS FOR SKIPPING TESTS -----------------------
+     */
+    private static final int ALL_AV_TRACKS = -1;
+
+    private static final MediaCodecList sMCL = new MediaCodecList(MediaCodecList.REGULAR_CODECS);
+
+    /**
+     * Returns the test name (heuristically).
+     *
+     * Since it uses heuristics, this method has only been verified for media
+     * tests. This centralizes the way to signal errors during a test.
+     */
+    public static String getTestName() {
+        return getTestName(false /* withClass */);
+    }
+
+    /**
+     * Returns the test name with the full class (heuristically).
+     *
+     * Since it uses heuristics, this method has only been verified for media
+     * tests. This centralizes the way to signal errors during a test.
+     */
+    public static String getTestNameWithClass() {
+        return getTestName(true /* withClass */);
+    }
+
+    private static String getTestName(boolean withClass) {
+        int bestScore = -1;
+        String testName = "test???";
+        Map<Thread, StackTraceElement[]> traces = Thread.getAllStackTraces();
+        for (Map.Entry<Thread, StackTraceElement[]> entry : traces.entrySet()) {
+            StackTraceElement[] stack = entry.getValue();
+            for (int index = 0; index < stack.length; ++index) {
+                // method name must start with "test"
+                String methodName = stack[index].getMethodName();
+                if (!methodName.startsWith("test")) {
+                    continue;
+                }
+
+                int score = 0;
+                // see if there is a public non-static void method that takes no argument
+                Class<?> clazz;
+                try {
+                    clazz = Class.forName(stack[index].getClassName());
+                    ++score;
+                    for (final Method method : clazz.getDeclaredMethods()) {
+                        if (method.getName().equals(methodName)
+                                && isPublic(method.getModifiers())
+                                && !isStatic(method.getModifiers())
+                                && method.getParameterTypes().length == 0
+                                && method.getReturnType().equals(Void.TYPE)) {
+                            ++score;
+                            break;
+                        }
+                    }
+                    if (score == 1) {
+                        // if we could read the class, but method is not public void, it is
+                        // not a candidate
+                        continue;
+                    }
+                } catch (ClassNotFoundException e) {
+                }
+
+                // even if we cannot verify the method signature, there are signals in the stack
+
+                // usually test method is invoked by reflection
+                int depth = 1;
+                while (index + depth < stack.length
+                        && stack[index + depth].getMethodName().equals("invoke")
+                        && stack[index + depth].getClassName().equals(
+                                "java.lang.reflect.Method")) {
+                    ++depth;
+                }
+                if (depth > 1) {
+                    ++score;
+                    // and usually test method is run by runMethod method in android.test package
+                    if (index + depth < stack.length) {
+                        if (stack[index + depth].getClassName().startsWith("android.test.")) {
+                            ++score;
+                        }
+                        if (stack[index + depth].getMethodName().equals("runMethod")) {
+                            ++score;
+                        }
+                    }
+                }
+
+                if (score > bestScore) {
+                    bestScore = score;
+                    testName = methodName;
+                    if (withClass) {
+                        testName = stack[index].getClassName() + "." + testName;
+                    }
+                }
+            }
+        }
+        return testName;
+    }
+
+    /**
+     * Finds test name (heuristically) and prints out standard skip message.
+     *
+     * Since it uses heuristics, this method has only been verified for media
+     * tests. This centralizes the way to signal a skipped test.
+     */
+    public static void skipTest(String tag, String reason) {
+        Log.i(tag, "SKIPPING " + getTestName() + "(): " + reason);
+        DeviceReportLog log = new DeviceReportLog("CtsMediaSkippedTests", "test_skipped");
+        try {
+            log.addValue("reason", reason, ResultType.NEUTRAL, ResultUnit.NONE);
+            log.addValue(
+                    "test", getTestNameWithClass(), ResultType.NEUTRAL, ResultUnit.NONE);
+            log.submit();
+        } catch (NullPointerException e) { }
+    }
+
+    /**
+     * Finds test name (heuristically) and prints out standard skip message.
+     *
+     * Since it uses heuristics, this method has only been verified for media
+     * tests.  This centralizes the way to signal a skipped test.
+     */
+    public static void skipTest(String reason) {
+        skipTest(TAG, reason);
+    }
+
+    public static boolean check(boolean result, String message) {
+        if (!result) {
+            skipTest(message);
+        }
+        return result;
+    }
+
+    /*
+     *  ------------------- HELPER METHODS FOR CHECKING CODEC SUPPORT -------------------
+     */
+
+    public static boolean isGoogle(String codecName) {
+        codecName = codecName.toLowerCase();
+        return codecName.startsWith("omx.google.")
+                || codecName.startsWith("c2.android.")
+                || codecName.startsWith("c2.google.");
+    }
+
+    // returns the list of codecs that support any one of the formats
+    private static String[] getCodecNames(
+            boolean isEncoder, Boolean isGoog, MediaFormat... formats) {
+        MediaCodecList mcl = new MediaCodecList(MediaCodecList.REGULAR_CODECS);
+        ArrayList<String> result = new ArrayList<>();
+        for (MediaCodecInfo info : mcl.getCodecInfos()) {
+            if (info.isEncoder() != isEncoder) {
+                continue;
+            }
+            if (isGoog != null && isGoogle(info.getName()) != isGoog) {
+                continue;
+            }
+
+            for (MediaFormat format : formats) {
+                String mime = format.getString(MediaFormat.KEY_MIME);
+
+                CodecCapabilities caps = null;
+                try {
+                    caps = info.getCapabilitiesForType(mime);
+                } catch (IllegalArgumentException e) {  // mime is not supported
+                    continue;
+                }
+                if (caps.isFormatSupported(format)) {
+                    result.add(info.getName());
+                    break;
+                }
+            }
+        }
+        return result.toArray(new String[result.size()]);
+    }
+
+    /* Use isGoog = null to query all decoders */
+    public static String[] getDecoderNames(/* Nullable */ Boolean isGoog, MediaFormat... formats) {
+        return getCodecNames(false /* isEncoder */, isGoog, formats);
+    }
+
+    public static String[] getDecoderNames(MediaFormat... formats) {
+        return getCodecNames(false /* isEncoder */, null /* isGoog */, formats);
+    }
+
+    /* Use isGoog = null to query all decoders */
+    public static String[] getEncoderNames(/* Nullable */ Boolean isGoog, MediaFormat... formats) {
+        return getCodecNames(true /* isEncoder */, isGoog, formats);
+    }
+
+    public static String[] getEncoderNames(MediaFormat... formats) {
+        return getCodecNames(true /* isEncoder */, null /* isGoog */, formats);
+    }
+
+    public static String[] getDecoderNamesForMime(String mime) {
+        MediaFormat format = new MediaFormat();
+        format.setString(MediaFormat.KEY_MIME, mime);
+        return getCodecNames(false /* isEncoder */, null /* isGoog */, format);
+    }
+
+    public static String[] getEncoderNamesForMime(String mime) {
+        MediaFormat format = new MediaFormat();
+        format.setString(MediaFormat.KEY_MIME, mime);
+        return getCodecNames(true /* isEncoder */, null /* isGoog */, format);
+    }
+
+    public static void verifyNumCodecs(
+            int count, boolean isEncoder, Boolean isGoog, MediaFormat... formats) {
+        String desc = (isEncoder ? "encoders" : "decoders") + " for "
+                + (formats.length == 1 ? formats[0].toString() : Arrays.toString(formats));
+        if (isGoog != null) {
+            desc = (isGoog ? "Google " : "non-Google ") + desc;
+        }
+
+        String[] codecs = getCodecNames(isEncoder, isGoog, formats);
+        assertTrue("test can only verify " + count + " " + desc + "; found " + codecs.length + ": "
+                + Arrays.toString(codecs), codecs.length <= count);
+    }
+
+    public static MediaCodec getDecoder(MediaFormat format) {
+        String decoder = sMCL.findDecoderForFormat(format);
+        if (decoder != null) {
+            try {
+                return MediaCodec.createByCodecName(decoder);
+            } catch (IOException e) {
+            }
+        }
+        return null;
+    }
+
+    public static boolean canEncode(MediaFormat format) {
+        if (sMCL.findEncoderForFormat(format) == null) {
+            Log.i(TAG, "no encoder for " + format);
+            return false;
+        }
+        return true;
+    }
+
+    public static boolean canDecode(MediaFormat format) {
+        if (sMCL.findDecoderForFormat(format) == null) {
+            Log.i(TAG, "no decoder for " + format);
+            return false;
+        }
+        return true;
+    }
+
+    public static boolean supports(String codecName, String mime, int w, int h) {
+        // While this could be simply written as such, give more graceful feedback.
+        // MediaFormat format = MediaFormat.createVideoFormat(mime, w, h);
+        // return supports(codecName, format);
+
+        VideoCapabilities vidCap = getVideoCapabilities(codecName, mime);
+        if (vidCap == null) {
+            return false;
+        } else if (vidCap.isSizeSupported(w, h)) {
+            return true;
+        }
+
+        Log.w(TAG, "unsupported size " + w + "x" + h);
+        return false;
+    }
+
+    public static boolean supports(String codecName, MediaFormat format) {
+        MediaCodec codec;
+        try {
+            codec = MediaCodec.createByCodecName(codecName);
+        } catch (IOException e) {
+            Log.w(TAG, "codec not found: " + codecName);
+            return false;
+        }
+
+        String mime = format.getString(MediaFormat.KEY_MIME);
+        CodecCapabilities cap = null;
+        try {
+            cap = codec.getCodecInfo().getCapabilitiesForType(mime);
+            return cap.isFormatSupported(format);
+        } catch (IllegalArgumentException e) {
+            Log.w(TAG, "not supported mime: " + mime);
+            return false;
+        } finally {
+            codec.release();
+        }
+    }
+
+    public static boolean hasCodecForTrack(MediaExtractor ex, int track) {
+        int count = ex.getTrackCount();
+        if (track < 0 || track >= count) {
+            throw new IndexOutOfBoundsException(track + " not in [0.." + (count - 1) + "]");
+        }
+        return canDecode(ex.getTrackFormat(track));
+    }
+
+    /**
+     * return true iff all audio and video tracks are supported
+     */
+    public static boolean hasCodecsForMedia(MediaExtractor ex) {
+        for (int i = 0; i < ex.getTrackCount(); ++i) {
+            MediaFormat format = ex.getTrackFormat(i);
+            // only check for audio and video codecs
+            String mime = format.getString(MediaFormat.KEY_MIME).toLowerCase();
+            if (!mime.startsWith("audio/") && !mime.startsWith("video/")) {
+                continue;
+            }
+            if (!canDecode(format)) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    /**
+     * return true iff any track starting with mimePrefix is supported
+     */
+    public static boolean hasCodecForMediaAndDomain(MediaExtractor ex, String mimePrefix) {
+        mimePrefix = mimePrefix.toLowerCase();
+        for (int i = 0; i < ex.getTrackCount(); ++i) {
+            MediaFormat format = ex.getTrackFormat(i);
+            String mime = format.getString(MediaFormat.KEY_MIME);
+            if (mime.toLowerCase().startsWith(mimePrefix)) {
+                if (canDecode(format)) {
+                    return true;
+                }
+                Log.i(TAG, "no decoder for " + format);
+            }
+        }
+        return false;
+    }
+
+    private static boolean hasCodecsForResourceCombo(
+            Context context, int resourceId, int track, String mimePrefix) {
+        try {
+            AssetFileDescriptor afd = null;
+            MediaExtractor ex = null;
+            try {
+                afd = context.getResources().openRawResourceFd(resourceId);
+                ex = new MediaExtractor();
+                ex.setDataSource(afd.getFileDescriptor(), afd.getStartOffset(), afd.getLength());
+                if (mimePrefix != null) {
+                    return hasCodecForMediaAndDomain(ex, mimePrefix);
+                } else if (track == ALL_AV_TRACKS) {
+                    return hasCodecsForMedia(ex);
+                } else {
+                    return hasCodecForTrack(ex, track);
+                }
+            } finally {
+                if (ex != null) {
+                    ex.release();
+                }
+                if (afd != null) {
+                    afd.close();
+                }
+            }
+        } catch (IOException e) {
+            Log.i(TAG, "could not open resource");
+        }
+        return false;
+    }
+
+    /**
+     * return true iff all audio and video tracks are supported
+     */
+    public static boolean hasCodecsForResource(Context context, int resourceId) {
+        return hasCodecsForResourceCombo(context, resourceId, ALL_AV_TRACKS, null /* mimePrefix */);
+    }
+
+    public static boolean checkCodecsForResource(Context context, int resourceId) {
+        return check(hasCodecsForResource(context, resourceId), "no decoder found");
+    }
+
+    /**
+     * return true iff track is supported.
+     */
+    public static boolean hasCodecForResource(Context context, int resourceId, int track) {
+        return hasCodecsForResourceCombo(context, resourceId, track, null /* mimePrefix */);
+    }
+
+    public static boolean checkCodecForResource(Context context, int resourceId, int track) {
+        return check(hasCodecForResource(context, resourceId, track), "no decoder found");
+    }
+
+    /**
+     * return true iff any track starting with mimePrefix is supported
+     */
+    public static boolean hasCodecForResourceAndDomain(
+            Context context, int resourceId, String mimePrefix) {
+        return hasCodecsForResourceCombo(context, resourceId, ALL_AV_TRACKS, mimePrefix);
+    }
+
+    /**
+     * return true iff all audio and video tracks are supported
+     */
+    public static boolean hasCodecsForPath(Context context, String path) {
+        MediaExtractor ex = null;
+        try {
+            ex = getExtractorForPath(context, path);
+            return hasCodecsForMedia(ex);
+        } catch (IOException e) {
+            Log.i(TAG, "could not open path " + path);
+        } finally {
+            if (ex != null) {
+                ex.release();
+            }
+        }
+        return true;
+    }
+
+    private static MediaExtractor getExtractorForPath(Context context, String path)
+            throws IOException {
+        Uri uri = Uri.parse(path);
+        String scheme = uri.getScheme();
+        MediaExtractor ex = new MediaExtractor();
+        try {
+            if (scheme == null) { // file
+                ex.setDataSource(path);
+            } else if (scheme.equalsIgnoreCase("file")) {
+                ex.setDataSource(uri.getPath());
+            } else {
+                ex.setDataSource(context, uri, null);
+            }
+        } catch (IOException e) {
+            ex.release();
+            throw e;
+        }
+        return ex;
+    }
+
+    public static boolean checkCodecsForPath(Context context, String path) {
+        return check(hasCodecsForPath(context, path), "no decoder found");
+    }
+
+    public static boolean hasCodecForDomain(boolean encoder, String domain) {
+        for (MediaCodecInfo info : sMCL.getCodecInfos()) {
+            if (encoder != info.isEncoder()) {
+                continue;
+            }
+
+            for (String type : info.getSupportedTypes()) {
+                if (type.toLowerCase().startsWith(domain.toLowerCase() + "/")) {
+                    Log.i(TAG, "found codec " + info.getName() + " for mime " + type);
+                    return true;
+                }
+            }
+        }
+        return false;
+    }
+
+    public static boolean checkCodecForDomain(boolean encoder, String domain) {
+        return check(hasCodecForDomain(encoder, domain),
+                "no " + domain + (encoder ? " encoder" : " decoder") + " found");
+    }
+
+    private static boolean hasCodecForMime(boolean encoder, String mime) {
+        for (MediaCodecInfo info : sMCL.getCodecInfos()) {
+            if (encoder != info.isEncoder()) {
+                continue;
+            }
+
+            for (String type : info.getSupportedTypes()) {
+                if (type.equalsIgnoreCase(mime)) {
+                    Log.i(TAG, "found codec " + info.getName() + " for mime " + mime);
+                    return true;
+                }
+            }
+        }
+        return false;
+    }
+
+    private static boolean hasCodecForMimes(boolean encoder, String[] mimes) {
+        for (String mime : mimes) {
+            if (!hasCodecForMime(encoder, mime)) {
+                Log.i(TAG, "no " + (encoder ? "encoder" : "decoder") + " for mime " + mime);
+                return false;
+            }
+        }
+        return true;
+    }
+
+
+    public static boolean hasEncoder(String... mimes) {
+        return hasCodecForMimes(true /* encoder */, mimes);
+    }
+
+    public static boolean hasDecoder(String... mimes) {
+        return hasCodecForMimes(false /* encoder */, mimes);
+    }
+
+    public static boolean checkDecoder(String... mimes) {
+        return check(hasCodecForMimes(false /* encoder */, mimes), "no decoder found");
+    }
+
+    public static boolean checkEncoder(String... mimes) {
+        return check(hasCodecForMimes(true /* encoder */, mimes), "no encoder found");
+    }
+
+    public static boolean canDecodeVideo(String mime, int width, int height, float rate) {
+        MediaFormat format = MediaFormat.createVideoFormat(mime, width, height);
+        format.setFloat(MediaFormat.KEY_FRAME_RATE, rate);
+        return canDecode(format);
+    }
+
+    public static boolean canDecodeVideo(
+            String mime, int width, int height, float rate,
+            Integer profile, Integer level, Integer bitrate) {
+        MediaFormat format = MediaFormat.createVideoFormat(mime, width, height);
+        format.setFloat(MediaFormat.KEY_FRAME_RATE, rate);
+        if (profile != null) {
+            format.setInteger(MediaFormat.KEY_PROFILE, profile);
+            if (level != null) {
+                format.setInteger(MediaFormat.KEY_LEVEL, level);
+            }
+        }
+        if (bitrate != null) {
+            format.setInteger(MediaFormat.KEY_BIT_RATE, bitrate);
+        }
+        return canDecode(format);
+    }
+
+    public static boolean checkEncoderForFormat(MediaFormat format) {
+        return check(canEncode(format), "no encoder for " + format);
+    }
+
+    public static boolean checkDecoderForFormat(MediaFormat format) {
+        return check(canDecode(format), "no decoder for " + format);
+    }
+
+    /*
+     *  ----------------------- HELPER METHODS FOR MEDIA HANDLING -----------------------
+     */
+
+    public static VideoCapabilities getVideoCapabilities(String codecName, String mime) {
+        for (MediaCodecInfo info : sMCL.getCodecInfos()) {
+            if (!info.getName().equalsIgnoreCase(codecName)) {
+                continue;
+            }
+            CodecCapabilities caps;
+            try {
+                caps = info.getCapabilitiesForType(mime);
+            } catch (IllegalArgumentException e) {
+                // mime is not supported
+                Log.w(TAG, "not supported mime: " + mime);
+                return null;
+            }
+            VideoCapabilities vidCaps = caps.getVideoCapabilities();
+            if (vidCaps == null) {
+                Log.w(TAG, "not a video codec: " + codecName);
+            }
+            return vidCaps;
+        }
+        Log.w(TAG, "codec not found: " + codecName);
+        return null;
+    }
+
+    public static MediaFormat getTrackFormatForResource(
+            Context context,
+            int resourceId,
+            String mimeTypePrefix) throws IOException {
+        MediaExtractor extractor = new MediaExtractor();
+        AssetFileDescriptor afd = context.getResources().openRawResourceFd(resourceId);
+        try {
+            extractor.setDataSource(afd.getFileDescriptor(), afd.getStartOffset(), afd.getLength());
+        } finally {
+            afd.close();
+        }
+        return getTrackFormatForExtractor(extractor, mimeTypePrefix);
+    }
+
+    public static MediaFormat getTrackFormatForPath(
+            Context context, String path, String mimeTypePrefix)
+            throws IOException {
+      MediaExtractor extractor = getExtractorForPath(context, path);
+      return getTrackFormatForExtractor(extractor, mimeTypePrefix);
+    }
+
+    private static MediaFormat getTrackFormatForExtractor(
+            MediaExtractor extractor,
+            String mimeTypePrefix) {
+      int trackIndex;
+      MediaFormat format = null;
+      for (trackIndex = 0; trackIndex < extractor.getTrackCount(); trackIndex++) {
+          MediaFormat trackMediaFormat = extractor.getTrackFormat(trackIndex);
+          if (trackMediaFormat.getString(MediaFormat.KEY_MIME).startsWith(mimeTypePrefix)) {
+              format = trackMediaFormat;
+              break;
+          }
+      }
+      extractor.release();
+      if (format == null) {
+          throw new RuntimeException("couldn't get a track for " + mimeTypePrefix);
+      }
+
+      return format;
+    }
+
+    public static MediaExtractor createMediaExtractorForMimeType(
+            Context context, int resourceId, String mimeTypePrefix)
+            throws IOException {
+        MediaExtractor extractor = new MediaExtractor();
+        AssetFileDescriptor afd = context.getResources().openRawResourceFd(resourceId);
+        try {
+            extractor.setDataSource(
+                    afd.getFileDescriptor(), afd.getStartOffset(), afd.getLength());
+        } finally {
+            afd.close();
+        }
+        int trackIndex;
+        for (trackIndex = 0; trackIndex < extractor.getTrackCount(); trackIndex++) {
+            MediaFormat trackMediaFormat = extractor.getTrackFormat(trackIndex);
+            if (trackMediaFormat.getString(MediaFormat.KEY_MIME).startsWith(mimeTypePrefix)) {
+                extractor.selectTrack(trackIndex);
+                break;
+            }
+        }
+        if (trackIndex == extractor.getTrackCount()) {
+            extractor.release();
+            throw new IllegalStateException("couldn't get a track for " + mimeTypePrefix);
+        }
+
+        return extractor;
+    }
+
+    /*
+     *  ---------------------- HELPER METHODS FOR CODEC CONFIGURATION
+     */
+
+    /** Format must contain mime, width and height.
+     *  Throws Exception if encoder does not support this width and height */
+    public static void setMaxEncoderFrameAndBitrates(
+            MediaCodec encoder, MediaFormat format, int maxFps) {
+        String mime = format.getString(MediaFormat.KEY_MIME);
+
+        VideoCapabilities vidCaps =
+            encoder.getCodecInfo().getCapabilitiesForType(mime).getVideoCapabilities();
+        setMaxEncoderFrameAndBitrates(vidCaps, format, maxFps);
+    }
+
+    public static void setMaxEncoderFrameAndBitrates(
+            VideoCapabilities vidCaps, MediaFormat format, int maxFps) {
+        int width = format.getInteger(MediaFormat.KEY_WIDTH);
+        int height = format.getInteger(MediaFormat.KEY_HEIGHT);
+
+        int maxWidth = vidCaps.getSupportedWidths().getUpper();
+        int maxHeight = vidCaps.getSupportedHeightsFor(maxWidth).getUpper();
+        int frameRate = Math.min(
+                maxFps, vidCaps.getSupportedFrameRatesFor(width, height).getUpper().intValue());
+        format.setInteger(MediaFormat.KEY_FRAME_RATE, frameRate);
+
+        int bitrate = vidCaps.getBitrateRange().clamp(
+            (int)(vidCaps.getBitrateRange().getUpper() /
+                  Math.sqrt((double)maxWidth * maxHeight / width / height)));
+        format.setInteger(MediaFormat.KEY_BIT_RATE, bitrate);
+    }
+
+    /*
+     *  ------------------ HELPER METHODS FOR STATISTICS AND REPORTING ------------------
+     */
+
+    // TODO: migrate this into com.android.compatibility.common.util.Stat
+    public static class Stats {
+        /** does not support NaN or Inf in |data| */
+        public Stats(double[] data) {
+            mData = data;
+            if (mData != null) {
+                mNum = mData.length;
+            }
+        }
+
+        public int getNum() {
+            return mNum;
+        }
+
+        /** calculate mSumX and mSumXX */
+        private void analyze() {
+            if (mAnalyzed) {
+                return;
+            }
+
+            if (mData != null) {
+                for (double x : mData) {
+                    if (!(x >= mMinX)) { // mMinX may be NaN
+                        mMinX = x;
+                    }
+                    if (!(x <= mMaxX)) { // mMaxX may be NaN
+                        mMaxX = x;
+                    }
+                    mSumX += x;
+                    mSumXX += x * x;
+                }
+            }
+            mAnalyzed = true;
+        }
+
+        /** returns the maximum or NaN if it does not exist */
+        public double getMin() {
+            analyze();
+            return mMinX;
+        }
+
+        /** returns the minimum or NaN if it does not exist */
+        public double getMax() {
+            analyze();
+            return mMaxX;
+        }
+
+        /** returns the average or NaN if it does not exist. */
+        public double getAverage() {
+            analyze();
+            if (mNum == 0) {
+                return Double.NaN;
+            } else {
+                return mSumX / mNum;
+            }
+        }
+
+        /** returns the standard deviation or NaN if it does not exist. */
+        public double getStdev() {
+            analyze();
+            if (mNum == 0) {
+                return Double.NaN;
+            } else {
+                double average = mSumX / mNum;
+                return Math.sqrt(mSumXX / mNum - average * average);
+            }
+        }
+
+        /** returns the statistics for the moving average over n values */
+        public Stats movingAverage(int n) {
+            if (n < 1 || mNum < n) {
+                return new Stats(null);
+            } else if (n == 1) {
+                return this;
+            }
+
+            double[] avgs = new double[mNum - n + 1];
+            double sum = 0;
+            for (int i = 0; i < mNum; ++i) {
+                sum += mData[i];
+                if (i >= n - 1) {
+                    avgs[i - n + 1] = sum / n;
+                    sum -= mData[i - n + 1];
+                }
+            }
+            return new Stats(avgs);
+        }
+
+        /** returns the statistics for the moving average over a window over the
+         *  cumulative sum. Basically, moves a window from: [0, window] to
+         *  [sum - window, sum] over the cumulative sum, over ((sum - window) / average)
+         *  steps, and returns the average value over each window.
+         *  This method is used to average time-diff data over a window of a constant time.
+         */
+        public Stats movingAverageOverSum(double window) {
+            if (window <= 0 || mNum < 1) {
+                return new Stats(null);
+            }
+
+            analyze();
+            double average = mSumX / mNum;
+            if (window >= mSumX) {
+                return new Stats(new double[] { average });
+            }
+            int samples = (int)Math.ceil((mSumX - window) / average);
+            double[] avgs = new double[samples];
+
+            // A somewhat brute force approach to calculating the moving average.
+            // TODO: add support for weights in Stats, so we can do a more refined approach.
+            double sum = 0; // sum of elements in the window
+            int num = 0; // number of elements in the moving window
+            int bi = 0; // index of the first element in the moving window
+            int ei = 0; // index of the last element in the moving window
+            double space = window; // space at the end of the window
+            double foot = 0; // space at the beginning of the window
+
+            // invariants: foot + sum + space == window
+            //             bi + num == ei
+            //
+            //  window:             |-------------------------------|
+            //                      |    <-----sum------>           |
+            //                      <foot>               <---space-->
+            //                           |               |
+            //  intervals:   |-----------|-------|-------|--------------------|--------|
+            //                           ^bi             ^ei
+
+            int ix = 0; // index in the result
+            while (ix < samples) {
+                // add intervals while there is space in the window
+                while (ei < mData.length && mData[ei] <= space) {
+                    space -= mData[ei];
+                    sum += mData[ei];
+                    num++;
+                    ei++;
+                }
+
+                // calculate average over window and deal with odds and ends (e.g. if there are no
+                // intervals in the current window: pick whichever element overlaps the window
+                // most.
+                if (num > 0) {
+                    avgs[ix++] = sum / num;
+                } else if (bi > 0 && foot > space) {
+                    // consider previous
+                    avgs[ix++] = mData[bi - 1];
+                } else if (ei == mData.length) {
+                    break;
+                } else {
+                    avgs[ix++] = mData[ei];
+                }
+
+                // move the window to the next position
+                foot -= average;
+                space += average;
+
+                // remove intervals that are now partially or wholly outside of the window
+                while (bi < ei && foot < 0) {
+                    foot += mData[bi];
+                    sum -= mData[bi];
+                    num--;
+                    bi++;
+                }
+            }
+            return new Stats(Arrays.copyOf(avgs, ix));
+        }
+
+        /** calculate mSortedData */
+        private void sort() {
+            if (mSorted || mNum == 0) {
+                return;
+            }
+            mSortedData = Arrays.copyOf(mData, mNum);
+            Arrays.sort(mSortedData);
+            mSorted = true;
+        }
+
+        /** returns an array of percentiles for the points using nearest rank */
+        public double[] getPercentiles(double... points) {
+            sort();
+            double[] res = new double[points.length];
+            for (int i = 0; i < points.length; ++i) {
+                if (mNum < 1 || points[i] < 0 || points[i] > 100) {
+                    res[i] = Double.NaN;
+                } else {
+                    res[i] = mSortedData[(int)Math.round(points[i] / 100 * (mNum - 1))];
+                }
+            }
+            return res;
+        }
+
+        @Override
+        public boolean equals(Object o) {
+            if (o instanceof Stats) {
+                Stats other = (Stats)o;
+                if (other.mNum != mNum) {
+                    return false;
+                } else if (mNum == 0) {
+                    return true;
+                }
+                return Arrays.equals(mData, other.mData);
+            }
+            return false;
+        }
+
+        private double[] mData;
+        private double mSumX = 0;
+        private double mSumXX = 0;
+        private double mMinX = Double.NaN;
+        private double mMaxX = Double.NaN;
+        private int mNum = 0;
+        private boolean mAnalyzed = false;
+        private double[] mSortedData;
+        private boolean mSorted = false;
+    }
+
+    /**
+     * Convert a forward lock .dm message stream to a .fl file
+     * @param context Context to use
+     * @param dmStream The .dm message
+     * @param flFile The output file to be written
+     * @return success
+     */
+    public static boolean convertDmToFl(
+            Context context,
+            InputStream dmStream,
+            RandomAccessFile flFile) {
+        final String MIMETYPE_DRM_MESSAGE = "application/vnd.oma.drm.message";
+        byte[] dmData = new byte[10000];
+        int totalRead = 0;
+        int numRead;
+        while (true) {
+            try {
+                numRead = dmStream.read(dmData, totalRead, dmData.length - totalRead);
+            } catch (IOException e) {
+                Log.w(TAG, "Failed to read from input file");
+                return false;
+            }
+            if (numRead == -1) {
+                break;
+            }
+            totalRead += numRead;
+            if (totalRead == dmData.length) {
+                // grow array
+                dmData = Arrays.copyOf(dmData, dmData.length + 10000);
+            }
+        }
+        byte[] fileData = Arrays.copyOf(dmData, totalRead);
+
+        DrmManagerClient drmClient = null;
+        try {
+            drmClient = new DrmManagerClient(context);
+        } catch (IllegalArgumentException e) {
+            Log.w(TAG, "DrmManagerClient instance could not be created, context is Illegal.");
+            return false;
+        } catch (IllegalStateException e) {
+            Log.w(TAG, "DrmManagerClient didn't initialize properly.");
+            return false;
+        }
+
+        try {
+            int convertSessionId = -1;
+            try {
+                convertSessionId = drmClient.openConvertSession(MIMETYPE_DRM_MESSAGE);
+            } catch (IllegalArgumentException e) {
+                Log.w(TAG, "Conversion of Mimetype: " + MIMETYPE_DRM_MESSAGE
+                        + " is not supported.", e);
+                return false;
+            } catch (IllegalStateException e) {
+                Log.w(TAG, "Could not access Open DrmFramework.", e);
+                return false;
+            }
+
+            if (convertSessionId < 0) {
+                Log.w(TAG, "Failed to open session.");
+                return false;
+            }
+
+            DrmConvertedStatus convertedStatus = null;
+            try {
+                convertedStatus = drmClient.convertData(convertSessionId, fileData);
+            } catch (IllegalArgumentException e) {
+                Log.w(TAG, "Buffer with data to convert is illegal. Convertsession: "
+                        + convertSessionId, e);
+                return false;
+            } catch (IllegalStateException e) {
+                Log.w(TAG, "Could not convert data. Convertsession: " + convertSessionId, e);
+                return false;
+            }
+
+            if (convertedStatus == null ||
+                    convertedStatus.statusCode != DrmConvertedStatus.STATUS_OK ||
+                    convertedStatus.convertedData == null) {
+                Log.w(TAG, "Error in converting data. Convertsession: " + convertSessionId);
+                try {
+                    DrmConvertedStatus result = drmClient.closeConvertSession(convertSessionId);
+                    if (result.statusCode != DrmConvertedStatus.STATUS_OK) {
+                        Log.w(TAG, "Conversion failed with status: " + result.statusCode);
+                        return false;
+                    }
+                } catch (IllegalStateException e) {
+                    Log.w(TAG, "Could not close session. Convertsession: " +
+                           convertSessionId, e);
+                }
+                return false;
+            }
+
+            try {
+                flFile.write(convertedStatus.convertedData, 0, convertedStatus.convertedData.length);
+            } catch (IOException e) {
+                Log.w(TAG, "Failed to write to output file: " + e);
+                return false;
+            }
+
+            try {
+                convertedStatus = drmClient.closeConvertSession(convertSessionId);
+            } catch (IllegalStateException e) {
+                Log.w(TAG, "Could not close convertsession. Convertsession: " +
+                        convertSessionId, e);
+                return false;
+            }
+
+            if (convertedStatus == null ||
+                    convertedStatus.statusCode != DrmConvertedStatus.STATUS_OK ||
+                    convertedStatus.convertedData == null) {
+                Log.w(TAG, "Error in closing session. Convertsession: " + convertSessionId);
+                return false;
+            }
+
+            try {
+                flFile.seek(convertedStatus.offset);
+                flFile.write(convertedStatus.convertedData);
+            } catch (IOException e) {
+                Log.w(TAG, "Could not update file.", e);
+                return false;
+            }
+
+            return true;
+        } finally {
+            drmClient.close();
+        }
+    }
+
+    /**
+     * @param decoder new MediaCodec object
+     * @param ex MediaExtractor after setDataSource and selectTrack
+     * @param frameMD5Sums reference MD5 checksum for decoded frames
+     * @return true if decoded frames checksums matches reference checksums
+     * @throws IOException
+     */
+    public static boolean verifyDecoder(
+            MediaCodec decoder, MediaExtractor ex, List<String> frameMD5Sums)
+            throws IOException {
+
+        int trackIndex = ex.getSampleTrackIndex();
+        MediaFormat format = ex.getTrackFormat(trackIndex);
+        decoder.configure(format, null /* surface */, null /* crypto */, 0 /* flags */);
+        decoder.start();
+
+        boolean sawInputEOS = false;
+        boolean sawOutputEOS = false;
+        final long kTimeOutUs = 5000; // 5ms timeout
+        int decodedFrameCount = 0;
+        int expectedFrameCount = frameMD5Sums.size();
+        MediaCodec.BufferInfo info = new MediaCodec.BufferInfo();
+
+        while (!sawOutputEOS) {
+            // handle input
+            if (!sawInputEOS) {
+                int inIdx = decoder.dequeueInputBuffer(kTimeOutUs);
+                if (inIdx >= 0) {
+                    ByteBuffer buffer = decoder.getInputBuffer(inIdx);
+                    int sampleSize = ex.readSampleData(buffer, 0);
+                    if (sampleSize < 0) {
+                        final int flagEOS = MediaCodec.BUFFER_FLAG_END_OF_STREAM;
+                        decoder.queueInputBuffer(inIdx, 0, 0, 0, flagEOS);
+                        sawInputEOS = true;
+                    } else {
+                        decoder.queueInputBuffer(inIdx, 0, sampleSize, ex.getSampleTime(), 0);
+                        ex.advance();
+                    }
+                }
+            }
+
+            // handle output
+            int outputBufIndex = decoder.dequeueOutputBuffer(info, kTimeOutUs);
+            if (outputBufIndex >= 0) {
+                try {
+                    if (info.size > 0) {
+                        // Disregard 0-sized buffers at the end.
+                        String md5CheckSum = "";
+                        Image image = decoder.getOutputImage(outputBufIndex);
+                        md5CheckSum = getImageMD5Checksum(image);
+
+                        if (!md5CheckSum.equals(frameMD5Sums.get(decodedFrameCount))) {
+                            Log.d(TAG,
+                                    String.format(
+                                            "Frame %d md5sum mismatch: %s(actual) vs %s(expected)",
+                                            decodedFrameCount, md5CheckSum,
+                                            frameMD5Sums.get(decodedFrameCount)));
+                            return false;
+                        }
+
+                        decodedFrameCount++;
+                    }
+                } catch (Exception e) {
+                    Log.e(TAG, "getOutputImage md5CheckSum failed", e);
+                    return false;
+                } finally {
+                    decoder.releaseOutputBuffer(outputBufIndex, false /* render */);
+                }
+                if ((info.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
+                    sawOutputEOS = true;
+                }
+            } else if (outputBufIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
+                MediaFormat decOutputFormat = decoder.getOutputFormat();
+                Log.d(TAG, "output format " + decOutputFormat);
+            } else if (outputBufIndex == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) {
+                Log.i(TAG, "Skip handling MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED");
+            } else if (outputBufIndex == MediaCodec.INFO_TRY_AGAIN_LATER) {
+                continue;
+            } else {
+                Log.w(TAG, "decoder.dequeueOutputBuffer() unrecognized index: " + outputBufIndex);
+                return false;
+            }
+        }
+
+        if (decodedFrameCount != expectedFrameCount) {
+            return false;
+        }
+
+        return true;
+    }
+
+    public static String getImageMD5Checksum(Image image) throws Exception {
+        int format = image.getFormat();
+        if (ImageFormat.YUV_420_888 != format) {
+            Log.w(TAG, "unsupported image format");
+            return "";
+        }
+
+        MessageDigest md = MessageDigest.getInstance("MD5");
+
+        int imageWidth = image.getWidth();
+        int imageHeight = image.getHeight();
+
+        Image.Plane[] planes = image.getPlanes();
+        for (int i = 0; i < planes.length; ++i) {
+            ByteBuffer buf = planes[i].getBuffer();
+
+            int width, height, rowStride, pixelStride, x, y;
+            rowStride = planes[i].getRowStride();
+            pixelStride = planes[i].getPixelStride();
+            if (i == 0) {
+                width = imageWidth;
+                height = imageHeight;
+            } else {
+                width = imageWidth / 2;
+                height = imageHeight /2;
+            }
+            // local contiguous pixel buffer
+            byte[] bb = new byte[width * height];
+            if (buf.hasArray()) {
+                byte b[] = buf.array();
+                int offs = buf.arrayOffset();
+                if (pixelStride == 1) {
+                    for (y = 0; y < height; ++y) {
+                        System.arraycopy(bb, y * width, b, y * rowStride + offs, width);
+                    }
+                } else {
+                    // do it pixel-by-pixel
+                    for (y = 0; y < height; ++y) {
+                        int lineOffset = offs + y * rowStride;
+                        for (x = 0; x < width; ++x) {
+                            bb[y * width + x] = b[lineOffset + x * pixelStride];
+                        }
+                    }
+                }
+            } else { // almost always ends up here due to direct buffers
+                int pos = buf.position();
+                if (pixelStride == 1) {
+                    for (y = 0; y < height; ++y) {
+                        buf.position(pos + y * rowStride);
+                        buf.get(bb, y * width, width);
+                    }
+                } else {
+                    // local line buffer
+                    byte[] lb = new byte[rowStride];
+                    // do it pixel-by-pixel
+                    for (y = 0; y < height; ++y) {
+                        buf.position(pos + y * rowStride);
+                        // we're only guaranteed to have pixelStride * (width - 1) + 1 bytes
+                        buf.get(lb, 0, pixelStride * (width - 1) + 1);
+                        for (x = 0; x < width; ++x) {
+                            bb[y * width + x] = lb[x * pixelStride];
+                        }
+                    }
+                }
+                buf.position(pos);
+            }
+            md.update(bb, 0, width * height);
+        }
+
+        return convertByteArrayToHEXString(md.digest());
+    }
+
+    private static String convertByteArrayToHEXString(byte[] ba) throws Exception {
+        StringBuilder result = new StringBuilder();
+        for (int i = 0; i < ba.length; i++) {
+            result.append(Integer.toString((ba[i] & 0xff) + 0x100, 16).substring(1));
+        }
+        return result.toString();
+    }
+
+
+    /*
+     *  -------------------------------------- END --------------------------------------
+     */
+}
diff --git a/common/device-side/util-axt/src/com/android/compatibility/common/util/MoreMatchers.java b/common/device-side/util-axt/src/com/android/compatibility/common/util/MoreMatchers.java
new file mode 100644
index 0000000..cee610e
--- /dev/null
+++ b/common/device-side/util-axt/src/com/android/compatibility/common/util/MoreMatchers.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.compatibility.common.util;
+
+import org.mockito.ArgumentMatchers;
+
+public class MoreMatchers {
+    private MoreMatchers() {
+    }
+
+    public static <T> T anyOrNull(Class<T> clazz) {
+        return ArgumentMatchers.argThat(value -> true);
+    }
+
+    public static String anyStringOrNull() {
+        return ArgumentMatchers.argThat(value -> true);
+    }
+}
diff --git a/common/device-side/util-axt/src/com/android/compatibility/common/util/NullWebViewUtils.java b/common/device-side/util-axt/src/com/android/compatibility/common/util/NullWebViewUtils.java
new file mode 100644
index 0000000..3153adb
--- /dev/null
+++ b/common/device-side/util-axt/src/com/android/compatibility/common/util/NullWebViewUtils.java
@@ -0,0 +1,88 @@
+/*
+ * Copyright (C) 2010 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.compatibility.common.util;
+
+import android.content.Context;
+import android.content.pm.PackageManager;
+
+/**
+ * Utilities to enable the android.webkit.* CTS tests (and others that rely on a functioning
+ * android.webkit.WebView implementation) to determine whether a functioning WebView is present
+ * on the device or not.
+ *
+ * Test cases that require android.webkit.* classes should wrap their first usage of WebView in a
+ * try catch block, and pass any exception that is thrown to
+ * NullWebViewUtils.determineIfWebViewAvailable. The return value of
+ * NullWebViewUtils.isWebViewAvailable will then determine if the test should expect to be able to
+ * use a WebView.
+ */
+public class NullWebViewUtils {
+
+    private static boolean sWebViewUnavailable;
+
+    /**
+     * @param context Current Activity context, used to query the PackageManager.
+     * @param t       An exception thrown by trying to invoke android.webkit.* APIs.
+     */
+    public static void determineIfWebViewAvailable(Context context, Throwable t) {
+        sWebViewUnavailable = !hasWebViewFeature(context) && checkCauseWasUnsupportedOperation(t);
+    }
+
+    /**
+     * After calling determineIfWebViewAvailable, this returns whether a WebView is available on the
+     * device and wheter the test can rely on it.
+     * @return True iff. PackageManager determined that there is no WebView on the device and the
+     *         exception thrown from android.webkit.* was UnsupportedOperationException.
+     */
+    public static boolean isWebViewAvailable() {
+        return !sWebViewUnavailable;
+    }
+
+    private static boolean hasWebViewFeature(Context context) {
+        // Query the system property that determins if there is a functional WebView on the device.
+        PackageManager pm = context.getPackageManager();
+        return pm.hasSystemFeature(PackageManager.FEATURE_WEBVIEW);
+    }
+
+    private static boolean checkCauseWasUnsupportedOperation(Throwable t) {
+        if (t == null) return false;
+        while (t.getCause() != null) {
+            t = t.getCause();
+        }
+        return t instanceof UnsupportedOperationException;
+    }
+
+    /**
+     * Some CTS tests (by design) first use android.webkit.* from a background thread. This helper
+     * allows the test to catch the UnsupportedOperationException from that background thread, and
+     * then query the result from the test main thread.
+     */
+    public static class NullWebViewFromThreadExceptionHandler
+            implements Thread.UncaughtExceptionHandler {
+        private Throwable mPendingException;
+
+        @Override
+        public void uncaughtException(Thread t, Throwable e) {
+            mPendingException = e;
+        }
+
+        public boolean isWebViewAvailable(Context context) {
+            return hasWebViewFeature(context) ||
+                    !checkCauseWasUnsupportedOperation(mPendingException);
+        }
+    }
+}
diff --git a/common/device-side/util-axt/src/com/android/compatibility/common/util/OnFailureRule.java b/common/device-side/util-axt/src/com/android/compatibility/common/util/OnFailureRule.java
new file mode 100644
index 0000000..585c145
--- /dev/null
+++ b/common/device-side/util-axt/src/com/android/compatibility/common/util/OnFailureRule.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.compatibility.common.util;
+
+import android.util.Log;
+
+import org.junit.rules.TestRule;
+import org.junit.runner.Description;
+import org.junit.runners.model.Statement;
+
+/**
+ * Custom JUnit4 rule that provides a callback upon test failures.
+ */
+public abstract class OnFailureRule implements TestRule {
+    private String mLogTag = "OnFailureRule";
+
+    public OnFailureRule() {
+    }
+
+    public OnFailureRule(String logTag) {
+        mLogTag = logTag;
+    }
+
+    @Override
+    public Statement apply(Statement base, Description description) {
+        return new Statement() {
+
+            @Override
+            public void evaluate() throws Throwable {
+                try {
+                    base.evaluate();
+                } catch (Throwable t) {
+                    Log.e(mLogTag, "Test failed: description=" +  description + "\nThrowable=" + t);
+                    onTestFailure(base, description, t);
+                    throw t;
+                }
+            }
+        };
+    }
+
+    protected abstract void onTestFailure(Statement base, Description description, Throwable t);
+}
\ No newline at end of file
diff --git a/common/device-side/util-axt/src/com/android/compatibility/common/util/PackageUtil.java b/common/device-side/util-axt/src/com/android/compatibility/common/util/PackageUtil.java
new file mode 100644
index 0000000..0dd9e82
--- /dev/null
+++ b/common/device-side/util-axt/src/com/android/compatibility/common/util/PackageUtil.java
@@ -0,0 +1,134 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.compatibility.common.util;
+
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.os.Build;
+import androidx.test.InstrumentationRegistry;
+import android.util.Log;
+
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+
+/**
+ * Device-side utility class for PackageManager-related operations
+ */
+public class PackageUtil {
+
+    private static final String TAG = PackageUtil.class.getSimpleName();
+
+    private static final int SYSTEM_APP_MASK =
+            ApplicationInfo.FLAG_SYSTEM | ApplicationInfo.FLAG_UPDATED_SYSTEM_APP;
+    private static final char[] HEX_ARRAY = "0123456789ABCDEF".toCharArray();
+
+    /** Returns true if a package with the given name exists on the device */
+    public static boolean exists(String packageName) {
+        try {
+            return (getPackageManager().getApplicationInfo(packageName,
+                    PackageManager.GET_META_DATA) != null);
+        } catch(PackageManager.NameNotFoundException e) {
+            return false;
+        }
+    }
+
+    /** Returns true if a package with the given name AND SHA digest exists on the device */
+    public static boolean exists(String packageName, String sha) {
+        try {
+            if (getPackageManager().getApplicationInfo(
+                    packageName, PackageManager.GET_META_DATA) == null) {
+                return false;
+            }
+            return sha.equals(computePackageSignatureDigest(packageName));
+        } catch (NoSuchAlgorithmException | PackageManager.NameNotFoundException e) {
+            return false;
+        }
+    }
+
+    /** Returns true if the app for the given package name is a system app for this device */
+    public static boolean isSystemApp(String packageName) {
+        try {
+            ApplicationInfo ai = getPackageManager().getApplicationInfo(packageName,
+                    PackageManager.GET_META_DATA);
+            return ai != null && ((ai.flags & SYSTEM_APP_MASK) != 0);
+        } catch(PackageManager.NameNotFoundException e) {
+            return false;
+        }
+    }
+
+    /** Returns the version string of the package name, or null if the package can't be found */
+    public static String getVersionString(String packageName) {
+        try {
+            PackageInfo info = getPackageManager().getPackageInfo(packageName,
+                    PackageManager.GET_META_DATA);
+            return info.versionName;
+        } catch (PackageManager.NameNotFoundException | NullPointerException e) {
+            Log.w(TAG, "Could not find version string for package " + packageName);
+            return null;
+        }
+    }
+
+    /**
+     * Compute the signature SHA digest for a package.
+     * @param package the name of the package for which the signature SHA digest is requested
+     * @return the signature SHA digest
+     */
+    public static String computePackageSignatureDigest(String packageName)
+            throws NoSuchAlgorithmException, PackageManager.NameNotFoundException {
+        PackageInfo packageInfo = getPackageManager()
+                .getPackageInfo(packageName, PackageManager.GET_SIGNATURES);
+        MessageDigest messageDigest = MessageDigest.getInstance("SHA256");
+        messageDigest.update(packageInfo.signatures[0].toByteArray());
+
+        final byte[] digest = messageDigest.digest();
+        final int digestLength = digest.length;
+        final int charCount = 3 * digestLength - 1;
+
+        final char[] chars = new char[charCount];
+        for (int i = 0; i < digestLength; i++) {
+            final int byteHex = digest[i] & 0xFF;
+            chars[i * 3] = HEX_ARRAY[byteHex >>> 4];
+            chars[i * 3 + 1] = HEX_ARRAY[byteHex & 0x0F];
+            if (i < digestLength - 1) {
+                chars[i * 3 + 2] = ':';
+            }
+        }
+        return new String(chars);
+    }
+
+    private static PackageManager getPackageManager() {
+        return InstrumentationRegistry.getInstrumentation().getTargetContext().getPackageManager();
+    }
+
+    private static boolean hasDeviceFeature(final String requiredFeature) {
+        return InstrumentationRegistry.getContext()
+                .getPackageManager()
+                .hasSystemFeature(requiredFeature);
+    }
+
+    /**
+     * Rotation support is indicated by explicitly having both landscape and portrait
+     * features or not listing either at all.
+     */
+    public static boolean supportsRotation() {
+        final boolean supportsLandscape = hasDeviceFeature(PackageManager.FEATURE_SCREEN_LANDSCAPE);
+        final boolean supportsPortrait = hasDeviceFeature(PackageManager.FEATURE_SCREEN_PORTRAIT);
+        return (supportsLandscape && supportsPortrait)
+                || (!supportsLandscape && !supportsPortrait);
+    }
+}
diff --git a/common/device-side/util-axt/src/com/android/compatibility/common/util/ParcelUtils.java b/common/device-side/util-axt/src/com/android/compatibility/common/util/ParcelUtils.java
new file mode 100644
index 0000000..ecaa722
--- /dev/null
+++ b/common/device-side/util-axt/src/com/android/compatibility/common/util/ParcelUtils.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.compatibility.common.util;
+
+import static org.junit.Assert.assertNotNull;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+
+public class ParcelUtils {
+    private ParcelUtils() {
+    }
+
+    /** Convert a Parcelable into a byte[]. */
+    public static byte[] toBytes(Parcelable p) {
+        assertNotNull(p);
+
+        final Parcel parcel = Parcel.obtain();
+        parcel.writeParcelable(p, 0);
+        byte[] data = parcel.marshall();
+        parcel.recycle();
+
+        return data;
+    }
+
+    /** Decode a byte[] into a Parcelable. */
+    public static <T extends Parcelable> T fromBytes(byte[] data) {
+        assertNotNull(data);
+
+        final Parcel parcel = Parcel.obtain();
+        parcel.unmarshall(data, 0, data.length);
+        parcel.setDataPosition(0);
+        T ret = parcel.readParcelable(ParcelUtils.class.getClassLoader());
+        parcel.recycle();
+
+        return ret;
+    }
+}
diff --git a/common/device-side/util-axt/src/com/android/compatibility/common/util/PollingCheck.java b/common/device-side/util-axt/src/com/android/compatibility/common/util/PollingCheck.java
new file mode 100644
index 0000000..bcc3530
--- /dev/null
+++ b/common/device-side/util-axt/src/com/android/compatibility/common/util/PollingCheck.java
@@ -0,0 +1,94 @@
+/*
+ * Copyright (C) 2012 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.compatibility.common.util;
+
+import java.util.concurrent.Callable;
+
+import junit.framework.Assert;
+
+public abstract class PollingCheck {
+    private static final long TIME_SLICE = 50;
+    private long mTimeout = 3000;
+
+    public static interface PollingCheckCondition {
+        boolean canProceed();
+    }
+
+    public PollingCheck() {
+    }
+
+    public PollingCheck(long timeout) {
+        mTimeout = timeout;
+    }
+
+    protected abstract boolean check();
+
+    public void run() {
+        if (check()) {
+            return;
+        }
+
+        long timeout = mTimeout;
+        while (timeout > 0) {
+            try {
+                Thread.sleep(TIME_SLICE);
+            } catch (InterruptedException e) {
+                Assert.fail("unexpected InterruptedException");
+            }
+
+            if (check()) {
+                return;
+            }
+
+            timeout -= TIME_SLICE;
+        }
+
+        Assert.fail("unexpected timeout");
+    }
+
+    public static void check(CharSequence message, long timeout, Callable<Boolean> condition)
+            throws Exception {
+        while (timeout > 0) {
+            if (condition.call()) {
+                return;
+            }
+
+            Thread.sleep(TIME_SLICE);
+            timeout -= TIME_SLICE;
+        }
+
+        Assert.fail(message.toString());
+    }
+
+    public static void waitFor(final PollingCheckCondition condition) {
+        new PollingCheck() {
+            @Override
+            protected boolean check() {
+                return condition.canProceed();
+            }
+        }.run();
+    }
+
+    public static void waitFor(long timeout, final PollingCheckCondition condition) {
+        new PollingCheck(timeout) {
+            @Override
+            protected boolean check() {
+                return condition.canProceed();
+            }
+        }.run();
+    }
+}
diff --git a/common/device-side/util-axt/src/com/android/compatibility/common/util/PropertyUtil.java b/common/device-side/util-axt/src/com/android/compatibility/common/util/PropertyUtil.java
new file mode 100644
index 0000000..285b732
--- /dev/null
+++ b/common/device-side/util-axt/src/com/android/compatibility/common/util/PropertyUtil.java
@@ -0,0 +1,179 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.compatibility.common.util;
+
+import android.os.Build;
+import androidx.test.InstrumentationRegistry;
+
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Scanner;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Device-side utility class for reading properties and gathering information for testing
+ * Android device compatibility.
+ */
+public class PropertyUtil {
+
+    /**
+     * Name of read-only property detailing the first API level for which the product was
+     * shipped. Property should be undefined for factory ROM products.
+     */
+    public static final String FIRST_API_LEVEL = "ro.product.first_api_level";
+    private static final String BUILD_TYPE_PROPERTY = "ro.build.type";
+    private static final String MANUFACTURER_PROPERTY = "ro.product.manufacturer";
+    private static final String TAG_DEV_KEYS = "dev-keys";
+    private static final String VNDK_VERSION = "ro.vndk.version";
+
+    public static final String GOOGLE_SETTINGS_QUERY =
+            "content query --uri content://com.google.settings/partner";
+
+    /** Value to be returned by getPropertyInt() if property is not found */
+    public static int INT_VALUE_IF_UNSET = -1;
+
+    /** Returns whether the device build is a user build */
+    public static boolean isUserBuild() {
+        return propertyEquals(BUILD_TYPE_PROPERTY, "user");
+    }
+
+    /** Returns whether the device build is the factory ROM */
+    public static boolean isFactoryROM() {
+        // property should be undefined if and only if the product is factory ROM.
+        return getPropertyInt(FIRST_API_LEVEL) == INT_VALUE_IF_UNSET;
+    }
+
+    /** Returns whether this build is built with dev-keys */
+    public static boolean isDevKeysBuild() {
+        for (String tag : Build.TAGS.split(",")) {
+            if (TAG_DEV_KEYS.equals(tag.trim())) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Return the first API level for this product. If the read-only property is unset,
+     * this means the first API level is the current API level, and the current API level
+     * is returned.
+     */
+    public static int getFirstApiLevel() {
+        int firstApiLevel = getPropertyInt(FIRST_API_LEVEL);
+        return (firstApiLevel == INT_VALUE_IF_UNSET) ? Build.VERSION.SDK_INT : firstApiLevel;
+    }
+
+    /**
+     * Return whether the SDK version of the vendor partiton is newer than the given API level.
+     * If the property is set to non-integer value, this means the vendor partition is using
+     * current API level and true is returned.
+     */
+    public static boolean isVendorApiLevelNewerThan(int apiLevel) {
+        int vendorApiLevel = getPropertyInt(VNDK_VERSION);
+        if (vendorApiLevel == INT_VALUE_IF_UNSET) {
+            return true;
+        }
+        return vendorApiLevel > apiLevel;
+    }
+
+    /**
+     * Return the manufacturer of this product. If unset, return null.
+     */
+    public static String getManufacturer() {
+        return getProperty(MANUFACTURER_PROPERTY);
+    }
+
+    /** Returns a mapping from client ID names to client ID values */
+    public static Map<String, String> getClientIds() throws IOException {
+        Map<String,String> clientIds = new HashMap<>();
+        String queryOutput = SystemUtil.runShellCommand(
+                InstrumentationRegistry.getInstrumentation(), GOOGLE_SETTINGS_QUERY);
+        for (String line : queryOutput.split("[\\r?\\n]+")) {
+            // Expected line format: "Row: 1 _id=123, name=<property_name>, value=<property_value>"
+            Pattern pattern = Pattern.compile("name=([a-z_]*), value=(.*)$");
+            Matcher matcher = pattern.matcher(line);
+            if (matcher.find()) {
+                String name = matcher.group(1);
+                String value = matcher.group(2);
+                if (name.contains("client_id")) {
+                    clientIds.put(name, value); // only add name-value pair for client ids
+                }
+            }
+        }
+        return clientIds;
+    }
+
+    /** Returns whether the property exists on this device */
+    public static boolean propertyExists(String property) {
+        return getProperty(property) != null;
+    }
+
+    /** Returns whether the property value is equal to a given string */
+    public static boolean propertyEquals(String property, String value) {
+        if (value == null) {
+            return !propertyExists(property); // null value implies property does not exist
+        }
+        return value.equals(getProperty(property));
+    }
+
+    /**
+     * Returns whether the property value matches a given regular expression. The method uses
+     * String.matches(), requiring a complete match (i.e. expression matches entire value string)
+     */
+    public static boolean propertyMatches(String property, String regex) {
+        if (regex == null || regex.isEmpty()) {
+            // null or empty pattern implies property does not exist
+            return !propertyExists(property);
+        }
+        String value = getProperty(property);
+        return (value == null) ? false : value.matches(regex);
+    }
+
+    /**
+     * Retrieves the desired integer property, returning INT_VALUE_IF_UNSET if not found.
+     */
+    public static int getPropertyInt(String property) {
+        String value = getProperty(property);
+        if (value == null) {
+            return INT_VALUE_IF_UNSET;
+        }
+        try {
+            return Integer.parseInt(value);
+        } catch (NumberFormatException e) {
+            return INT_VALUE_IF_UNSET;
+        }
+    }
+
+    /** Retrieves the desired property value in string form */
+    public static String getProperty(String property) {
+        Scanner scanner = null;
+        try {
+            Process process = new ProcessBuilder("getprop", property).start();
+            scanner = new Scanner(process.getInputStream());
+            String value = scanner.nextLine().trim();
+            return (value.isEmpty()) ? null : value;
+        } catch (IOException e) {
+            return null;
+        } finally {
+            if (scanner != null) {
+                scanner.close();
+            }
+        }
+    }
+}
diff --git a/common/device-side/util-axt/src/com/android/compatibility/common/util/ReadElf.java b/common/device-side/util-axt/src/com/android/compatibility/common/util/ReadElf.java
new file mode 100644
index 0000000..feaa9cd
--- /dev/null
+++ b/common/device-side/util-axt/src/com/android/compatibility/common/util/ReadElf.java
@@ -0,0 +1,494 @@
+/*
+ * Copyright (C) 2011 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.compatibility.common.util;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.RandomAccessFile;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * A poor man's implementation of the readelf command. This program is designed
+ * to parse ELF (Executable and Linkable Format) files.
+ */
+public class ReadElf implements AutoCloseable {
+    /** The magic values for the ELF identification. */
+    private static final byte[] ELFMAG = {
+            (byte) 0x7F, (byte) 'E', (byte) 'L', (byte) 'F', };
+
+    private static final int EI_NIDENT = 16;
+
+    private static final int EI_CLASS = 4;
+    private static final int EI_DATA = 5;
+
+    private static final int EM_386 = 3;
+    private static final int EM_MIPS = 8;
+    private static final int EM_ARM = 40;
+    private static final int EM_X86_64 = 62;
+    // http://en.wikipedia.org/wiki/Qualcomm_Hexagon
+    private static final int EM_QDSP6 = 164;
+    private static final int EM_AARCH64 = 183;
+
+    private static final int ELFCLASS32 = 1;
+    private static final int ELFCLASS64 = 2;
+
+    private static final int ELFDATA2LSB = 1;
+    private static final int ELFDATA2MSB = 2;
+
+    private static final int EV_CURRENT = 1;
+
+    private static final long PT_LOAD = 1;
+
+    private static final int SHT_SYMTAB = 2;
+    private static final int SHT_STRTAB = 3;
+    private static final int SHT_DYNAMIC = 6;
+    private static final int SHT_DYNSYM = 11;
+
+    public static class Symbol {
+        public static final int STB_LOCAL = 0;
+        public static final int STB_GLOBAL = 1;
+        public static final int STB_WEAK = 2;
+        public static final int STB_LOPROC = 13;
+        public static final int STB_HIPROC = 15;
+
+        public static final int STT_NOTYPE = 0;
+        public static final int STT_OBJECT = 1;
+        public static final int STT_FUNC = 2;
+        public static final int STT_SECTION = 3;
+        public static final int STT_FILE = 4;
+        public static final int STT_COMMON = 5;
+        public static final int STT_TLS = 6;
+
+        public final String name;
+        public final int bind;
+        public final int type;
+
+        Symbol(String name, int st_info) {
+            this.name = name;
+            this.bind = (st_info >> 4) & 0x0F;
+            this.type = st_info & 0x0F;
+        }
+
+        @Override
+        public String toString() {
+            return "Symbol[" + name + "," + toBind() + "," + toType() + "]";
+        }
+
+        private String toBind() {
+            switch (bind) {
+                case STB_LOCAL:
+                    return "LOCAL";
+                case STB_GLOBAL:
+                    return "GLOBAL";
+                case STB_WEAK:
+                    return "WEAK";
+            }
+            return "STB_??? (" + bind + ")";
+        }
+
+        private String toType() {
+            switch (type) {
+                case STT_NOTYPE:
+                    return "NOTYPE";
+                case STT_OBJECT:
+                    return "OBJECT";
+                case STT_FUNC:
+                    return "FUNC";
+                case STT_SECTION:
+                    return "SECTION";
+                case STT_FILE:
+                    return "FILE";
+                case STT_COMMON:
+                    return "COMMON";
+                case STT_TLS:
+                    return "TLS";
+            }
+            return "STT_??? (" + type + ")";
+        }
+    }
+
+    private final String mPath;
+    private final RandomAccessFile mFile;
+    private final byte[] mBuffer = new byte[512];
+    private int mEndian;
+    private boolean mIsDynamic;
+    private boolean mIsPIE;
+    private int mType;
+    private int mAddrSize;
+
+    /** Symbol Table offset */
+    private long mSymTabOffset;
+
+    /** Symbol Table size */
+    private long mSymTabSize;
+
+    /** Dynamic Symbol Table offset */
+    private long mDynSymOffset;
+
+    /** Dynamic Symbol Table size */
+    private long mDynSymSize;
+
+    /** Section Header String Table offset */
+    private long mShStrTabOffset;
+
+    /** Section Header String Table size */
+    private long mShStrTabSize;
+
+    /** String Table offset */
+    private long mStrTabOffset;
+
+    /** String Table size */
+    private long mStrTabSize;
+
+    /** Dynamic String Table offset */
+    private long mDynStrOffset;
+
+    /** Dynamic String Table size */
+    private long mDynStrSize;
+
+    /** Symbol Table symbol names */
+    private Map<String, Symbol> mSymbols;
+
+    /** Dynamic Symbol Table symbol names */
+    private Map<String, Symbol> mDynamicSymbols;
+
+    public static ReadElf read(File file) throws IOException {
+        return new ReadElf(file);
+    }
+
+    public static void main(String[] args) throws IOException {
+        for (String arg : args) {
+            ReadElf re = new ReadElf(new File(arg));
+            re.getSymbol("x");
+            re.getDynamicSymbol("x");
+            re.close();
+        }
+    }
+
+    public boolean isDynamic() {
+        return mIsDynamic;
+    }
+
+    public int getType() {
+        return mType;
+    }
+
+    public boolean isPIE() {
+        return mIsPIE;
+    }
+
+    private ReadElf(File file) throws IOException {
+        mPath = file.getPath();
+        mFile = new RandomAccessFile(file, "r");
+
+        if (mFile.length() < EI_NIDENT) {
+            throw new IllegalArgumentException("Too small to be an ELF file: " + file);
+        }
+
+        readHeader();
+    }
+
+    @Override
+    public void close() {
+        try {
+            mFile.close();
+        } catch (IOException ignored) {
+        }
+    }
+
+    @Override
+    protected void finalize() throws Throwable {
+        try {
+            close();
+        } finally {
+            super.finalize();
+        }
+    }
+
+    private void readHeader() throws IOException {
+        mFile.seek(0);
+        mFile.readFully(mBuffer, 0, EI_NIDENT);
+
+        if (mBuffer[0] != ELFMAG[0] || mBuffer[1] != ELFMAG[1] ||
+                mBuffer[2] != ELFMAG[2] || mBuffer[3] != ELFMAG[3]) {
+            throw new IllegalArgumentException("Invalid ELF file: " + mPath);
+        }
+
+        int elfClass = mBuffer[EI_CLASS];
+        if (elfClass == ELFCLASS32) {
+            mAddrSize = 4;
+        } else if (elfClass == ELFCLASS64) {
+            mAddrSize = 8;
+        } else {
+            throw new IOException("Invalid ELF EI_CLASS: " + elfClass + ": " + mPath);
+        }
+
+        mEndian = mBuffer[EI_DATA];
+        if (mEndian == ELFDATA2LSB) {
+        } else if (mEndian == ELFDATA2MSB) {
+            throw new IOException("Unsupported ELFDATA2MSB file: " + mPath);
+        } else {
+            throw new IOException("Invalid ELF EI_DATA: " + mEndian + ": " + mPath);
+        }
+
+        mType = readHalf();
+
+        int e_machine = readHalf();
+        if (e_machine != EM_386 && e_machine != EM_X86_64 &&
+                e_machine != EM_AARCH64 && e_machine != EM_ARM &&
+                e_machine != EM_MIPS &&
+                e_machine != EM_QDSP6) {
+            throw new IOException("Invalid ELF e_machine: " + e_machine + ": " + mPath);
+        }
+
+        // AbiTest relies on us rejecting any unsupported combinations.
+        if ((e_machine == EM_386 && elfClass != ELFCLASS32) ||
+                (e_machine == EM_X86_64 && elfClass != ELFCLASS64) ||
+                (e_machine == EM_AARCH64 && elfClass != ELFCLASS64) ||
+                (e_machine == EM_ARM && elfClass != ELFCLASS32) ||
+                (e_machine == EM_QDSP6 && elfClass != ELFCLASS32)) {
+            throw new IOException("Invalid e_machine/EI_CLASS ELF combination: " +
+                    e_machine + "/" + elfClass + ": " + mPath);
+        }
+
+        long e_version = readWord();
+        if (e_version != EV_CURRENT) {
+            throw new IOException("Invalid e_version: " + e_version + ": " + mPath);
+        }
+
+        long e_entry = readAddr();
+
+        long ph_off = readOff();
+        long sh_off = readOff();
+
+        long e_flags = readWord();
+        int e_ehsize = readHalf();
+        int e_phentsize = readHalf();
+        int e_phnum = readHalf();
+        int e_shentsize = readHalf();
+        int e_shnum = readHalf();
+        int e_shstrndx = readHalf();
+
+        readSectionHeaders(sh_off, e_shnum, e_shentsize, e_shstrndx);
+        readProgramHeaders(ph_off, e_phnum, e_phentsize);
+    }
+
+    private void readSectionHeaders(long sh_off, int e_shnum, int e_shentsize, int e_shstrndx)
+            throws IOException {
+        // Read the Section Header String Table offset first.
+        {
+            mFile.seek(sh_off + e_shstrndx * e_shentsize);
+
+            long sh_name = readWord();
+            long sh_type = readWord();
+            long sh_flags = readX(mAddrSize);
+            long sh_addr = readAddr();
+            long sh_offset = readOff();
+            long sh_size = readX(mAddrSize);
+            // ...
+
+            if (sh_type == SHT_STRTAB) {
+                mShStrTabOffset = sh_offset;
+                mShStrTabSize = sh_size;
+            }
+        }
+
+        for (int i = 0; i < e_shnum; ++i) {
+            // Don't bother to re-read the Section Header StrTab.
+            if (i == e_shstrndx) {
+                continue;
+            }
+
+            mFile.seek(sh_off + i * e_shentsize);
+
+            long sh_name = readWord();
+            long sh_type = readWord();
+            long sh_flags = readX(mAddrSize);
+            long sh_addr = readAddr();
+            long sh_offset = readOff();
+            long sh_size = readX(mAddrSize);
+
+            if (sh_type == SHT_SYMTAB || sh_type == SHT_DYNSYM) {
+                final String symTabName = readShStrTabEntry(sh_name);
+                if (".symtab".equals(symTabName)) {
+                    mSymTabOffset = sh_offset;
+                    mSymTabSize = sh_size;
+                } else if (".dynsym".equals(symTabName)) {
+                    mDynSymOffset = sh_offset;
+                    mDynSymSize = sh_size;
+                }
+            } else if (sh_type == SHT_STRTAB) {
+                final String strTabName = readShStrTabEntry(sh_name);
+                if (".strtab".equals(strTabName)) {
+                    mStrTabOffset = sh_offset;
+                    mStrTabSize = sh_size;
+                } else if (".dynstr".equals(strTabName)) {
+                    mDynStrOffset = sh_offset;
+                    mDynStrSize = sh_size;
+                }
+            } else if (sh_type == SHT_DYNAMIC) {
+                mIsDynamic = true;
+            }
+        }
+    }
+
+    private void readProgramHeaders(long ph_off, int e_phnum, int e_phentsize) throws IOException {
+        for (int i = 0; i < e_phnum; ++i) {
+            mFile.seek(ph_off + i * e_phentsize);
+
+            long p_type = readWord();
+            if (p_type == PT_LOAD) {
+                if (mAddrSize == 8) {
+                    // Only in Elf64_phdr; in Elf32_phdr p_flags is at the end.
+                    long p_flags = readWord();
+                }
+                long p_offset = readOff();
+                long p_vaddr = readAddr();
+                // ...
+
+                if (p_vaddr == 0) {
+                    mIsPIE = true;
+                }
+            }
+        }
+    }
+
+    private HashMap<String, Symbol> readSymbolTable(long symStrOffset, long symStrSize,
+            long tableOffset, long tableSize) throws IOException {
+        HashMap<String, Symbol> result = new HashMap<String, Symbol>();
+        mFile.seek(tableOffset);
+        while (mFile.getFilePointer() < tableOffset + tableSize) {
+            long st_name = readWord();
+            int st_info;
+            if (mAddrSize == 8) {
+                st_info = readByte();
+                int st_other = readByte();
+                int st_shndx = readHalf();
+                long st_value = readAddr();
+                long st_size = readX(mAddrSize);
+            } else {
+                long st_value = readAddr();
+                long st_size = readWord();
+                st_info = readByte();
+                int st_other = readByte();
+                int st_shndx = readHalf();
+            }
+            if (st_name == 0) {
+                continue;
+            }
+
+            final String symName = readStrTabEntry(symStrOffset, symStrSize, st_name);
+            if (symName != null) {
+                Symbol s = new Symbol(symName, st_info);
+                result.put(symName, s);
+            }
+        }
+        return result;
+    }
+
+    private String readShStrTabEntry(long strOffset) throws IOException {
+        if (mShStrTabOffset == 0 || strOffset < 0 || strOffset >= mShStrTabSize) {
+            return null;
+        }
+        return readString(mShStrTabOffset + strOffset);
+    }
+
+    private String readStrTabEntry(long tableOffset, long tableSize, long strOffset)
+            throws IOException {
+        if (tableOffset == 0 || strOffset < 0 || strOffset >= tableSize) {
+            return null;
+        }
+        return readString(tableOffset + strOffset);
+    }
+
+    private int readHalf() throws IOException {
+        return (int) readX(2);
+    }
+
+    private long readWord() throws IOException {
+        return readX(4);
+    }
+
+    private long readOff() throws IOException {
+        return readX(mAddrSize);
+    }
+
+    private long readAddr() throws IOException {
+        return readX(mAddrSize);
+    }
+
+    private long readX(int byteCount) throws IOException {
+        mFile.readFully(mBuffer, 0, byteCount);
+
+        int answer = 0;
+        if (mEndian == ELFDATA2LSB) {
+            for (int i = byteCount - 1; i >= 0; i--) {
+                answer = (answer << 8) | (mBuffer[i] & 0xff);
+            }
+        } else {
+            final int N = byteCount - 1;
+            for (int i = 0; i <= N; ++i) {
+                answer = (answer << 8) | (mBuffer[i] & 0xff);
+            }
+        }
+
+        return answer;
+    }
+
+    private String readString(long offset) throws IOException {
+        long originalOffset = mFile.getFilePointer();
+        mFile.seek(offset);
+        mFile.readFully(mBuffer, 0, (int) Math.min(mBuffer.length, mFile.length() - offset));
+        mFile.seek(originalOffset);
+
+        for (int i = 0; i < mBuffer.length; ++i) {
+            if (mBuffer[i] == 0) {
+                return new String(mBuffer, 0, i);
+            }
+        }
+
+        return null;
+    }
+
+    private int readByte() throws IOException {
+        return mFile.read() & 0xff;
+    }
+
+    public Symbol getSymbol(String name) {
+        if (mSymbols == null) {
+            try {
+                mSymbols = readSymbolTable(mStrTabOffset, mStrTabSize, mSymTabOffset, mSymTabSize);
+            } catch (IOException e) {
+                return null;
+            }
+        }
+        return mSymbols.get(name);
+    }
+
+    public Symbol getDynamicSymbol(String name) {
+        if (mDynamicSymbols == null) {
+            try {
+                mDynamicSymbols = readSymbolTable(
+                        mDynStrOffset, mDynStrSize, mDynSymOffset, mDynSymSize);
+            } catch (IOException e) {
+                return null;
+            }
+        }
+        return mDynamicSymbols.get(name);
+    }
+}
diff --git a/common/device-side/util-axt/src/com/android/compatibility/common/util/ReportLogDeviceInfoStore.java b/common/device-side/util-axt/src/com/android/compatibility/common/util/ReportLogDeviceInfoStore.java
new file mode 100644
index 0000000..538881d
--- /dev/null
+++ b/common/device-side/util-axt/src/com/android/compatibility/common/util/ReportLogDeviceInfoStore.java
@@ -0,0 +1,96 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.compatibility.common.util;
+
+import android.util.JsonWriter;
+
+import java.io.BufferedReader;
+import java.io.BufferedWriter;
+import java.io.File;
+import java.io.FileReader;
+import java.io.FileWriter;
+import java.io.IOException;
+
+public class ReportLogDeviceInfoStore extends DeviceInfoStore {
+
+    private final String mStreamName;
+    private File tempJsonFile;
+
+    public ReportLogDeviceInfoStore(File jsonFile, String streamName) throws Exception {
+        mJsonFile = jsonFile;
+        mStreamName = streamName;
+    }
+
+    /**
+     * Creates the writer and starts the JSON Object for the metric stream.
+     */
+    @Override
+    public void open() throws IOException {
+        // Write new metrics to a temp file to avoid invalid JSON files due to failed tests.
+        BufferedWriter formatWriter;
+        tempJsonFile = File.createTempFile(mStreamName, "-temp-report-log");
+        formatWriter = new BufferedWriter(new FileWriter(tempJsonFile));
+        if (mJsonFile.exists()) {
+            BufferedReader jsonReader = new BufferedReader(new FileReader(mJsonFile));
+            String currentLine;
+            String nextLine = jsonReader.readLine();
+            while ((currentLine = nextLine) != null) {
+                nextLine = jsonReader.readLine();
+                if (nextLine == null && currentLine.charAt(currentLine.length() - 1) == '}') {
+                    // Reopen overall JSON object to write new metrics.
+                    currentLine = currentLine.substring(0, currentLine.length() - 1) + ",";
+                }
+                // Copy to temp file directly to avoid large metrics string in memory.
+                formatWriter.write(currentLine, 0, currentLine.length());
+            }
+            jsonReader.close();
+        } else {
+            formatWriter.write("{", 0 , 1);
+        }
+        // Start new JSON object for new metrics.
+        formatWriter.write("\"" + mStreamName + "\":", 0, mStreamName.length() + 3);
+        formatWriter.flush();
+        formatWriter.close();
+        mJsonWriter = new JsonWriter(new FileWriter(tempJsonFile, true));
+        mJsonWriter.beginObject();
+    }
+
+    /**
+     * Closes the writer.
+     */
+    @Override
+    public void close() throws IOException {
+        // Close JSON Writer.
+        mJsonWriter.endObject();
+        mJsonWriter.close();
+        // Close overall JSON Object.
+        try (BufferedWriter formatWriter = new BufferedWriter(new FileWriter(tempJsonFile, true))) {
+            formatWriter.write("}", 0, 1);
+        }
+        // Copy metrics from temp file and delete temp file.
+        mJsonFile.createNewFile();
+        try (
+                BufferedReader jsonReader = new BufferedReader(new FileReader(tempJsonFile));
+                BufferedWriter metricsWriter = new BufferedWriter(new FileWriter(mJsonFile))
+        ) {
+            String line;
+            while ((line = jsonReader.readLine()) != null) {
+                // Copy from temp file directly to avoid large metrics string in memory.
+                metricsWriter.write(line, 0, line.length());
+            }
+        }
+    }
+}
diff --git a/common/device-side/util-axt/src/com/android/compatibility/common/util/RequiredFeatureRule.java b/common/device-side/util-axt/src/com/android/compatibility/common/util/RequiredFeatureRule.java
new file mode 100644
index 0000000..deb7056
--- /dev/null
+++ b/common/device-side/util-axt/src/com/android/compatibility/common/util/RequiredFeatureRule.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.compatibility.common.util;
+
+import androidx.test.InstrumentationRegistry;
+import android.util.Log;
+
+import org.junit.rules.TestRule;
+import org.junit.runner.Description;
+import org.junit.runners.model.Statement;
+
+/**
+ * Custom JUnit4 rule that does not run a test case if the device does not have a given feature.
+ */
+public class RequiredFeatureRule implements TestRule {
+    private static final String TAG = "RequiredFeatureRule";
+
+    private final String mFeature;
+    private final boolean mHasFeature;
+
+    public RequiredFeatureRule(String feature) {
+        mFeature = feature;
+        mHasFeature = hasFeature(feature);
+    }
+
+    @Override
+    public Statement apply(Statement base, Description description) {
+        return new Statement() {
+
+            @Override
+            public void evaluate() throws Throwable {
+                if (!mHasFeature) {
+                    Log.d(TAG, "skipping "
+                            + description.getClassName() + "#" + description.getMethodName()
+                            + " because device does not have feature '" + mFeature + "'");
+                    return;
+                }
+                base.evaluate();
+            }
+        };
+    }
+
+    public static boolean hasFeature(String feature) {
+        return InstrumentationRegistry.getContext().getPackageManager().hasSystemFeature(feature);
+    }
+}
diff --git a/common/device-side/util-axt/src/com/android/compatibility/common/util/Result.java b/common/device-side/util-axt/src/com/android/compatibility/common/util/Result.java
new file mode 100644
index 0000000..0d8a29a
--- /dev/null
+++ b/common/device-side/util-axt/src/com/android/compatibility/common/util/Result.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2014 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.compatibility.common.util;
+
+/**
+ * Represents the result of a test.
+ */
+public interface Result {
+    public static final int RESULT_OK = 1;
+    public static final int RESULT_FAIL = 2;
+    /**
+     * Sets the test result of this object.
+     *
+     * @param resultCode The test result, either {@code RESULT_OK} or {@code RESULT_FAIL}.
+     */
+    void setResult(int resultCode);
+}
diff --git a/common/device-side/util-axt/src/com/android/compatibility/common/util/SettingsUtils.java b/common/device-side/util-axt/src/com/android/compatibility/common/util/SettingsUtils.java
new file mode 100644
index 0000000..c34cb60
--- /dev/null
+++ b/common/device-side/util-axt/src/com/android/compatibility/common/util/SettingsUtils.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.compatibility.common.util;
+
+public class SettingsUtils {
+    private SettingsUtils() {
+    }
+
+    /**
+     * Put a global setting.
+     */
+    public static void putGlobalSetting(String key, String value) {
+        // Hmm, technically we should escape a value, but if I do like '1', it won't work. ??
+        SystemUtil.runShellCommandForNoOutput("settings put global " + key + " " + value);
+    }
+
+    /**
+     * Put a global setting for the current (foreground) user.
+     */
+    public static void putSecureSetting(String key, String value) {
+        SystemUtil.runShellCommandForNoOutput(
+                "settings --user current put secure " + key + " " + value);
+    }
+}
diff --git a/common/device-side/util-axt/src/com/android/compatibility/common/util/SystemUtil.java b/common/device-side/util-axt/src/com/android/compatibility/common/util/SystemUtil.java
new file mode 100644
index 0000000..a7c2321
--- /dev/null
+++ b/common/device-side/util-axt/src/com/android/compatibility/common/util/SystemUtil.java
@@ -0,0 +1,142 @@
+/*
+ * Copyright (C) 2014 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.compatibility.common.util;
+
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import android.app.ActivityManager;
+import android.app.ActivityManager.MemoryInfo;
+import android.app.Instrumentation;
+import android.content.Context;
+import android.os.ParcelFileDescriptor;
+import android.os.StatFs;
+import androidx.test.InstrumentationRegistry;
+import android.util.Log;
+
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.util.function.Predicate;
+
+public class SystemUtil {
+    private static final String TAG = "CtsSystemUtil";
+
+    public static long getFreeDiskSize(Context context) {
+        final StatFs statFs = new StatFs(context.getFilesDir().getAbsolutePath());
+        return (long)statFs.getAvailableBlocks() * statFs.getBlockSize();
+    }
+
+    public static long getFreeMemory(Context context) {
+        final MemoryInfo info = new MemoryInfo();
+        ((ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE)).getMemoryInfo(info);
+        return info.availMem;
+    }
+
+    public static long getTotalMemory(Context context) {
+        final MemoryInfo info = new MemoryInfo();
+        ((ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE)).getMemoryInfo(info);
+        return info.totalMem;
+    }
+
+    /**
+     * Executes a shell command using shell user identity, and return the standard output in string
+     * <p>Note: calling this function requires API level 21 or above
+     * @param instrumentation {@link Instrumentation} instance, obtained from a test running in
+     * instrumentation framework
+     * @param cmd the command to run
+     * @return the standard output of the command
+     * @throws Exception
+     */
+    public static String runShellCommand(Instrumentation instrumentation, String cmd)
+            throws IOException {
+        Log.v(TAG, "Running command: " + cmd);
+        if (cmd.startsWith("pm grant ") || cmd.startsWith("pm revoke ")) {
+            throw new UnsupportedOperationException("Use UiAutomation.grantRuntimePermission() "
+                    + "or revokeRuntimePermission() directly, which are more robust.");
+        }
+        ParcelFileDescriptor pfd = instrumentation.getUiAutomation().executeShellCommand(cmd);
+        byte[] buf = new byte[512];
+        int bytesRead;
+        FileInputStream fis = new ParcelFileDescriptor.AutoCloseInputStream(pfd);
+        StringBuffer stdout = new StringBuffer();
+        while ((bytesRead = fis.read(buf)) != -1) {
+            stdout.append(new String(buf, 0, bytesRead));
+        }
+        fis.close();
+        return stdout.toString();
+    }
+
+    /**
+     * Simpler version of {@link #runShellCommand(Instrumentation, String)}.
+     */
+    public static String runShellCommand(String cmd) {
+        try {
+            return runShellCommand(InstrumentationRegistry.getInstrumentation(), cmd);
+        } catch (IOException e) {
+            fail("Failed reading command output: " + e);
+            return "";
+        }
+    }
+
+    /**
+     * Same as {@link #runShellCommand(String)}, with optionally
+     * check the result using {@code resultChecker}.
+     */
+    public static String runShellCommand(String cmd, Predicate<String> resultChecker) {
+        final String result = runShellCommand(cmd);
+        if (resultChecker != null) {
+            assertTrue("Assertion failed. Command was: " + cmd + "\n"
+                    + "Output was:\n" + result,
+                    resultChecker.test(result));
+        }
+        return result;
+    }
+
+    /**
+     * Same as {@link #runShellCommand(String)}, but fails if the output is not empty.
+     */
+    public static String runShellCommandForNoOutput(String cmd) {
+        final String result = runShellCommand(cmd);
+        assertTrue("Command failed. Command was: " + cmd + "\n"
+                + "Didn't expect any output, but the output was:\n" + result,
+                result.length() == 0);
+        return result;
+    }
+
+    /**
+     * Run a command and print the result on logcat.
+     */
+    public static void runCommandAndPrintOnLogcat(String logtag, String cmd) {
+        Log.i(logtag, "Executing: " + cmd);
+        final String output = runShellCommand(cmd);
+        for (String line : output.split("\\n", -1)) {
+            Log.i(logtag, line);
+        }
+    }
+
+    /**
+     * Run a command and return the section matching the patterns.
+     *
+     * @see TextUtils#extractSection
+     */
+    public static String runCommandAndExtractSection(String cmd,
+            String extractionStartRegex, boolean startInclusive,
+            String extractionEndRegex, boolean endInclusive) {
+        return TextUtils.extractSection(runShellCommand(cmd), extractionStartRegex, startInclusive,
+                extractionEndRegex, endInclusive);
+    }
+}
diff --git a/common/device-side/util-axt/src/com/android/compatibility/common/util/TestThread.java b/common/device-side/util-axt/src/com/android/compatibility/common/util/TestThread.java
new file mode 100644
index 0000000..894b9c8
--- /dev/null
+++ b/common/device-side/util-axt/src/com/android/compatibility/common/util/TestThread.java
@@ -0,0 +1,93 @@
+/*
+ * Copyright (C) 2009 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.compatibility.common.util;
+
+/**
+ * Thread class for executing a Runnable containing assertions in a separate thread.
+ * Uncaught exceptions in the Runnable are rethrown in the context of the the thread
+ * calling the <code>runTest()</code> method.
+ */
+public final class TestThread extends Thread {
+    private Throwable mThrowable;
+    private Runnable mTarget;
+
+    public TestThread(Runnable target) {
+        mTarget = target;
+    }
+
+    @Override
+    public final void run() {
+        try {
+            mTarget.run();
+        } catch (Throwable t) {
+            mThrowable = t;
+        }
+    }
+
+    /**
+     * Run the target Runnable object and wait until the test finish or throw
+     * out Exception if test fail.
+     *
+     * @param runTime
+     * @throws Throwable
+     */
+    public void runTest(long runTime) throws Throwable {
+        start();
+        joinAndCheck(runTime);
+    }
+
+    /**
+     * Get the Throwable object which is thrown when test running
+     * @return  The Throwable object
+     */
+    public Throwable getThrowable() {
+        return mThrowable;
+    }
+
+    /**
+     * Set the Throwable object which is thrown when test running
+     * @param t The Throwable object
+     */
+    public void setThrowable(Throwable t) {
+        mThrowable = t;
+    }
+
+    /**
+     * Wait for the test thread to complete and throw the stored exception if there is one.
+     *
+     * @param runTime The time to wait for the test thread to complete.
+     * @throws Throwable
+     */
+    public void joinAndCheck(long runTime) throws Throwable {
+        this.join(runTime);
+        if (this.isAlive()) {
+            this.interrupt();
+            this.join(runTime);
+            throw new Exception("Thread did not finish within allotted time.");
+        }
+        checkException();
+    }
+
+    /**
+     * Check whether there is an exception when running Runnable object.
+     * @throws Throwable
+     */
+    public void checkException() throws Throwable {
+        if (mThrowable != null) {
+            throw mThrowable;
+        }
+    }
+}
diff --git a/common/device-side/util-axt/src/com/android/compatibility/common/util/TestUtils.java b/common/device-side/util-axt/src/com/android/compatibility/common/util/TestUtils.java
new file mode 100644
index 0000000..1702875
--- /dev/null
+++ b/common/device-side/util-axt/src/com/android/compatibility/common/util/TestUtils.java
@@ -0,0 +1,97 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.compatibility.common.util;
+
+import static junit.framework.Assert.fail;
+
+import android.os.SystemClock;
+import android.util.Log;
+
+public class TestUtils {
+    private static final String TAG = "CtsTestUtils";
+
+    private TestUtils() {
+    }
+
+    public static final int DEFAULT_TIMEOUT_SECONDS = 30;
+
+    /** Print an error log and fail. */
+    public static void failWithLog(String message) {
+        Log.e(TAG, message);
+        fail(message);
+    }
+
+    @FunctionalInterface
+    public interface BooleanSupplierWithThrow {
+        boolean getAsBoolean() throws Exception;
+    }
+
+    @FunctionalInterface
+    public interface RunnableWithThrow {
+        void run() throws Exception;
+    }
+
+    /**
+     * Wait until {@code predicate} is satisfied, or fail, with {@link #DEFAULT_TIMEOUT_SECONDS}.
+     */
+    public static void waitUntil(String message, BooleanSupplierWithThrow predicate)
+            throws Exception {
+        waitUntil(message, 0, predicate);
+    }
+
+    /**
+     * Wait until {@code predicate} is satisfied, or fail, with a given timeout.
+     */
+    public static void waitUntil(
+            String message, int timeoutSecond, BooleanSupplierWithThrow predicate)
+            throws Exception {
+        if (timeoutSecond <= 0) {
+            timeoutSecond = DEFAULT_TIMEOUT_SECONDS;
+        }
+        int sleep = 125;
+        final long timeout = SystemClock.uptimeMillis() + timeoutSecond * 1000;
+        while (SystemClock.uptimeMillis() < timeout) {
+            if (predicate.getAsBoolean()) {
+                return; // okay
+            }
+            Thread.sleep(sleep);
+            sleep *= 5;
+            sleep = Math.min(2000, sleep);
+        }
+        failWithLog("Timeout: " + message);
+    }
+
+    /**
+     * Run a Runnable {@code r}, and if it throws, also run {@code onFailure}.
+     */
+    public static void runWithFailureHook(RunnableWithThrow r, RunnableWithThrow onFailure)
+            throws Exception {
+        if (r == null) {
+            throw new NullPointerException("r");
+        }
+        if (onFailure == null) {
+            throw new NullPointerException("onFailure");
+        }
+        try {
+            r.run();
+        } catch (Throwable th) {
+            Log.e(TAG, "Caught exception: " + th, th);
+            onFailure.run();
+            throw th;
+        }
+    }
+}
+
diff --git a/common/device-side/util-axt/src/com/android/compatibility/common/util/TextUtils.java b/common/device-side/util-axt/src/com/android/compatibility/common/util/TextUtils.java
new file mode 100644
index 0000000..639dc9c
--- /dev/null
+++ b/common/device-side/util-axt/src/com/android/compatibility/common/util/TextUtils.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.compatibility.common.util;
+
+import java.util.regex.Pattern;
+
+public class TextUtils {
+    private TextUtils() {
+    }
+
+    /**
+     * Return the first section in {@code source} between the line matches
+     * {@code extractionStartRegex} and the line matches {@code extractionEndRegex}.
+     */
+    public static String extractSection(String source,
+            String extractionStartRegex, boolean startInclusive,
+            String extractionEndRegex, boolean endInclusive) {
+
+        final Pattern start = Pattern.compile(extractionStartRegex);
+        final Pattern end = Pattern.compile(extractionEndRegex);
+
+        final StringBuilder sb = new StringBuilder();
+        final String[] lines = source.split("\\n", -1);
+
+        int i = 0;
+        for (; i < lines.length; i++) {
+            final String line = lines[i];
+            if (start.matcher(line).matches()) {
+                if (startInclusive) {
+                    sb.append(line);
+                    sb.append('\n');
+                }
+                i++;
+                break;
+            }
+        }
+
+        for (; i < lines.length; i++) {
+            final String line = lines[i];
+            if (end.matcher(line).matches()) {
+                if (endInclusive) {
+                    sb.append(line);
+                    sb.append('\n');
+                }
+                break;
+            }
+            sb.append(line);
+            sb.append('\n');
+        }
+        return sb.toString();
+    }
+}
diff --git a/common/device-side/util-axt/src/com/android/compatibility/common/util/ThreadUtils.java b/common/device-side/util-axt/src/com/android/compatibility/common/util/ThreadUtils.java
new file mode 100644
index 0000000..3948628
--- /dev/null
+++ b/common/device-side/util-axt/src/com/android/compatibility/common/util/ThreadUtils.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.compatibility.common.util;
+
+import android.os.SystemClock;
+
+public final class ThreadUtils {
+    private ThreadUtils() {
+    }
+
+    public static void sleepUntilRealtime(long realtime) throws Exception {
+        Thread.sleep(Math.max(0, realtime - SystemClock.elapsedRealtime()));
+    }
+}
diff --git a/common/device-side/util-axt/src/com/android/compatibility/common/util/WatchDog.java b/common/device-side/util-axt/src/com/android/compatibility/common/util/WatchDog.java
new file mode 100644
index 0000000..efcc693
--- /dev/null
+++ b/common/device-side/util-axt/src/com/android/compatibility/common/util/WatchDog.java
@@ -0,0 +1,103 @@
+/*
+ * Copyright (C) 2012 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.compatibility.common.util;
+
+import java.util.concurrent.Semaphore;
+import java.util.concurrent.TimeUnit;
+
+import android.util.Log;
+
+import junit.framework.Assert;
+
+/**
+ * class for checking if rendering function is alive or not.
+ * panic if watch-dog is not reset over certain amount of time
+ */
+public class WatchDog implements Runnable {
+    private static final String TAG = "WatchDog";
+    private Thread mThread;
+    private Semaphore mSemaphore;
+    private volatile boolean mStopRequested;
+    private final long mTimeoutInMilliSecs;
+    private TimeoutCallback mCallback = null;
+
+    public WatchDog(long timeoutInMilliSecs) {
+        mTimeoutInMilliSecs = timeoutInMilliSecs;
+    }
+
+    public WatchDog(long timeoutInMilliSecs, TimeoutCallback callback) {
+        this(timeoutInMilliSecs);
+        mCallback = callback;
+    }
+
+    /** start watch-dog */
+    public void start() {
+        Log.i(TAG, "start");
+        mStopRequested = false;
+        mSemaphore = new Semaphore(0);
+        mThread = new Thread(this);
+        mThread.start();
+    }
+
+    /** stop watch-dog */
+    public void stop() {
+        Log.i(TAG, "stop");
+        if (mThread == null) {
+            return; // already finished
+        }
+        mStopRequested = true;
+        mSemaphore.release();
+        try {
+            mThread.join();
+        } catch (InterruptedException e) {
+            // ignore
+        }
+        mThread = null;
+        mSemaphore = null;
+    }
+
+    /** resets watch-dog, thus prevent it from panic */
+    public void reset() {
+        if (!mStopRequested) { // stop requested, but rendering still on-going
+            mSemaphore.release();
+        }
+    }
+
+    @Override
+    public void run() {
+        while (!mStopRequested) {
+            try {
+                boolean success = mSemaphore.tryAcquire(mTimeoutInMilliSecs, TimeUnit.MILLISECONDS);
+                if (mCallback == null) {
+                    Assert.assertTrue("Watchdog timed-out", success);
+                } else if (!success) {
+                    mCallback.onTimeout();
+                }
+            } catch (InterruptedException e) {
+                // this thread will not be interrupted,
+                // but if it happens, just check the exit condition.
+            }
+        }
+    }
+
+    /**
+     * Called by the Watchdog when it has timed out.
+     */
+    public interface TimeoutCallback {
+
+        public void onTimeout();
+    }
+}
diff --git a/common/device-side/util-axt/src/com/android/compatibility/common/util/WidgetTestUtils.java b/common/device-side/util-axt/src/com/android/compatibility/common/util/WidgetTestUtils.java
new file mode 100644
index 0000000..8f3ab8f
--- /dev/null
+++ b/common/device-side/util-axt/src/com/android/compatibility/common/util/WidgetTestUtils.java
@@ -0,0 +1,310 @@
+/*
+ * Copyright (C) 2008 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.compatibility.common.util;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.test.rule.ActivityTestRule;
+import android.text.Editable;
+import android.text.TextUtils;
+import android.view.View;
+import android.view.ViewTreeObserver;
+
+import junit.framework.Assert;
+
+import org.hamcrest.BaseMatcher;
+import org.hamcrest.Description;
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+
+import java.io.IOException;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+import static android.view.ViewTreeObserver.OnDrawListener;
+import static android.view.ViewTreeObserver.OnGlobalLayoutListener;
+import static org.mockito.hamcrest.MockitoHamcrest.argThat;
+
+/**
+ * The useful methods for widget test.
+ */
+public class WidgetTestUtils {
+    /**
+     * Assert that two bitmaps have identical content (same dimensions, same configuration,
+     * same pixel content).
+     *
+     * @param b1 the first bitmap which needs to compare.
+     * @param b2 the second bitmap which needs to compare.
+     */
+    public static void assertEquals(Bitmap b1, Bitmap b2) {
+        if (b1 == b2) {
+            return;
+        }
+
+        if (b1 == null || b2 == null) {
+            Assert.fail("the bitmaps are not equal");
+        }
+
+        // b1 and b2 are all not null.
+        if (b1.getWidth() != b2.getWidth() || b1.getHeight() != b2.getHeight()
+            || b1.getConfig() != b2.getConfig()) {
+            Assert.fail("the bitmaps are not equal");
+        }
+
+        int w = b1.getWidth();
+        int h = b1.getHeight();
+        int s = w * h;
+        int[] pixels1 = new int[s];
+        int[] pixels2 = new int[s];
+
+        b1.getPixels(pixels1, 0, w, 0, 0, w, h);
+        b2.getPixels(pixels2, 0, w, 0, 0, w, h);
+
+        for (int i = 0; i < s; i++) {
+            if (pixels1[i] != pixels2[i]) {
+                Assert.fail("the bitmaps are not equal");
+            }
+        }
+    }
+
+    /**
+     * Find beginning of the special element.
+     * @param parser XmlPullParser will be parsed.
+     * @param firstElementName the target element name.
+     *
+     * @throws XmlPullParserException if XML Pull Parser related faults occur.
+     * @throws IOException if I/O-related error occur when parsing.
+     */
+    public static final void beginDocument(XmlPullParser parser, String firstElementName)
+            throws XmlPullParserException, IOException {
+        Assert.assertNotNull(parser);
+        Assert.assertNotNull(firstElementName);
+
+        int type;
+        while ((type = parser.next()) != XmlPullParser.START_TAG
+                && type != XmlPullParser.END_DOCUMENT) {
+            ;
+        }
+
+        if (!parser.getName().equals(firstElementName)) {
+            throw new XmlPullParserException("Unexpected start tag: found " + parser.getName()
+                    + ", expected " + firstElementName);
+        }
+    }
+
+    /**
+     * Compare the expected pixels with actual, scaling for the target context density
+     *
+     * @throws AssertionFailedError
+     */
+    public static void assertScaledPixels(int expected, int actual, Context context) {
+        Assert.assertEquals(expected * context.getResources().getDisplayMetrics().density,
+                actual, 3);
+    }
+
+    /** Converts dips into pixels using the {@link Context}'s density. */
+    public static int convertDipToPixels(Context context, int dip) {
+      float density = context.getResources().getDisplayMetrics().density;
+      return Math.round(density * dip);
+    }
+
+    /**
+     * Retrieve a bitmap that can be used for comparison on any density
+     * @param resources
+     * @return the {@link Bitmap} or <code>null</code>
+     */
+    public static Bitmap getUnscaledBitmap(Resources resources, int resId) {
+        BitmapFactory.Options options = new BitmapFactory.Options();
+        options.inScaled = false;
+        return BitmapFactory.decodeResource(resources, resId, options);
+    }
+
+    /**
+     * Retrieve a dithered bitmap that can be used for comparison on any density
+     * @param resources
+     * @param config the preferred config for the returning bitmap
+     * @return the {@link Bitmap} or <code>null</code>
+     */
+    public static Bitmap getUnscaledAndDitheredBitmap(Resources resources,
+            int resId, Bitmap.Config config) {
+        BitmapFactory.Options options = new BitmapFactory.Options();
+        options.inDither = true;
+        options.inScaled = false;
+        options.inPreferredConfig = config;
+        return BitmapFactory.decodeResource(resources, resId, options);
+    }
+
+    /**
+     * Argument matcher for equality check of a CharSequence.
+     *
+     * @param expected expected CharSequence
+     *
+     * @return
+     */
+    public static CharSequence sameCharSequence(final CharSequence expected) {
+        return argThat(new BaseMatcher<CharSequence>() {
+            @Override
+            public boolean matches(Object o) {
+                if (o instanceof CharSequence) {
+                    return TextUtils.equals(expected, (CharSequence) o);
+                }
+                return false;
+            }
+
+            @Override
+            public void describeTo(Description description) {
+                description.appendText("doesn't match " + expected);
+            }
+        });
+    }
+
+    /**
+     * Argument matcher for equality check of an Editable.
+     *
+     * @param expected expected Editable
+     *
+     * @return
+     */
+    public static Editable sameEditable(final Editable expected) {
+        return argThat(new BaseMatcher<Editable>() {
+            @Override
+            public boolean matches(Object o) {
+                if (o instanceof Editable) {
+                    return TextUtils.equals(expected, (Editable) o);
+                }
+                return false;
+            }
+
+            @Override
+            public void describeTo(Description description) {
+                description.appendText("doesn't match " + expected);
+            }
+        });
+    }
+
+    /**
+     * Runs the specified Runnable on the main thread and ensures that the specified View's tree is
+     * drawn before returning.
+     *
+     * @param activityTestRule the activity test rule used to run the test
+     * @param view the view whose tree should be drawn before returning
+     * @param runner the runnable to run on the main thread, or {@code null} to
+     *               simply force invalidation and a draw pass
+     */
+    public static void runOnMainAndDrawSync(@NonNull final ActivityTestRule activityTestRule,
+            @NonNull final View view, @Nullable final Runnable runner) throws Throwable {
+        final CountDownLatch latch = new CountDownLatch(1);
+
+        activityTestRule.runOnUiThread(() -> {
+            final OnDrawListener listener = new OnDrawListener() {
+                @Override
+                public void onDraw() {
+                    // posting so that the sync happens after the draw that's about to happen
+                    view.post(() -> {
+                        activityTestRule.getActivity().getWindow().getDecorView().
+                                getViewTreeObserver().removeOnDrawListener(this);
+                        latch.countDown();
+                    });
+                }
+            };
+
+            activityTestRule.getActivity().getWindow().getDecorView().
+                    getViewTreeObserver().addOnDrawListener(listener);
+
+            if (runner != null) {
+                runner.run();
+            }
+            view.invalidate();
+        });
+
+        try {
+            Assert.assertTrue("Expected draw pass occurred within 5 seconds",
+                    latch.await(5, TimeUnit.SECONDS));
+        } catch (InterruptedException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    /**
+     * Runs the specified Runnable on the main thread and ensures that the activity's view tree is
+     * laid out before returning.
+     *
+     * @param activityTestRule the activity test rule used to run the test
+     * @param runner the runnable to run on the main thread. {@code null} is
+     * allowed, and simply means that there no runnable is required.
+     * @param forceLayout true if there should be an explicit call to requestLayout(),
+     * false otherwise
+     */
+    public static void runOnMainAndLayoutSync(@NonNull final ActivityTestRule activityTestRule,
+            @Nullable final Runnable runner, boolean forceLayout)
+            throws Throwable {
+        runOnMainAndLayoutSync(activityTestRule,
+                activityTestRule.getActivity().getWindow().getDecorView(), runner, forceLayout);
+    }
+
+    /**
+     * Runs the specified Runnable on the main thread and ensures that the specified view is
+     * laid out before returning.
+     *
+     * @param activityTestRule the activity test rule used to run the test
+     * @param view The view
+     * @param runner the runnable to run on the main thread. {@code null} is
+     * allowed, and simply means that there no runnable is required.
+     * @param forceLayout true if there should be an explicit call to requestLayout(),
+     * false otherwise
+     */
+    public static void runOnMainAndLayoutSync(@NonNull final ActivityTestRule activityTestRule,
+            @NonNull final View view, @Nullable final Runnable runner, boolean forceLayout)
+            throws Throwable {
+        final View rootView = view.getRootView();
+
+        final CountDownLatch latch = new CountDownLatch(1);
+
+        activityTestRule.runOnUiThread(() -> {
+            final OnGlobalLayoutListener listener = new ViewTreeObserver.OnGlobalLayoutListener() {
+                @Override
+                public void onGlobalLayout() {
+                    rootView.getViewTreeObserver().removeOnGlobalLayoutListener(this);
+                    // countdown immediately since the layout we were waiting on has happened
+                    latch.countDown();
+                }
+            };
+
+            rootView.getViewTreeObserver().addOnGlobalLayoutListener(listener);
+
+            if (runner != null) {
+                runner.run();
+            }
+
+            if (forceLayout) {
+                rootView.requestLayout();
+            }
+        });
+
+        try {
+            Assert.assertTrue("Expected layout pass within 5 seconds",
+                    latch.await(5, TimeUnit.SECONDS));
+        } catch (InterruptedException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+}
diff --git a/common/device-side/util-axt/src/com/android/compatibility/common/util/WifiConfigCreator.java b/common/device-side/util-axt/src/com/android/compatibility/common/util/WifiConfigCreator.java
new file mode 100755
index 0000000..19d843b
--- /dev/null
+++ b/common/device-side/util-axt/src/com/android/compatibility/common/util/WifiConfigCreator.java
@@ -0,0 +1,258 @@
+/*
+ * Copyright (C) 2015 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.compatibility.common.util;
+
+import static android.net.wifi.WifiManager.EXTRA_WIFI_STATE;
+import static android.net.wifi.WifiManager.WIFI_STATE_ENABLED;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.net.ProxyInfo;
+import android.net.Uri;
+import android.net.wifi.WifiConfiguration;
+import android.net.wifi.WifiManager;
+import android.text.TextUtils;
+import android.util.Log;
+
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * A simple activity to create and manage wifi configurations.
+ */
+public class WifiConfigCreator {
+    public static final String ACTION_CREATE_WIFI_CONFIG =
+            "com.android.compatibility.common.util.CREATE_WIFI_CONFIG";
+    public static final String ACTION_UPDATE_WIFI_CONFIG =
+            "com.android.compatibility.common.util.UPDATE_WIFI_CONFIG";
+    public static final String ACTION_REMOVE_WIFI_CONFIG =
+            "com.android.compatibility.common.util.REMOVE_WIFI_CONFIG";
+    public static final String EXTRA_NETID = "extra-netid";
+    public static final String EXTRA_SSID = "extra-ssid";
+    public static final String EXTRA_SECURITY_TYPE = "extra-security-type";
+    public static final String EXTRA_PASSWORD = "extra-password";
+
+    public static final int SECURITY_TYPE_NONE = 1;
+    public static final int SECURITY_TYPE_WPA = 2;
+    public static final int SECURITY_TYPE_WEP = 3;
+
+    private static final String TAG = "WifiConfigCreator";
+
+    private static final long ENABLE_WIFI_WAIT_SEC = 10L;
+
+    private final Context mContext;
+    private final WifiManager mWifiManager;
+
+    public WifiConfigCreator(Context context) {
+        mContext = context;
+        mWifiManager = (WifiManager) context.getSystemService(Context.WIFI_SERVICE);
+    }
+
+    /**
+     * Adds a new WiFi network.
+     * @return network id or -1 in case of error
+     */
+    public int addNetwork(String ssid, boolean hidden, int securityType,
+            String password) throws InterruptedException, SecurityException {
+        checkAndEnableWifi();
+
+        WifiConfiguration wifiConf = createConfig(ssid, hidden, securityType, password);
+
+        int netId = mWifiManager.addNetwork(wifiConf);
+
+        if (netId != -1) {
+            mWifiManager.enableNetwork(netId, true);
+        } else {
+            Log.w(TAG, "Unable to add SSID '" + ssid + "': netId = " + netId);
+        }
+        return netId;
+    }
+
+    /**
+     * Adds a new wifiConfiguration with OPEN security type, and the given pacProxy
+     * verifies that the proxy is added by getting the configuration back, and checking it.
+     * @return returns the PAC proxy URL after adding the network and getting it from WifiManager
+     * @throws IllegalStateException if any of the WifiManager operations fail
+     */
+    public String addHttpProxyNetworkVerifyAndRemove(String ssid, String pacProxyUrl)
+            throws IllegalStateException {
+        String retrievedPacProxyUrl = null;
+        int netId = -1;
+        try {
+            WifiConfiguration conf = createConfig(ssid, false, SECURITY_TYPE_NONE, null);
+            if (pacProxyUrl != null) {
+                conf.setHttpProxy(ProxyInfo.buildPacProxy(Uri.parse(pacProxyUrl)));
+            }
+            netId = mWifiManager.addNetwork(conf);
+            if (netId == -1) {
+                throw new IllegalStateException("Failed to addNetwork: " + ssid);
+            }
+            for (final WifiConfiguration w : mWifiManager.getConfiguredNetworks()) {
+                if (w.SSID.equals(ssid)) {
+                    conf = w;
+                    break;
+                }
+            }
+            if (conf == null) {
+                throw new IllegalStateException("Failed to get WifiConfiguration for: " + ssid);
+            }
+            Uri pacProxyFileUri = null;
+            ProxyInfo httpProxy = conf.getHttpProxy();
+            if (httpProxy != null) pacProxyFileUri = httpProxy.getPacFileUrl();
+            if (pacProxyFileUri != null) {
+                retrievedPacProxyUrl = conf.getHttpProxy().getPacFileUrl().toString();
+            }
+            if (!mWifiManager.removeNetwork(netId)) {
+                throw new IllegalStateException("Failed to remove WifiConfiguration: " + ssid);
+            }
+        } finally {
+            mWifiManager.removeNetwork(netId);
+        }
+        return retrievedPacProxyUrl;
+    }
+
+    /**
+     * Updates a new WiFi network.
+     * @return network id (may differ from original) or -1 in case of error
+     */
+    public int updateNetwork(WifiConfiguration wifiConf, String ssid, boolean hidden,
+            int securityType, String password) throws InterruptedException, SecurityException {
+        checkAndEnableWifi();
+        if (wifiConf == null) {
+            return -1;
+        }
+
+        WifiConfiguration conf = createConfig(ssid, hidden, securityType, password);
+        conf.networkId = wifiConf.networkId;
+
+        int newNetId = mWifiManager.updateNetwork(conf);
+
+        if (newNetId != -1) {
+            mWifiManager.saveConfiguration();
+            mWifiManager.enableNetwork(newNetId, true);
+        } else {
+            Log.w(TAG, "Unable to update SSID '" + ssid + "': netId = " + newNetId);
+        }
+        return newNetId;
+    }
+
+    /**
+     * Updates a new WiFi network.
+     * @return network id (may differ from original) or -1 in case of error
+     */
+    public int updateNetwork(int netId, String ssid, boolean hidden,
+            int securityType, String password) throws InterruptedException, SecurityException {
+        checkAndEnableWifi();
+
+        WifiConfiguration wifiConf = null;
+        List<WifiConfiguration> configs = mWifiManager.getConfiguredNetworks();
+        for (WifiConfiguration config : configs) {
+            if (config.networkId == netId) {
+                wifiConf = config;
+                break;
+            }
+        }
+        return updateNetwork(wifiConf, ssid, hidden, securityType, password);
+    }
+
+    public boolean removeNetwork(int netId) {
+        return mWifiManager.removeNetwork(netId);
+    }
+
+    /**
+     * Creates a WifiConfiguration set up according to given parameters
+     * @param ssid SSID of the network
+     * @param hidden Is SSID not broadcast?
+     * @param securityType One of {@link #SECURITY_TYPE_NONE}, {@link #SECURITY_TYPE_WPA} or
+     *                     {@link #SECURITY_TYPE_WEP}
+     * @param password Password for WPA or WEP
+     * @return Created configuration object
+     */
+    private WifiConfiguration createConfig(String ssid, boolean hidden, int securityType,
+            String password) {
+        WifiConfiguration wifiConf = new WifiConfiguration();
+        if (!TextUtils.isEmpty(ssid)) {
+            wifiConf.SSID = '"' + ssid + '"';
+        }
+        wifiConf.status = WifiConfiguration.Status.ENABLED;
+        wifiConf.hiddenSSID = hidden;
+        switch (securityType) {
+            case SECURITY_TYPE_NONE:
+                wifiConf.allowedKeyManagement.set(WifiConfiguration.KeyMgmt.NONE);
+                break;
+            case SECURITY_TYPE_WPA:
+                updateForWPAConfiguration(wifiConf, password);
+                break;
+            case SECURITY_TYPE_WEP:
+                updateForWEPConfiguration(wifiConf, password);
+                break;
+        }
+        return wifiConf;
+    }
+
+    private void updateForWPAConfiguration(WifiConfiguration wifiConf, String wifiPassword) {
+        wifiConf.allowedKeyManagement.set(WifiConfiguration.KeyMgmt.WPA_PSK);
+        if (!TextUtils.isEmpty(wifiPassword)) {
+            wifiConf.preSharedKey = '"' + wifiPassword + '"';
+        }
+    }
+
+    private void updateForWEPConfiguration(WifiConfiguration wifiConf, String password) {
+        wifiConf.allowedKeyManagement.set(WifiConfiguration.KeyMgmt.NONE);
+        wifiConf.allowedAuthAlgorithms.set(WifiConfiguration.AuthAlgorithm.OPEN);
+        wifiConf.allowedAuthAlgorithms.set(WifiConfiguration.AuthAlgorithm.SHARED);
+        if (!TextUtils.isEmpty(password)) {
+            int length = password.length();
+            if ((length == 10 || length == 26
+                    || length == 58) && password.matches("[0-9A-Fa-f]*")) {
+                wifiConf.wepKeys[0] = password;
+            } else {
+                wifiConf.wepKeys[0] = '"' + password + '"';
+            }
+            wifiConf.wepTxKeyIndex = 0;
+        }
+    }
+
+    private void checkAndEnableWifi() throws InterruptedException {
+        final CountDownLatch enabledLatch = new CountDownLatch(1);
+
+        // Register a change receiver first to pick up events between isEnabled and setEnabled
+        final BroadcastReceiver watcher = new BroadcastReceiver() {
+            @Override
+            public void onReceive(Context context, Intent intent) {
+                if (intent.getIntExtra(EXTRA_WIFI_STATE, -1) == WIFI_STATE_ENABLED) {
+                    enabledLatch.countDown();
+                }
+            }
+        };
+
+        mContext.registerReceiver(watcher, new IntentFilter(WifiManager.WIFI_STATE_CHANGED_ACTION));
+        try {
+            // In case wifi is not already enabled, wait for it to come up
+            if (!mWifiManager.isWifiEnabled()) {
+                mWifiManager.setWifiEnabled(true);
+                enabledLatch.await(ENABLE_WIFI_WAIT_SEC, TimeUnit.SECONDS);
+            }
+        } finally {
+            mContext.unregisterReceiver(watcher);
+        }
+    }
+}
+
diff --git a/common/device-side/util-axt/src/com/android/compatibility/common/util/Within.java b/common/device-side/util-axt/src/com/android/compatibility/common/util/Within.java
new file mode 100644
index 0000000..4d9ff80
--- /dev/null
+++ b/common/device-side/util-axt/src/com/android/compatibility/common/util/Within.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.compatibility.common.util;
+
+import android.os.SystemClock;
+
+import org.mockito.Mockito;
+import org.mockito.exceptions.base.MockitoAssertionError;
+import org.mockito.internal.verification.api.VerificationData;
+import org.mockito.invocation.Invocation;
+import org.mockito.verification.VerificationMode;
+
+import java.util.List;
+
+/**
+ * Custom verification mode that allows waiting for the specific invocation to happen within
+ * a certain time interval. Not that unlike {@link Mockito#timeout(int)}, this mode will not
+ * return early and throw exception if the expected method was called with a different set of
+ * parameters before the call that we're waiting for.
+ */
+public class Within implements VerificationMode {
+    private static final long TIME_SLICE = 50;
+    private final long mTimeout;
+
+    public Within(long timeout) {
+        mTimeout = timeout;
+    }
+
+    @Override
+    public void verify(VerificationData data) {
+        long timeout = mTimeout;
+        MockitoAssertionError errorToRethrow = null;
+        // Loop in the same way we do in PollingCheck, sleeping and then testing for the target
+        // invocation
+        while (timeout > 0) {
+            SystemClock.sleep(TIME_SLICE);
+
+            try {
+                final List<Invocation> actualInvocations = data.getAllInvocations();
+                // Iterate over all invocations so far to see if we have a match
+                for (Invocation invocation : actualInvocations) {
+                    if (data.getWanted().matches(invocation)) {
+                        // Found our match within our timeout. Mark all invocations as verified
+                        markAllInvocationsAsVerified(data);
+                        // and return
+                        return;
+                    }
+                }
+            } catch (MockitoAssertionError assertionError) {
+                errorToRethrow = assertionError;
+            }
+
+            timeout -= TIME_SLICE;
+        }
+
+        if (errorToRethrow != null) {
+            throw errorToRethrow;
+        }
+
+        throw new MockitoAssertionError(
+                "Timed out while waiting " + mTimeout + "ms for " + data.getWanted().toString());
+    }
+
+    // TODO: Uncomment once upgraded to 2.7.13
+    // @Override
+    public VerificationMode description(String description) {
+        // Return this for now.
+        // TODO: Return wrapper once upgraded to 2.7.13
+        return this;
+    }
+
+    private void markAllInvocationsAsVerified(VerificationData data) {
+        for (Invocation invocation : data.getAllInvocations()) {
+            invocation.markVerified();
+            data.getWanted().captureArgumentsFrom(invocation);
+        }
+    }
+}
diff --git a/common/device-side/util-axt/src/com/android/compatibility/common/util/devicepolicy/provisioning/IBooleanCallback.aidl b/common/device-side/util-axt/src/com/android/compatibility/common/util/devicepolicy/provisioning/IBooleanCallback.aidl
new file mode 100644
index 0000000..2fdb26b
--- /dev/null
+++ b/common/device-side/util-axt/src/com/android/compatibility/common/util/devicepolicy/provisioning/IBooleanCallback.aidl
@@ -0,0 +1,20 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.compatibility.common.util.devicepolicy.provisioning;
+
+interface IBooleanCallback {
+    oneway void onResult(boolean result);
+}
\ No newline at end of file
diff --git a/common/device-side/util-axt/src/com/android/compatibility/common/util/devicepolicy/provisioning/SilentProvisioningTestManager.java b/common/device-side/util-axt/src/com/android/compatibility/common/util/devicepolicy/provisioning/SilentProvisioningTestManager.java
new file mode 100644
index 0000000..ae6849c
--- /dev/null
+++ b/common/device-side/util-axt/src/com/android/compatibility/common/util/devicepolicy/provisioning/SilentProvisioningTestManager.java
@@ -0,0 +1,179 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.compatibility.common.util.devicepolicy.provisioning;
+
+import static android.app.admin.DevicePolicyManager.ACTION_MANAGED_PROFILE_PROVISIONED;
+import static android.app.admin.DevicePolicyManager.ACTION_PROVISION_MANAGED_PROFILE;
+import static android.content.Intent.ACTION_MANAGED_PROFILE_ADDED;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.os.Bundle;
+import android.os.RemoteException;
+import androidx.test.InstrumentationRegistry;
+import android.support.test.uiautomator.UiDevice;
+import android.util.Log;
+
+import com.android.compatibility.common.util.BlockingBroadcastReceiver;
+
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.TimeUnit;
+
+public class SilentProvisioningTestManager {
+    private static final long TIMEOUT_SECONDS = 120L;
+    private static final String TAG = "SilentProvisioningTest";
+
+    private final LinkedBlockingQueue<Boolean> mProvisioningResults = new LinkedBlockingQueue(1);
+
+    private final IBooleanCallback mProvisioningResultCallback = new IBooleanCallback.Stub() {
+        @Override
+        public void onResult(boolean result) {
+            try {
+                mProvisioningResults.put(result);
+            } catch (InterruptedException e) {
+                Log.e(TAG, "IBooleanCallback.callback", e);
+            }
+        }
+    };
+
+    private final Context mContext;
+    private Intent mReceivedProfileProvisionedIntent;
+
+    public SilentProvisioningTestManager(Context context) {
+        mContext = context.getApplicationContext();
+    }
+
+    public Intent getReceviedProfileProvisionedIntent() {
+        return mReceivedProfileProvisionedIntent;
+    }
+
+    public boolean startProvisioningAndWait(Intent provisioningIntent) throws InterruptedException {
+        wakeUpAndDismissInsecureKeyguard();
+        mContext.startActivity(getStartIntent(provisioningIntent));
+        Log.i(TAG, "startActivity with intent: " + provisioningIntent);
+
+        if (ACTION_PROVISION_MANAGED_PROFILE.equals(provisioningIntent.getAction())) {
+            return waitManagedProfileProvisioning();
+        } else {
+            return waitDeviceOwnerProvisioning();
+        }
+    }
+
+    private boolean waitDeviceOwnerProvisioning() throws InterruptedException {
+        return pollProvisioningResult();
+    }
+
+    private boolean waitManagedProfileProvisioning() throws InterruptedException {
+        BlockingBroadcastReceiver managedProfileProvisionedReceiver =
+                new BlockingBroadcastReceiver(mContext, ACTION_MANAGED_PROFILE_PROVISIONED);
+        BlockingBroadcastReceiver managedProfileAddedReceiver =
+                new BlockingBroadcastReceiver(mContext, ACTION_MANAGED_PROFILE_ADDED);
+        try {
+            managedProfileProvisionedReceiver.register();
+            managedProfileAddedReceiver.register();
+
+            if (!pollProvisioningResult()) {
+                return false;
+            }
+
+            mReceivedProfileProvisionedIntent =
+                    managedProfileProvisionedReceiver.awaitForBroadcast();
+            if (mReceivedProfileProvisionedIntent == null) {
+                Log.i(TAG, "managedProfileProvisionedReceiver.awaitForBroadcast(): failed");
+                return false;
+            }
+
+            if (managedProfileAddedReceiver.awaitForBroadcast() == null) {
+                Log.i(TAG, "managedProfileAddedReceiver.awaitForBroadcast(): failed");
+                return false;
+            }
+        } finally {
+            managedProfileProvisionedReceiver.unregisterQuietly();
+            managedProfileAddedReceiver.unregisterQuietly();
+        }
+        return true;
+    }
+
+    private boolean pollProvisioningResult() throws InterruptedException {
+        Boolean result = mProvisioningResults.poll(TIMEOUT_SECONDS, TimeUnit.SECONDS);
+        if (result == null) {
+            Log.i(TAG, "ManagedProvisioning doesn't return result within "
+                    + TIMEOUT_SECONDS + " seconds ");
+            return false;
+        }
+
+        if (!result) {
+            Log.i(TAG, "Failed to provision");
+            return false;
+        }
+        return true;
+    }
+
+    private Intent getStartIntent(Intent intent) {
+        final Bundle bundle = new Bundle();
+        bundle.putParcelable(Intent.EXTRA_INTENT, intent);
+        bundle.putBinder(StartProvisioningActivity.EXTRA_BOOLEAN_CALLBACK,
+                mProvisioningResultCallback.asBinder());
+        return new Intent(mContext, StartProvisioningActivity.class)
+                .putExtras(bundle)
+                .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+    }
+
+    private static void wakeUpAndDismissInsecureKeyguard() {
+        try {
+            UiDevice uiDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());
+            uiDevice.wakeUp();
+            uiDevice.pressMenu();
+        } catch (RemoteException e) {
+            Log.e(TAG, "wakeUpScreen", e);
+        }
+    }
+
+    private static class BlockingReceiver extends BroadcastReceiver {
+
+        private final CountDownLatch mLatch = new CountDownLatch(1);
+        private final Context mContext;
+        private final String mAction;
+        private Intent mReceivedIntent;
+
+        private BlockingReceiver(Context context, String action) {
+            mContext = context;
+            mAction = action;
+            mReceivedIntent = null;
+        }
+
+        public void register() {
+            mContext.registerReceiver(this, new IntentFilter(mAction));
+        }
+
+        public boolean await() throws InterruptedException {
+            return mLatch.await(TIMEOUT_SECONDS, TimeUnit.SECONDS);
+        }
+
+        public Intent getReceivedIntent() {
+            return mReceivedIntent;
+        }
+
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            mReceivedIntent = intent;
+            mLatch.countDown();
+        }
+    }
+}
diff --git a/common/device-side/util-axt/src/com/android/compatibility/common/util/devicepolicy/provisioning/StartProvisioningActivity.java b/common/device-side/util-axt/src/com/android/compatibility/common/util/devicepolicy/provisioning/StartProvisioningActivity.java
new file mode 100644
index 0000000..4a98794
--- /dev/null
+++ b/common/device-side/util-axt/src/com/android/compatibility/common/util/devicepolicy/provisioning/StartProvisioningActivity.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.compatibility.common.util.devicepolicy.provisioning;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.os.Bundle;
+import android.os.RemoteException;
+import android.util.Log;
+import android.view.WindowManager;
+
+/**
+ * Must register it in AndroidManifest.xml
+ * <activity android:name="com.android.compatibility.common.util.devicepolicy.provisioning.StartProvisioningActivity"></activity>
+ */
+public class StartProvisioningActivity extends Activity {
+    private static final int REQUEST_CODE = 1;
+    private static final String TAG = "StartProvisionActivity";
+
+    public static final String EXTRA_BOOLEAN_CALLBACK = "EXTRA_BOOLEAN_CALLBACK";
+
+    IBooleanCallback mResultCallback;
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+
+        // Reduce flakiness of the test
+        // Show activity on top of keyguard
+        getWindow().addFlags(WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD);
+        // Turn on screen to prevent activity being paused by system
+        getWindow().addFlags(WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON);
+        getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
+
+        mResultCallback = IBooleanCallback.Stub.asInterface(
+                getIntent().getExtras().getBinder(EXTRA_BOOLEAN_CALLBACK));
+        Log.i(TAG, "result callback class name " + mResultCallback);
+
+        // Only provision it if the activity is not re-created
+        if (savedInstanceState == null) {
+            Intent provisioningIntent = getIntent().getParcelableExtra(Intent.EXTRA_INTENT);
+
+            startActivityForResult(provisioningIntent, REQUEST_CODE);
+            Log.i(TAG, "Start provisioning intent");
+        }
+    }
+
+    @Override
+    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+        if (requestCode == REQUEST_CODE) {
+            try {
+                boolean result = resultCode == RESULT_OK;
+                mResultCallback.onResult(result);
+                Log.i(TAG, "onActivityResult result: " + result);
+            } catch (RemoteException e) {
+                Log.e(TAG, "onActivityResult", e);
+            }
+        } else {
+            super.onActivityResult(requestCode, resultCode, data);
+        }
+    }
+}
\ No newline at end of file
diff --git a/common/device-side/util-axt/src/com/android/compatibility/common/util/transition/TargetTracking.java b/common/device-side/util-axt/src/com/android/compatibility/common/util/transition/TargetTracking.java
new file mode 100644
index 0000000..7c53921
--- /dev/null
+++ b/common/device-side/util-axt/src/com/android/compatibility/common/util/transition/TargetTracking.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.compatibility.common.util.transition;
+
+import android.graphics.Rect;
+import android.view.View;
+
+import java.util.ArrayList;
+
+public interface TargetTracking {
+    ArrayList<View> getTrackedTargets();
+    void clearTargets();
+    Rect getCapturedEpicenter();
+}
diff --git a/common/device-side/util-axt/src/com/android/compatibility/common/util/transition/TrackingTransition.java b/common/device-side/util-axt/src/com/android/compatibility/common/util/transition/TrackingTransition.java
new file mode 100644
index 0000000..55b235d
--- /dev/null
+++ b/common/device-side/util-axt/src/com/android/compatibility/common/util/transition/TrackingTransition.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.compatibility.common.util.transition;
+
+import android.animation.Animator;
+import android.graphics.Rect;
+import android.transition.Transition;
+import android.transition.TransitionValues;
+import android.view.View;
+import android.view.ViewGroup;
+
+import java.util.ArrayList;
+
+/**
+ * A transition that tracks which targets are applied to it.
+ * It will assume any target that it applies to will have differences
+ * between the start and end state, regardless of the differences
+ * that actually exist. In other words, it doesn't actually check
+ * any size or position differences or any other property of the view.
+ * It just records the difference.
+ * <p>
+ * Both start and end value Views are recorded, but no actual animation
+ * is created.
+ */
+public class TrackingTransition extends Transition implements TargetTracking {
+    public final ArrayList<View> targets = new ArrayList<>();
+    private final Rect[] mEpicenter = new Rect[1];
+    private static String PROP = "tracking:prop";
+    private static String[] PROPS = { PROP };
+
+    @Override
+    public String[] getTransitionProperties() {
+        return PROPS;
+    }
+
+    @Override
+    public void captureStartValues(TransitionValues transitionValues) {
+        transitionValues.values.put(PROP, 0);
+    }
+
+    @Override
+    public void captureEndValues(TransitionValues transitionValues) {
+        transitionValues.values.put(PROP, 1);
+    }
+
+    @Override
+    public Animator createAnimator(ViewGroup sceneRoot, TransitionValues startValues,
+            TransitionValues endValues) {
+        if (startValues != null) {
+            targets.add(startValues.view);
+        }
+        if (endValues != null) {
+            targets.add(endValues.view);
+        }
+        Rect epicenter = getEpicenter();
+        if (epicenter != null) {
+            mEpicenter[0] = new Rect(epicenter);
+        } else {
+            mEpicenter[0] = null;
+        }
+        return null;
+    }
+
+    @Override
+    public ArrayList<View> getTrackedTargets() {
+        return targets;
+    }
+
+    @Override
+    public void clearTargets() {
+        targets.clear();
+    }
+
+    @Override
+    public Rect getCapturedEpicenter() {
+        return mEpicenter[0];
+    }
+}
diff --git a/common/device-side/util-axt/src/com/android/compatibility/common/util/transition/TrackingVisibility.java b/common/device-side/util-axt/src/com/android/compatibility/common/util/transition/TrackingVisibility.java
new file mode 100644
index 0000000..8a5a19e
--- /dev/null
+++ b/common/device-side/util-axt/src/com/android/compatibility/common/util/transition/TrackingVisibility.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.compatibility.common.util.transition;
+
+import android.animation.Animator;
+import android.graphics.Rect;
+import android.transition.TransitionValues;
+import android.transition.Visibility;
+import android.view.View;
+import android.view.ViewGroup;
+
+import java.util.ArrayList;
+
+/**
+ * Visibility transition that tracks which targets are applied to it.
+ * This transition does no animation.
+ */
+public class TrackingVisibility extends Visibility implements TargetTracking {
+    public final ArrayList<View> targets = new ArrayList<>();
+    private final Rect[] mEpicenter = new Rect[1];
+
+    @Override
+    public Animator onAppear(ViewGroup sceneRoot, View view, TransitionValues startValues,
+            TransitionValues endValues) {
+        targets.add(endValues.view);
+        Rect epicenter = getEpicenter();
+        if (epicenter != null) {
+            mEpicenter[0] = new Rect(epicenter);
+        } else {
+            mEpicenter[0] = null;
+        }
+        return null;
+    }
+
+    @Override
+    public Animator onDisappear(ViewGroup sceneRoot, View view, TransitionValues startValues,
+            TransitionValues endValues) {
+        targets.add(startValues.view);
+        Rect epicenter = getEpicenter();
+        if (epicenter != null) {
+            mEpicenter[0] = new Rect(epicenter);
+        } else {
+            mEpicenter[0] = null;
+        }
+        return null;
+    }
+
+    @Override
+    public ArrayList<View> getTrackedTargets() {
+        return targets;
+    }
+
+    @Override
+    public void clearTargets() {
+        targets.clear();
+    }
+
+    @Override
+    public Rect getCapturedEpicenter() {
+        return mEpicenter[0];
+    }
+}
diff --git a/common/device-side/util-axt/tests/Android.mk b/common/device-side/util-axt/tests/Android.mk
new file mode 100644
index 0000000..a4681de
--- /dev/null
+++ b/common/device-side/util-axt/tests/Android.mk
@@ -0,0 +1,29 @@
+# Copyright (C) 2015 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.
+
+LOCAL_PATH:= $(call my-dir)
+
+include $(CLEAR_VARS)
+
+LOCAL_SRC_FILES := $(call all-java-files-under, src)
+
+LOCAL_STATIC_JAVA_LIBRARIES := compatibility-device-util-axt junit
+
+LOCAL_MODULE_TAGS := optional
+
+LOCAL_MODULE := compatibility-device-util-axt-tests
+
+LOCAL_SDK_VERSION := current
+
+include $(BUILD_STATIC_JAVA_LIBRARY)
diff --git a/common/device-side/util-axt/tests/src/com/android/compatibility/common/util/ApiLevelUtilTest.java b/common/device-side/util-axt/tests/src/com/android/compatibility/common/util/ApiLevelUtilTest.java
new file mode 100644
index 0000000..3b0f0de
--- /dev/null
+++ b/common/device-side/util-axt/tests/src/com/android/compatibility/common/util/ApiLevelUtilTest.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+package com.android.compatibility.common.util;
+
+import static junit.framework.Assert.assertEquals;
+import static junit.framework.Assert.assertFalse;
+import static junit.framework.Assert.assertTrue;
+
+import android.os.Build;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/**
+ * Tests for {@line ApiLevelUtil}.
+ */
+@RunWith(AndroidJUnit4.class)
+public class ApiLevelUtilTest {
+
+    @Test
+    public void testComparisonByInt() throws Exception {
+        int version = Build.VERSION.SDK_INT;
+
+        assertFalse(ApiLevelUtil.isBefore(version - 1));
+        assertFalse(ApiLevelUtil.isBefore(version));
+        assertTrue(ApiLevelUtil.isBefore(version + 1));
+
+        assertTrue(ApiLevelUtil.isAfter(version - 1));
+        assertFalse(ApiLevelUtil.isAfter(version));
+        assertFalse(ApiLevelUtil.isAfter(version + 1));
+
+        assertTrue(ApiLevelUtil.isAtLeast(version - 1));
+        assertTrue(ApiLevelUtil.isAtLeast(version));
+        assertFalse(ApiLevelUtil.isAtLeast(version + 1));
+
+        assertFalse(ApiLevelUtil.isAtMost(version - 1));
+        assertTrue(ApiLevelUtil.isAtMost(version));
+        assertTrue(ApiLevelUtil.isAtMost(version + 1));
+    }
+
+    @Test
+    public void testComparisonByString() throws Exception {
+        // test should pass as long as device SDK version is at least 12
+        assertTrue(ApiLevelUtil.isAtLeast("HONEYCOMB_MR1"));
+        assertTrue(ApiLevelUtil.isAtLeast("12"));
+    }
+
+    @Test
+    public void testResolveVersionString() throws Exception {
+        // can only test versions known to the device build
+        assertEquals(ApiLevelUtil.resolveVersionString("GINGERBREAD_MR1"), 10);
+        assertEquals(ApiLevelUtil.resolveVersionString("10"), 10);
+        assertEquals(ApiLevelUtil.resolveVersionString("HONEYCOMB"), 11);
+        assertEquals(ApiLevelUtil.resolveVersionString("11"), 11);
+        assertEquals(ApiLevelUtil.resolveVersionString("honeycomb_mr1"), 12);
+        assertEquals(ApiLevelUtil.resolveVersionString("12"), 12);
+    }
+
+    @Test(expected = RuntimeException.class)
+    public void testResolveMisspelledVersionString() throws Exception {
+        ApiLevelUtil.resolveVersionString("GINGERBEARD");
+    }
+}
diff --git a/common/device-side/util-axt/tests/src/com/android/compatibility/common/util/BusinessLogicDeviceExecutorTest.java b/common/device-side/util-axt/tests/src/com/android/compatibility/common/util/BusinessLogicDeviceExecutorTest.java
new file mode 100644
index 0000000..e8d5d29
--- /dev/null
+++ b/common/device-side/util-axt/tests/src/com/android/compatibility/common/util/BusinessLogicDeviceExecutorTest.java
@@ -0,0 +1,314 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+package com.android.compatibility.common.util;
+
+import static junit.framework.Assert.assertEquals;
+import static junit.framework.Assert.assertFalse;
+import static junit.framework.Assert.assertNull;
+import static junit.framework.Assert.assertTrue;
+import static junit.framework.Assert.fail;
+import static org.junit.Assume.assumeTrue;
+
+import android.content.Context;
+import androidx.test.InstrumentationRegistry;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.AssumptionViolatedException;
+import org.junit.Before;
+import org.junit.Ignore;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+import junit.framework.AssertionFailedError;
+
+/**
+ * Tests for {@line BusinessLogicDeviceExecutor}.
+ */
+@RunWith(AndroidJUnit4.class)
+public class BusinessLogicDeviceExecutorTest {
+
+    private static final String THIS_CLASS =
+            "com.android.compatibility.common.util.BusinessLogicDeviceExecutorTest";
+    private static final String METHOD_1 = THIS_CLASS + ".method1";
+    private static final String METHOD_2 = THIS_CLASS + ".method2";
+    private static final String METHOD_3 = THIS_CLASS + ".method3";
+    private static final String METHOD_4 = THIS_CLASS + ".method4";
+    private static final String METHOD_5 = THIS_CLASS + ".method5";
+    private static final String METHOD_6 = THIS_CLASS + ".method6";
+    private static final String METHOD_7 = THIS_CLASS + ".method7";
+    private static final String METHOD_8 = THIS_CLASS + ".method8";
+    private static final String METHOD_9 = THIS_CLASS + ".method9";
+    private static final String METHOD_10 = THIS_CLASS + ".method10";
+    private static final String FAKE_METHOD = THIS_CLASS + ".methodDoesntExist";
+    private static final String ARG_STRING_1 = "arg1";
+    private static final String ARG_STRING_2 = "arg2";
+
+    private static final String OTHER_METHOD_1 = THIS_CLASS + "$OtherClass.method1";
+
+    private String mInvoked = null;
+    private Object[] mArgsUsed = null;
+    private Context mContext;
+    private BusinessLogicExecutor mExecutor;
+
+    @Before
+    public void setUp() {
+        mContext = InstrumentationRegistry.getTargetContext();
+        mExecutor = new BusinessLogicDeviceExecutor(mContext, this);
+        // reset the instance variables tracking the method invoked and the args used
+        mInvoked = null;
+        mArgsUsed = null;
+        // reset the OtherClass class variable tracking the method invoked
+        OtherClass.otherInvoked = null;
+    }
+
+    @Test
+    public void testInvokeMethodInThisClass() throws Exception {
+        mExecutor.invokeMethod(METHOD_1);
+        // assert that mInvoked was set for this BusinessLogicDeviceExecutorTest instance
+        assertEquals("Failed to invoke method in this class", mInvoked, METHOD_1);
+    }
+
+    @Test
+    public void testInvokeMethodInOtherClass() throws Exception {
+        mExecutor.invokeMethod(OTHER_METHOD_1);
+        // assert that OtherClass.method1 was invoked, and static field of OtherClass was changed
+        assertEquals("Failed to invoke method in other class", OtherClass.otherInvoked,
+                OTHER_METHOD_1);
+    }
+
+    @Test
+    public void testInvokeMethodWithStringArgs() throws Exception {
+        mExecutor.invokeMethod(METHOD_2, ARG_STRING_1, ARG_STRING_2);
+        assertEquals("Failed to invoke method in this class", mInvoked, METHOD_2);
+        // assert both String arguments were correctly set for method2
+        assertEquals("Failed to set first argument", mArgsUsed[0], ARG_STRING_1);
+        assertEquals("Failed to set second argument", mArgsUsed[1], ARG_STRING_2);
+    }
+
+    @Test
+    public void testInvokeMethodWithStringAndContextArgs() throws Exception {
+        mExecutor.invokeMethod(METHOD_3, ARG_STRING_1);
+        assertEquals("Failed to invoke method in this class", mInvoked, METHOD_3);
+        // assert that String arg and Context arg were correctly set for method3
+        assertEquals("Failed to set first argument", mArgsUsed[0], ARG_STRING_1);
+        assertEquals("Failed to set second argument", mArgsUsed[1], mContext);
+    }
+
+    @Test
+    public void testInvokeMethodWithContextAndStringArgs() throws Exception {
+        mExecutor.invokeMethod(METHOD_4, ARG_STRING_1);
+        assertEquals("Failed to invoke method in this class", mInvoked, METHOD_4);
+        // Like testInvokeMethodWithStringAndContextArgs, but flip the args for method4
+        assertEquals("Failed to set first argument", mArgsUsed[0], mContext);
+        assertEquals("Failed to set second argument", mArgsUsed[1], ARG_STRING_1);
+    }
+
+    @Test
+    public void testInvokeMethodWithStringArrayArg() throws Exception {
+        mExecutor.invokeMethod(METHOD_5, ARG_STRING_1, ARG_STRING_2);
+        assertEquals("Failed to invoke method in this class", mInvoked, METHOD_5);
+        // assert both String arguments were correctly set for method5
+        assertEquals("Failed to set first argument", mArgsUsed[0], ARG_STRING_1);
+        assertEquals("Failed to set second argument", mArgsUsed[1], ARG_STRING_2);
+    }
+
+    @Test
+    public void testInvokeMethodWithEmptyStringArrayArg() throws Exception {
+        mExecutor.invokeMethod(METHOD_5);
+        assertEquals("Failed to invoke method in this class", mInvoked, METHOD_5);
+        // assert no String arguments were set for method5
+        assertEquals("Incorrectly set args", mArgsUsed.length, 0);
+    }
+
+    @Test
+    public void testInvokeMethodWithStringAndStringArrayArgs() throws Exception {
+        mExecutor.invokeMethod(METHOD_6, ARG_STRING_1, ARG_STRING_2);
+        assertEquals("Failed to invoke method in this class", mInvoked, METHOD_6);
+        // assert both String arguments were correctly set for method6
+        assertEquals("Failed to set first argument", mArgsUsed[0], ARG_STRING_1);
+        assertEquals("Failed to set second argument", mArgsUsed[1], ARG_STRING_2);
+    }
+
+    @Test
+    public void testInvokeMethodWithAllArgTypes() throws Exception {
+        mExecutor.invokeMethod(METHOD_7, ARG_STRING_1, ARG_STRING_2);
+        assertEquals("Failed to invoke method in this class", mInvoked, METHOD_7);
+        // assert all arguments were correctly set for method7
+        assertEquals("Failed to set first argument", mArgsUsed[0], ARG_STRING_1);
+        assertEquals("Failed to set second argument", mArgsUsed[1], mContext);
+        assertEquals("Failed to set third argument", mArgsUsed[2], ARG_STRING_2);
+    }
+
+    @Test
+    public void testInvokeOverloadedMethodOneArg() throws Exception {
+        mExecutor.invokeMethod(METHOD_1, ARG_STRING_1);
+        assertEquals("Failed to invoke method in this class", mInvoked, METHOD_1);
+        assertEquals("Set wrong number of arguments", mArgsUsed.length, 1);
+        assertEquals("Failed to set first argument", mArgsUsed[0], ARG_STRING_1);
+    }
+
+    @Test
+    public void testInvokeOverloadedMethodTwoArgs() throws Exception {
+        mExecutor.invokeMethod(METHOD_1, ARG_STRING_1, ARG_STRING_2);
+        assertEquals("Failed to invoke method in this class", mInvoked, METHOD_1);
+        assertEquals("Set wrong number of arguments", mArgsUsed.length, 2);
+        assertEquals("Failed to set first argument", mArgsUsed[0], ARG_STRING_1);
+        assertEquals("Failed to set second argument", mArgsUsed[1], ARG_STRING_2);
+    }
+
+    @Test(expected = RuntimeException.class)
+    public void testInvokeNonExistentMethod() throws Exception {
+        mExecutor.invokeMethod(FAKE_METHOD, ARG_STRING_1, ARG_STRING_2);
+    }
+
+    @Test(expected = RuntimeException.class)
+    public void testInvokeMethodTooManyArgs() throws Exception {
+        mExecutor.invokeMethod(METHOD_3, ARG_STRING_1, ARG_STRING_2);
+    }
+
+    @Test(expected = RuntimeException.class)
+    public void testInvokeMethodTooFewArgs() throws Exception {
+        mExecutor.invokeMethod(METHOD_2, ARG_STRING_1);
+    }
+
+    @Test(expected = RuntimeException.class)
+    public void testInvokeMethodIncompatibleArgs() throws Exception {
+        mExecutor.invokeMethod(METHOD_8, ARG_STRING_1);
+    }
+
+    @Test
+    public void testExecuteConditionCheckReturnValue() throws Exception {
+        assertTrue("Wrong return value",
+                mExecutor.executeCondition(METHOD_2, ARG_STRING_1, ARG_STRING_1));
+        assertFalse("Wrong return value",
+                mExecutor.executeCondition(METHOD_2, ARG_STRING_1, ARG_STRING_2));
+    }
+
+    @Test(expected = RuntimeException.class)
+    public void testExecuteInvalidCondition() throws Exception {
+        mExecutor.executeCondition(METHOD_1); // method1 does not return type boolean
+    }
+
+    @Test
+    public void testExecuteAction() throws Exception {
+        mExecutor.executeAction(METHOD_2, ARG_STRING_1, ARG_STRING_2);
+        assertEquals("Failed to invoke method in this class", mInvoked, METHOD_2);
+        // assert both String arguments were correctly set for method2
+        assertEquals("Failed to set first argument", mArgsUsed[0], ARG_STRING_1);
+        assertEquals("Failed to set second argument", mArgsUsed[1], ARG_STRING_2);
+    }
+
+    @Test(expected = RuntimeException.class)
+    public void testExecuteActionThrowException() throws Exception {
+        mExecutor.executeAction(METHOD_9);
+    }
+
+    @Test
+    public void testExecuteActionViolateAssumption() throws Exception {
+        try {
+            mExecutor.executeAction(METHOD_10);
+            // JUnit4 doesn't support expecting AssumptionViolatedException with "expected"
+            // attribute on @Test annotation, so test using Assert.fail()
+            fail("Expected assumption failure");
+        } catch (AssumptionViolatedException e) {
+            // expected
+        }
+    }
+
+    public void method1() {
+        mInvoked = METHOD_1;
+    }
+
+    // overloaded method with one arg
+    public void method1(String arg1) {
+        mInvoked = METHOD_1;
+        mArgsUsed = new Object[]{arg1};
+    }
+
+    // overloaded method with two args
+    public void method1(String arg1, String arg2) {
+        mInvoked = METHOD_1;
+        mArgsUsed = new Object[]{arg1, arg2};
+    }
+
+    public boolean method2(String arg1, String arg2) {
+        mInvoked = METHOD_2;
+        mArgsUsed = new Object[]{arg1, arg2};
+        return arg1.equals(arg2);
+    }
+
+    public void method3(String arg1, Context arg2) {
+        mInvoked = METHOD_3;
+        mArgsUsed = new Object[]{arg1, arg2};
+    }
+
+    // Same as method3, but flipped args
+    public void method4(Context arg1, String arg2) {
+        mInvoked = METHOD_4;
+        mArgsUsed = new Object[]{arg1, arg2};
+    }
+
+    public void method5(String... args) {
+        mInvoked = METHOD_5;
+        mArgsUsed = args;
+    }
+
+    public void method6(String arg1, String... moreArgs) {
+        mInvoked = METHOD_6;
+        List<String> allArgs = new ArrayList<>();
+        allArgs.add(arg1);
+        allArgs.addAll(Arrays.asList(moreArgs));
+        mArgsUsed = allArgs.toArray(new String[0]);
+    }
+
+    public void method7(String arg1, Context arg2, String... moreArgs) {
+        mInvoked = METHOD_7;
+        List<Object> allArgs = new ArrayList<>();
+        allArgs.add(arg1);
+        allArgs.add(arg2);
+        allArgs.addAll(Arrays.asList(moreArgs));
+        mArgsUsed = allArgs.toArray(new Object[0]);
+    }
+
+    public void method8(String arg1, Integer arg2) {
+        // This method should never be successfully invoked, since Integer parameter types are
+        // unsupported for the BusinessLogic service
+    }
+
+    // throw AssertionFailedError
+    @Ignore
+    public void method9() throws AssertionFailedError {
+        assertTrue(false);
+    }
+
+    // throw AssumptionViolatedException
+    public void method10() throws AssumptionViolatedException {
+        assumeTrue(false);
+    }
+
+    public static class OtherClass {
+
+        public static String otherInvoked = null;
+
+        public void method1() {
+            otherInvoked = OTHER_METHOD_1;
+        }
+    }
+}
diff --git a/common/device-side/util-axt/tests/src/com/android/compatibility/common/util/BusinessLogicTestCaseTest.java b/common/device-side/util-axt/tests/src/com/android/compatibility/common/util/BusinessLogicTestCaseTest.java
new file mode 100644
index 0000000..0e779e3
--- /dev/null
+++ b/common/device-side/util-axt/tests/src/com/android/compatibility/common/util/BusinessLogicTestCaseTest.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+package com.android.compatibility.common.util;
+
+import static junit.framework.Assert.assertEquals;
+import static junit.framework.Assert.assertTrue;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Tests for {@line BusinessLogicTestCase}.
+ */
+@RunWith(AndroidJUnit4.class)
+public class BusinessLogicTestCaseTest {
+
+    private static final String KEY_1 = "key1";
+    private static final String KEY_2 = "key2";
+    private static final String VALUE_1 = "value1";
+    private static final String VALUE_2 = "value2";
+
+    DummyTest mDummyTest;
+    DummyTest mOtherDummyTest;
+
+    @Before
+    public void setUp() {
+        mDummyTest = new DummyTest();
+        mOtherDummyTest = new DummyTest();
+    }
+
+    @Test
+    public void testMapPut() throws Exception {
+        mDummyTest.mapPut("instanceMap", KEY_1, VALUE_1);
+        assertTrue("mapPut failed for instanceMap", mDummyTest.instanceMap.containsKey(KEY_1));
+        assertEquals("mapPut failed for instanceMap", mDummyTest.instanceMap.get(KEY_1), VALUE_1);
+        assertTrue("mapPut affected wrong instance", mOtherDummyTest.instanceMap.isEmpty());
+    }
+
+    @Test
+    public void testStaticMapPut() throws Exception {
+        mDummyTest.mapPut("staticMap", KEY_2, VALUE_2);
+        assertTrue("mapPut failed for staticMap", mDummyTest.staticMap.containsKey(KEY_2));
+        assertEquals("mapPut failed for staticMap", mDummyTest.staticMap.get(KEY_2), VALUE_2);
+        assertTrue("mapPut on static map should affect all instances",
+                mOtherDummyTest.staticMap.containsKey(KEY_2));
+    }
+
+    public static class DummyTest extends BusinessLogicTestCase {
+        public Map<String, String> instanceMap = new HashMap<>();
+        public static Map<String, String> staticMap = new HashMap<>();
+    }
+}
diff --git a/common/device-side/util-axt/tests/src/com/android/compatibility/common/util/DeviceReportTest.java b/common/device-side/util-axt/tests/src/com/android/compatibility/common/util/DeviceReportTest.java
new file mode 100644
index 0000000..ab42b32
--- /dev/null
+++ b/common/device-side/util-axt/tests/src/com/android/compatibility/common/util/DeviceReportTest.java
@@ -0,0 +1,154 @@
+/*
+ * Copyright (C) 2015 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.compatibility.common.util;
+
+import android.app.Instrumentation;
+import android.os.Bundle;
+import android.os.Environment;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileReader;
+import java.lang.StringBuilder;
+
+import junit.framework.TestCase;
+
+import org.json.JSONObject;
+
+/**
+ * Tests for {@line DeviceReportLog}.
+ */
+public class DeviceReportTest extends TestCase {
+
+    /**
+     * A stub of {@link Instrumentation}
+     */
+    public class TestInstrumentation extends Instrumentation {
+
+        private int mResultCode = -1;
+        private Bundle mResults = null;
+
+        @Override
+        public void sendStatus(int resultCode, Bundle results) {
+            mResultCode = resultCode;
+            mResults = results;
+        }
+    }
+
+    private static final int RESULT_CODE = 2;
+    private static final String RESULT_KEY = "COMPATIBILITY_TEST_RESULT";
+    private static final String TEST_MESSAGE_1 = "Foo";
+    private static final double TEST_VALUE_1 = 3;
+    private static final ResultType TEST_TYPE_1 = ResultType.HIGHER_BETTER;
+    private static final ResultUnit TEST_UNIT_1 = ResultUnit.SCORE;
+    private static final String TEST_MESSAGE_2 = "Bar";
+    private static final double TEST_VALUE_2 = 5;
+    private static final ResultType TEST_TYPE_2 = ResultType.LOWER_BETTER;
+    private static final ResultUnit TEST_UNIT_2 = ResultUnit.COUNT;
+    private static final String TEST_MESSAGE_3 = "Sample";
+    private static final double TEST_VALUE_3 = 7;
+    private static final ResultType TEST_TYPE_3 = ResultType.LOWER_BETTER;
+    private static final ResultUnit TEST_UNIT_3 = ResultUnit.COUNT;
+    private static final String TEST_MESSAGE_4 = "Message";
+    private static final double TEST_VALUE_4 = 9;
+    private static final ResultType TEST_TYPE_4 = ResultType.LOWER_BETTER;
+    private static final ResultUnit TEST_UNIT_4 = ResultUnit.COUNT;
+    private static final String REPORT_NAME_1 = "TestReport1";
+    private static final String REPORT_NAME_2 = "TestReport2";
+    private static final String STREAM_NAME_1 = "SampleStream1";
+    private static final String STREAM_NAME_2 = "SampleStream2";
+    private static final String STREAM_NAME_3 = "SampleStream3";
+    private static final String STREAM_NAME_4 = "SampleStream4";
+
+    public void testSubmit() throws Exception {
+        DeviceReportLog log = new DeviceReportLog(REPORT_NAME_1, STREAM_NAME_1);
+        log.addValue(TEST_MESSAGE_1, TEST_VALUE_1, TEST_TYPE_1, TEST_UNIT_1);
+        log.setSummary(TEST_MESSAGE_2, TEST_VALUE_2, TEST_TYPE_2, TEST_UNIT_2);
+        TestInstrumentation inst = new TestInstrumentation();
+        log.submit(inst);
+        assertEquals("Incorrect result code", RESULT_CODE, inst.mResultCode);
+        assertNotNull("Bundle missing", inst.mResults);
+        String metrics = inst.mResults.getString(RESULT_KEY);
+        assertNotNull("Metrics missing", metrics);
+        ReportLog result = ReportLog.parse(metrics);
+        assertNotNull("Metrics could not be decoded", result);
+    }
+
+    public void testFile() throws Exception {
+        assertTrue("External storage is not mounted",
+                Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED));
+        final File dir = new File(Environment.getExternalStorageDirectory(), "report-log-files");
+        assertTrue("Report Log directory missing", dir.isDirectory() || dir.mkdirs());
+
+        // Remove files from earlier possible runs.
+        File[] files = dir.listFiles();
+        for (File file : files) {
+            file.delete();
+        }
+
+        TestInstrumentation inst = new TestInstrumentation();
+
+        DeviceReportLog log1 = new DeviceReportLog(REPORT_NAME_1, STREAM_NAME_1);
+        log1.addValue(TEST_MESSAGE_1, TEST_VALUE_1, TEST_TYPE_1, TEST_UNIT_1);
+        log1.setSummary(TEST_MESSAGE_1, TEST_VALUE_1, TEST_TYPE_1, TEST_UNIT_1);
+        log1.submit(inst);
+
+        DeviceReportLog log2 = new DeviceReportLog(REPORT_NAME_1, STREAM_NAME_2);
+        log2.addValue(TEST_MESSAGE_2, TEST_VALUE_2, TEST_TYPE_2, TEST_UNIT_2);
+        log2.setSummary(TEST_MESSAGE_2, TEST_VALUE_2, TEST_TYPE_2, TEST_UNIT_2);
+        log2.submit(inst);
+
+        DeviceReportLog log3 = new DeviceReportLog(REPORT_NAME_2, STREAM_NAME_3);
+        log3.addValue(TEST_MESSAGE_3, TEST_VALUE_3, TEST_TYPE_3, TEST_UNIT_3);
+        log3.setSummary(TEST_MESSAGE_3, TEST_VALUE_3, TEST_TYPE_3, TEST_UNIT_3);
+        log3.submit(inst);
+
+        DeviceReportLog log4 = new DeviceReportLog(REPORT_NAME_2, STREAM_NAME_4);
+        log4.addValue(TEST_MESSAGE_4, TEST_VALUE_4, TEST_TYPE_4, TEST_UNIT_4);
+        log4.setSummary(TEST_MESSAGE_4, TEST_VALUE_4, TEST_TYPE_4, TEST_UNIT_4);
+        log4.submit(inst);
+
+        File jsonFile1 = new File(dir, REPORT_NAME_1 + ".reportlog.json");
+        File jsonFile2 = new File(dir, REPORT_NAME_2 + ".reportlog.json");
+        assertTrue("Report Log missing", jsonFile1.exists());
+        assertTrue("Report Log missing", jsonFile2.exists());
+
+        BufferedReader jsonReader = new BufferedReader(new FileReader(jsonFile1));
+        StringBuilder metricsBuilder = new StringBuilder();
+        String line;
+        while ((line = jsonReader.readLine()) != null) {
+            metricsBuilder.append(line);
+        }
+        String metrics = metricsBuilder.toString().trim();
+        JSONObject jsonObject = new JSONObject(metrics);
+        assertTrue("Incorrect metrics",
+                jsonObject.getJSONObject(STREAM_NAME_1).getDouble(TEST_MESSAGE_1) == TEST_VALUE_1);
+        assertTrue("Incorrect metrics",
+                jsonObject.getJSONObject(STREAM_NAME_2).getDouble(TEST_MESSAGE_2) == TEST_VALUE_2);
+
+        jsonReader = new BufferedReader(new FileReader(jsonFile2));
+        metricsBuilder = new StringBuilder();
+        while ((line = jsonReader.readLine()) != null) {
+            metricsBuilder.append(line);
+        }
+        metrics = metricsBuilder.toString().trim();
+        jsonObject = new JSONObject(metrics);
+        assertTrue("Incorrect metrics",
+                jsonObject.getJSONObject(STREAM_NAME_3).getDouble(TEST_MESSAGE_3) == TEST_VALUE_3);
+        assertTrue("Incorrect metrics",
+                jsonObject.getJSONObject(STREAM_NAME_4).getDouble(TEST_MESSAGE_4) == TEST_VALUE_4);
+    }
+}
diff --git a/libs/deviceutillegacy-axt/Android.mk b/libs/deviceutillegacy-axt/Android.mk
new file mode 100644
index 0000000..1748080
--- /dev/null
+++ b/libs/deviceutillegacy-axt/Android.mk
@@ -0,0 +1,36 @@
+# Copyright (C) 2014 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.
+
+LOCAL_PATH:= $(call my-dir)
+
+include $(CLEAR_VARS)
+
+LOCAL_STATIC_JAVA_LIBRARIES := \
+    compatibility-device-util-axt \
+    junit
+
+LOCAL_JAVA_LIBRARIES := android.test.base.stubs
+
+LOCAL_SRC_FILES := \
+    $(call all-java-files-under, src)
+
+LOCAL_ADDITIONAL_DEPENDENCIES := $(LOCAL_PATH)/Android.mk
+
+LOCAL_MODULE_TAGS := optional
+
+LOCAL_MODULE := ctsdeviceutillegacy-axt
+
+LOCAL_SDK_VERSION := current
+
+include $(BUILD_STATIC_JAVA_LIBRARY)
diff --git a/libs/deviceutillegacy-axt/src/android/webkit/cts/WebViewOnUiThread.java b/libs/deviceutillegacy-axt/src/android/webkit/cts/WebViewOnUiThread.java
new file mode 100644
index 0000000..3072b07
--- /dev/null
+++ b/libs/deviceutillegacy-axt/src/android/webkit/cts/WebViewOnUiThread.java
@@ -0,0 +1,1116 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.webkit.cts;
+
+import com.android.compatibility.common.util.PollingCheck;
+import com.android.compatibility.common.util.TestThread;
+
+import android.graphics.Bitmap;
+import android.graphics.Picture;
+import android.graphics.Rect;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Looper;
+import android.os.Message;
+import android.os.SystemClock;
+import android.print.PrintDocumentAdapter;
+import androidx.test.rule.ActivityTestRule;
+import android.test.InstrumentationTestCase;
+import android.util.DisplayMetrics;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewParent;
+import android.webkit.DownloadListener;
+import android.webkit.CookieManager;
+import android.webkit.ValueCallback;
+import android.webkit.WebBackForwardList;
+import android.webkit.WebChromeClient;
+import android.webkit.WebMessage;
+import android.webkit.WebMessagePort;
+import android.webkit.WebSettings;
+import android.webkit.WebView.HitTestResult;
+import android.webkit.WebView.PictureListener;
+import android.webkit.WebView.VisualStateCallback;
+import android.webkit.WebView;
+import android.webkit.WebViewClient;
+
+import junit.framework.Assert;
+
+import java.io.File;
+import java.util.concurrent.Callable;
+import java.util.Map;
+
+/**
+ * Many tests need to run WebView code in the UI thread. This class
+ * wraps a WebView so that calls are ensured to arrive on the UI thread.
+ *
+ * All methods may be run on either the UI thread or test thread.
+ */
+public class WebViewOnUiThread {
+    /**
+     * The maximum time, in milliseconds (10 seconds) to wait for a load
+     * to be triggered.
+     */
+    private static final long LOAD_TIMEOUT = 10000;
+
+    /**
+     * Set to true after onPageFinished is called.
+     */
+    private boolean mLoaded;
+
+    /**
+     * Set to true after onNewPicture is called. Reset when onPageStarted
+     * is called.
+     */
+    private boolean mNewPicture;
+
+    /**
+     * The progress, in percentage, of the page load. Valid values are between
+     * 0 and 100.
+     */
+    private int mProgress;
+
+    /**
+     * The test that this class is being used in. Used for runTestOnUiThread.
+     */
+    private InstrumentationTestCase mTest;
+
+    /**
+     * The test rule that this class is being used in. Used for runTestOnUiThread.
+     */
+    private ActivityTestRule mActivityTestRule;
+
+    /**
+     * The WebView that calls will be made on.
+     */
+    private WebView mWebView;
+
+    /**
+     * Initializes the webView with a WebViewClient, WebChromeClient,
+     * and PictureListener to prepare for loadUrlAndWaitForCompletion.
+     *
+     * A new WebViewOnUiThread should be called during setUp so as to
+     * reinitialize between calls.
+     *
+     * @param test The test in which this is being run.
+     * @param webView The webView that the methods should call.
+     * @see #loadDataAndWaitForCompletion(String, String, String)
+     * @deprecated Use {@link WebViewOnUiThread#WebViewOnUiThread(ActivityTestRule, WebView)}
+     */
+    @Deprecated
+    public WebViewOnUiThread(InstrumentationTestCase test, WebView webView) {
+        mTest = test;
+        mWebView = webView;
+        final WebViewClient webViewClient = new WaitForLoadedClient(this);
+        final WebChromeClient webChromeClient = new WaitForProgressClient(this);
+        runOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                mWebView.setWebViewClient(webViewClient);
+                mWebView.setWebChromeClient(webChromeClient);
+                mWebView.setPictureListener(new WaitForNewPicture());
+            }
+        });
+    }
+
+    /**
+     * Initializes the webView with a WebViewClient, WebChromeClient,
+     * and PictureListener to prepare for loadUrlAndWaitForCompletion.
+     *
+     * A new WebViewOnUiThread should be called during setUp so as to
+     * reinitialize between calls.
+     *
+     * @param activityTestRule The test rule in which this is being run.
+     * @param webView The webView that the methods should call.
+     * @see #loadDataAndWaitForCompletion(String, String, String)
+     */
+    public WebViewOnUiThread(ActivityTestRule activityTestRule, WebView webView) {
+        mActivityTestRule = activityTestRule;
+        mWebView = webView;
+        final WebViewClient webViewClient = new WaitForLoadedClient(this);
+        final WebChromeClient webChromeClient = new WaitForProgressClient(this);
+        runOnUiThread(() -> {
+            mWebView.setWebViewClient(webViewClient);
+            mWebView.setWebChromeClient(webChromeClient);
+            mWebView.setPictureListener(new WaitForNewPicture());
+        });
+    }
+
+    /**
+     * Called after a test is complete and the WebView should be disengaged from
+     * the tests.
+     */
+    public void cleanUp() {
+        clearHistory();
+        clearCache(true);
+        setPictureListener(null);
+        setWebChromeClient(null);
+        setWebViewClient(null);
+        runOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                mWebView.destroy();
+            }
+        });
+    }
+
+    /**
+     * Called from WaitForNewPicture, this is used to indicate that
+     * the page has been drawn.
+     */
+    synchronized public void onNewPicture() {
+        mNewPicture = true;
+        this.notifyAll();
+    }
+
+    /**
+     * Called from WaitForLoadedClient, this is used to clear the picture
+     * draw state so that draws before the URL begins loading don't count.
+     */
+    synchronized public void onPageStarted() {
+        mNewPicture = false; // Earlier paints won't count.
+    }
+
+    /**
+     * Called from WaitForLoadedClient, this is used to indicate that
+     * the page is loaded, but not drawn yet.
+     */
+    synchronized public void onPageFinished() {
+        mLoaded = true;
+        this.notifyAll();
+    }
+
+    /**
+     * Called from the WebChrome client, this sets the current progress
+     * for a page.
+     * @param progress The progress made so far between 0 and 100.
+     */
+    synchronized public void onProgressChanged(int progress) {
+        mProgress = progress;
+        this.notifyAll();
+    }
+
+    public void setWebViewClient(final WebViewClient webViewClient) {
+        runOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                mWebView.setWebViewClient(webViewClient);
+            }
+        });
+    }
+
+    public void setWebChromeClient(final WebChromeClient webChromeClient) {
+        runOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                mWebView.setWebChromeClient(webChromeClient);
+            }
+        });
+    }
+
+    public void setPictureListener(final PictureListener pictureListener) {
+        runOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                mWebView.setPictureListener(pictureListener);
+            }
+        });
+    }
+
+    public void setNetworkAvailable(final boolean available) {
+        runOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                mWebView.setNetworkAvailable(available);
+            }
+        });
+    }
+
+    public void setDownloadListener(final DownloadListener listener) {
+        runOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                mWebView.setDownloadListener(listener);
+            }
+        });
+    }
+
+    public void setBackgroundColor(final int color) {
+        runOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                mWebView.setBackgroundColor(color);
+            }
+        });
+    }
+
+    public void clearCache(final boolean includeDiskFiles) {
+        runOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                mWebView.clearCache(includeDiskFiles);
+            }
+        });
+    }
+
+    public void clearHistory() {
+        runOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                mWebView.clearHistory();
+            }
+        });
+    }
+
+    public void requestFocus() {
+        new PollingCheck(LOAD_TIMEOUT) {
+            @Override
+            protected boolean check() {
+                requestFocusOnUiThread();
+                return hasFocus();
+            }
+        }.run();
+    }
+
+    private void requestFocusOnUiThread() {
+        runOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                mWebView.requestFocus();
+            }
+        });
+    }
+
+    private boolean hasFocus() {
+        return getValue(new ValueGetter<Boolean>() {
+            @Override
+            public Boolean capture() {
+                return mWebView.hasFocus();
+            }
+        });
+    }
+
+    public boolean canZoomIn() {
+        return getValue(new ValueGetter<Boolean>() {
+            @Override
+            public Boolean capture() {
+                return mWebView.canZoomIn();
+            }
+        });
+    }
+
+    public boolean canZoomOut() {
+        return getValue(new ValueGetter<Boolean>() {
+            @Override
+            public Boolean capture() {
+                return mWebView.canZoomOut();
+            }
+        });
+    }
+
+    public boolean zoomIn() {
+        return getValue(new ValueGetter<Boolean>() {
+            @Override
+            public Boolean capture() {
+                return mWebView.zoomIn();
+            }
+        });
+    }
+
+    public boolean zoomOut() {
+        return getValue(new ValueGetter<Boolean>() {
+            @Override
+            public Boolean capture() {
+                return mWebView.zoomOut();
+            }
+        });
+    }
+
+    public void zoomBy(final float zoomFactor) {
+        runOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                mWebView.zoomBy(zoomFactor);
+            }
+        });
+    }
+
+    public void setFindListener(final WebView.FindListener listener) {
+        runOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                mWebView.setFindListener(listener);
+            }
+        });
+    }
+
+    public void removeJavascriptInterface(final String interfaceName) {
+        runOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                mWebView.removeJavascriptInterface(interfaceName);
+            }
+        });
+    }
+
+    public WebMessagePort[] createWebMessageChannel() {
+        return getValue(new ValueGetter<WebMessagePort[]>() {
+            @Override
+            public WebMessagePort[] capture() {
+                return mWebView.createWebMessageChannel();
+            }
+        });
+    }
+
+    public void postWebMessage(final WebMessage message, final Uri targetOrigin) {
+        runOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                mWebView.postWebMessage(message, targetOrigin);
+            }
+        });
+    }
+
+    public void addJavascriptInterface(final Object object, final String name) {
+        runOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                mWebView.addJavascriptInterface(object, name);
+            }
+        });
+    }
+
+    public void flingScroll(final int vx, final int vy) {
+        runOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                mWebView.flingScroll(vx, vy);
+            }
+        });
+    }
+
+    public void requestFocusNodeHref(final Message hrefMsg) {
+        runOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                mWebView.requestFocusNodeHref(hrefMsg);
+            }
+        });
+    }
+
+    public void requestImageRef(final Message msg) {
+        runOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                mWebView.requestImageRef(msg);
+            }
+        });
+    }
+
+    public void setInitialScale(final int scaleInPercent) {
+        runOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                mWebView.setInitialScale(scaleInPercent);
+            }
+        });
+    }
+
+    public void clearSslPreferences() {
+        runOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                mWebView.clearSslPreferences();
+            }
+        });
+    }
+
+    public void clearClientCertPreferences(final Runnable onCleared) {
+        runOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                WebView.clearClientCertPreferences(onCleared);
+            }
+        });
+    }
+
+    public void resumeTimers() {
+        runOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                mWebView.resumeTimers();
+            }
+        });
+    }
+
+    public void findNext(final boolean forward) {
+        runOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                mWebView.findNext(forward);
+            }
+        });
+    }
+
+    public void clearMatches() {
+        runOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                mWebView.clearMatches();
+            }
+        });
+    }
+
+    /**
+     * Calls loadUrl on the WebView and then waits onPageFinished,
+     * onNewPicture and onProgressChange to reach 100.
+     * Test fails if the load timeout elapses.
+     * @param url The URL to load.
+     */
+    public void loadUrlAndWaitForCompletion(final String url) {
+        callAndWait(new Runnable() {
+            @Override
+            public void run() {
+                mWebView.loadUrl(url);
+            }
+        });
+    }
+
+    /**
+     * Calls loadUrl on the WebView and then waits onPageFinished,
+     * onNewPicture and onProgressChange to reach 100.
+     * Test fails if the load timeout elapses.
+     * @param url The URL to load.
+     * @param extraHeaders The additional headers to be used in the HTTP request.
+     */
+    public void loadUrlAndWaitForCompletion(final String url,
+            final Map<String, String> extraHeaders) {
+        callAndWait(new Runnable() {
+            @Override
+            public void run() {
+                mWebView.loadUrl(url, extraHeaders);
+            }
+        });
+    }
+
+    public void loadUrl(final String url) {
+        runOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                mWebView.loadUrl(url);
+            }
+        });
+    }
+
+    public void stopLoading() {
+        runOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                mWebView.stopLoading();
+            }
+        });
+    }
+
+    public void postUrlAndWaitForCompletion(final String url, final byte[] postData) {
+        callAndWait(new Runnable() {
+            @Override
+            public void run() {
+                mWebView.postUrl(url, postData);
+            }
+        });
+    }
+
+    public void loadDataAndWaitForCompletion(final String data,
+            final String mimeType, final String encoding) {
+        callAndWait(new Runnable() {
+            @Override
+            public void run() {
+                mWebView.loadData(data, mimeType, encoding);
+            }
+        });
+    }
+
+    public void loadDataWithBaseURLAndWaitForCompletion(final String baseUrl,
+            final String data, final String mimeType, final String encoding,
+            final String historyUrl) {
+        callAndWait(new Runnable() {
+            @Override
+            public void run() {
+                mWebView.loadDataWithBaseURL(baseUrl, data, mimeType, encoding,
+                        historyUrl);
+            }
+        });
+    }
+
+    /**
+     * Reloads a page and waits for it to complete reloading. Use reload
+     * if it is a form resubmission and the onFormResubmission responds
+     * by telling WebView not to resubmit it.
+     */
+    public void reloadAndWaitForCompletion() {
+        callAndWait(new Runnable() {
+            @Override
+            public void run() {
+                mWebView.reload();
+            }
+        });
+    }
+
+    /**
+     * Reload the previous URL. Use reloadAndWaitForCompletion unless
+     * it is a form resubmission and the onFormResubmission responds
+     * by telling WebView not to resubmit it.
+     */
+    public void reload() {
+        runOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                mWebView.reload();
+            }
+        });
+    }
+
+    /**
+     * Use this only when JavaScript causes a page load to wait for the
+     * page load to complete. Otherwise use loadUrlAndWaitForCompletion or
+     * similar functions.
+     */
+    public void waitForLoadCompletion() {
+        waitForCriteria(LOAD_TIMEOUT,
+                new Callable<Boolean>() {
+                    @Override
+                    public Boolean call() {
+                        return isLoaded();
+                    }
+                });
+        clearLoad();
+    }
+
+    private void waitForCriteria(long timeout, Callable<Boolean> doneCriteria) {
+        if (isUiThread()) {
+            waitOnUiThread(timeout, doneCriteria);
+        } else {
+            waitOnTestThread(timeout, doneCriteria);
+        }
+    }
+
+    public String getTitle() {
+        return getValue(new ValueGetter<String>() {
+            @Override
+            public String capture() {
+                return mWebView.getTitle();
+            }
+        });
+    }
+
+    public WebSettings getSettings() {
+        return getValue(new ValueGetter<WebSettings>() {
+            @Override
+            public WebSettings capture() {
+                return mWebView.getSettings();
+            }
+        });
+    }
+
+    public WebBackForwardList copyBackForwardList() {
+        return getValue(new ValueGetter<WebBackForwardList>() {
+            @Override
+            public WebBackForwardList capture() {
+                return mWebView.copyBackForwardList();
+            }
+        });
+    }
+
+    public Bitmap getFavicon() {
+        return getValue(new ValueGetter<Bitmap>() {
+            @Override
+            public Bitmap capture() {
+                return mWebView.getFavicon();
+            }
+        });
+    }
+
+    public String getUrl() {
+        return getValue(new ValueGetter<String>() {
+            @Override
+            public String capture() {
+                return mWebView.getUrl();
+            }
+        });
+    }
+
+    public int getProgress() {
+        return getValue(new ValueGetter<Integer>() {
+            @Override
+            public Integer capture() {
+                return mWebView.getProgress();
+            }
+        });
+    }
+
+    public int getHeight() {
+        return getValue(new ValueGetter<Integer>() {
+            @Override
+            public Integer capture() {
+                return mWebView.getHeight();
+            }
+        });
+    }
+
+    public int getContentHeight() {
+        return getValue(new ValueGetter<Integer>() {
+            @Override
+            public Integer capture() {
+                return mWebView.getContentHeight();
+            }
+        });
+    }
+
+    public boolean pageUp(final boolean top) {
+        return getValue(new ValueGetter<Boolean>() {
+            @Override
+            public Boolean capture() {
+                return mWebView.pageUp(top);
+            }
+        });
+    }
+
+    public boolean pageDown(final boolean bottom) {
+        return getValue(new ValueGetter<Boolean>() {
+            @Override
+            public Boolean capture() {
+                return mWebView.pageDown(bottom);
+            }
+        });
+    }
+
+    public void postVisualStateCallback(final long requestId, final VisualStateCallback callback) {
+        runOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                mWebView.postVisualStateCallback(requestId, callback);
+            }
+        });
+    }
+
+    public int[] getLocationOnScreen() {
+        final int[] location = new int[2];
+        return getValue(new ValueGetter<int[]>() {
+            @Override
+            public int[] capture() {
+                mWebView.getLocationOnScreen(location);
+                return location;
+            }
+        });
+    }
+
+    public float getScale() {
+        return getValue(new ValueGetter<Float>() {
+            @Override
+            public Float capture() {
+                return mWebView.getScale();
+            }
+        });
+    }
+
+    public boolean requestFocus(final int direction,
+            final Rect previouslyFocusedRect) {
+        return getValue(new ValueGetter<Boolean>() {
+            @Override
+            public Boolean capture() {
+                return mWebView.requestFocus(direction, previouslyFocusedRect);
+            }
+        });
+    }
+
+    public HitTestResult getHitTestResult() {
+        return getValue(new ValueGetter<HitTestResult>() {
+            @Override
+            public HitTestResult capture() {
+                return mWebView.getHitTestResult();
+            }
+        });
+    }
+
+    public int getScrollX() {
+        return getValue(new ValueGetter<Integer>() {
+            @Override
+            public Integer capture() {
+                return mWebView.getScrollX();
+            }
+        });
+    }
+
+    public int getScrollY() {
+        return getValue(new ValueGetter<Integer>() {
+            @Override
+            public Integer capture() {
+                return mWebView.getScrollY();
+            }
+        });
+    }
+
+    public final DisplayMetrics getDisplayMetrics() {
+        return getValue(new ValueGetter<DisplayMetrics>() {
+            @Override
+            public DisplayMetrics capture() {
+                return mWebView.getContext().getResources().getDisplayMetrics();
+            }
+        });
+    }
+
+    public boolean requestChildRectangleOnScreen(final View child,
+            final Rect rect,
+            final boolean immediate) {
+        return getValue(new ValueGetter<Boolean>() {
+            @Override
+            public Boolean capture() {
+                return mWebView.requestChildRectangleOnScreen(child, rect,
+                        immediate);
+            }
+        });
+    }
+
+    public int findAll(final String find) {
+        return getValue(new ValueGetter<Integer>() {
+            @Override
+            public Integer capture() {
+                return mWebView.findAll(find);
+            }
+        });
+    }
+
+    public Picture capturePicture() {
+        return getValue(new ValueGetter<Picture>() {
+            @Override
+            public Picture capture() {
+                return mWebView.capturePicture();
+            }
+        });
+    }
+
+    public void evaluateJavascript(final String script, final ValueCallback<String> result) {
+        runOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                mWebView.evaluateJavascript(script, result);
+            }
+        });
+    }
+
+    public void saveWebArchive(final String basename, final boolean autoname,
+                               final ValueCallback<String> callback) {
+        runOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                mWebView.saveWebArchive(basename, autoname, callback);
+            }
+        });
+    }
+
+    public WebView createWebView() {
+        return getValue(new ValueGetter<WebView>() {
+            @Override
+            public WebView capture() {
+                return new WebView(mWebView.getContext());
+            }
+        });
+    }
+
+    public PrintDocumentAdapter createPrintDocumentAdapter() {
+        return getValue(new ValueGetter<PrintDocumentAdapter>() {
+            @Override
+            public PrintDocumentAdapter capture() {
+                return mWebView.createPrintDocumentAdapter();
+            }
+        });
+    }
+
+    public void setLayoutHeightToMatchParent() {
+        runOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                ViewParent parent = mWebView.getParent();
+                if (parent instanceof ViewGroup) {
+                    ((ViewGroup) parent).getLayoutParams().height =
+                        ViewGroup.LayoutParams.MATCH_PARENT;
+                }
+                mWebView.getLayoutParams().height = ViewGroup.LayoutParams.MATCH_PARENT;
+                mWebView.requestLayout();
+            }
+        });
+    }
+
+    public void setLayoutToMatchParent() {
+        runOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                setMatchParent((View) mWebView.getParent());
+                setMatchParent(mWebView);
+                mWebView.requestLayout();
+            }
+        });
+    }
+
+    public void setAcceptThirdPartyCookies(final boolean accept) {
+        runOnUiThread(new Runnable() {
+            @Override
+            public void run() {
+                CookieManager.getInstance().setAcceptThirdPartyCookies(mWebView, accept);
+            }
+        });
+    }
+
+    public boolean acceptThirdPartyCookies() {
+        return getValue(new ValueGetter<Boolean>() {
+            @Override
+            public Boolean capture() {
+                return CookieManager.getInstance().acceptThirdPartyCookies(mWebView);
+            }
+        });
+    }
+
+    /**
+     * Helper for running code on the UI thread where an exception is
+     * a test failure. If this is already the UI thread then it runs
+     * the code immediately.
+     *
+     * @see InstrumentationTestCase#runTestOnUiThread(Runnable)
+     * @see ActivityTestRule#runOnUiThread(Runnable)
+     * @param r The code to run in the UI thread
+     */
+    public void runOnUiThread(Runnable r) {
+        try {
+            if (isUiThread()) {
+                r.run();
+            } else {
+                if (mActivityTestRule != null) {
+                    mActivityTestRule.runOnUiThread(r);
+                } else {
+                    mTest.runTestOnUiThread(r);
+                }
+            }
+        } catch (Throwable t) {
+            Assert.fail("Unexpected error while running on UI thread: "
+                    + t.getMessage());
+        }
+    }
+
+    /**
+     * Accessor for underlying WebView.
+     * @return The WebView being wrapped by this class.
+     */
+    public WebView getWebView() {
+        return mWebView;
+    }
+
+    private<T> T getValue(ValueGetter<T> getter) {
+        runOnUiThread(getter);
+        return getter.getValue();
+    }
+
+    private abstract class ValueGetter<T> implements Runnable {
+        private T mValue;
+
+        @Override
+        public void run() {
+            mValue = capture();
+        }
+
+        protected abstract T capture();
+
+        public T getValue() {
+           return mValue;
+        }
+    }
+
+    /**
+     * Returns true if the current thread is the UI thread based on the
+     * Looper.
+     */
+    private static boolean isUiThread() {
+        return (Looper.myLooper() == Looper.getMainLooper());
+    }
+
+    /**
+     * @return Whether or not the load has finished.
+     */
+    private synchronized boolean isLoaded() {
+        return mLoaded && mNewPicture && mProgress == 100;
+    }
+
+    /**
+     * Makes a WebView call, waits for completion and then resets the
+     * load state in preparation for the next load call.
+     * @param call The call to make on the UI thread prior to waiting.
+     */
+    private void callAndWait(Runnable call) {
+        Assert.assertTrue("WebViewOnUiThread.load*AndWaitForCompletion calls "
+                + "may not be mixed with load* calls directly on WebView "
+                + "without calling waitForLoadCompletion after the load",
+                !isLoaded());
+        clearLoad(); // clear any extraneous signals from a previous load.
+        runOnUiThread(call);
+        waitForLoadCompletion();
+    }
+
+    /**
+     * Called whenever a load has been completed so that a subsequent call to
+     * waitForLoadCompletion doesn't return immediately.
+     */
+    synchronized private void clearLoad() {
+        mLoaded = false;
+        mNewPicture = false;
+        mProgress = 0;
+    }
+
+    /**
+     * Uses a polling mechanism, while pumping messages to check when the
+     * criteria is met.
+     */
+    private void waitOnUiThread(long timeout, final Callable<Boolean> doneCriteria) {
+        new PollingCheck(timeout) {
+            @Override
+            protected boolean check() {
+                pumpMessages();
+                try {
+                    return doneCriteria.call();
+                } catch (Exception e) {
+                    Assert.fail("Unexpected error while checking the criteria: "
+                            + e.getMessage());
+                    return true;
+                }
+            }
+        }.run();
+    }
+
+    /**
+     * Uses a wait/notify to check when the criteria is met.
+     */
+    private synchronized void waitOnTestThread(long timeout, Callable<Boolean> doneCriteria) {
+        try {
+            long waitEnd = SystemClock.uptimeMillis() + timeout;
+            long timeRemaining = timeout;
+            while (!doneCriteria.call() && timeRemaining > 0) {
+                this.wait(timeRemaining);
+                timeRemaining = waitEnd - SystemClock.uptimeMillis();
+            }
+            Assert.assertTrue("Action failed to complete before timeout", doneCriteria.call());
+        } catch (InterruptedException e) {
+            // We'll just drop out of the loop and fail
+        } catch (Exception e) {
+            Assert.fail("Unexpected error while checking the criteria: "
+                    + e.getMessage());
+        }
+    }
+
+    /**
+     * Pumps all currently-queued messages in the UI thread and then exits.
+     * This is useful to force processing while running tests in the UI thread.
+     */
+    private void pumpMessages() {
+        class ExitLoopException extends RuntimeException {
+        }
+
+        // Force loop to exit when processing this. Loop.quit() doesn't
+        // work because this is the main Loop.
+        mWebView.getHandler().post(new Runnable() {
+            @Override
+            public void run() {
+                throw new ExitLoopException(); // exit loop!
+            }
+        });
+        try {
+            // Pump messages until our message gets through.
+            Looper.loop();
+        } catch (ExitLoopException e) {
+        }
+    }
+
+    /**
+     * Set LayoutParams to MATCH_PARENT.
+     *
+     * @param view Target view
+     */
+    private void setMatchParent(View view) {
+        ViewGroup.LayoutParams params = view.getLayoutParams();
+        params.height = ViewGroup.LayoutParams.MATCH_PARENT;
+        params.width = ViewGroup.LayoutParams.MATCH_PARENT;
+        view.setLayoutParams(params);
+    }
+
+    /**
+     * A WebChromeClient used to capture the onProgressChanged for use
+     * in waitFor functions. If a test must override the WebChromeClient,
+     * it can derive from this class or call onProgressChanged
+     * directly.
+     */
+    public static class WaitForProgressClient extends WebChromeClient {
+        private WebViewOnUiThread mOnUiThread;
+
+        public WaitForProgressClient(WebViewOnUiThread onUiThread) {
+            mOnUiThread = onUiThread;
+        }
+
+        @Override
+        public void onProgressChanged(WebView view, int newProgress) {
+            super.onProgressChanged(view, newProgress);
+            mOnUiThread.onProgressChanged(newProgress);
+        }
+    }
+
+    /**
+     * A WebViewClient that captures the onPageFinished for use in
+     * waitFor functions. Using initializeWebView sets the WaitForLoadedClient
+     * into the WebView. If a test needs to set a specific WebViewClient and
+     * needs the waitForCompletion capability then it should derive from
+     * WaitForLoadedClient or call WebViewOnUiThread.onPageFinished.
+     */
+    public static class WaitForLoadedClient extends WebViewClient {
+        private WebViewOnUiThread mOnUiThread;
+
+        public WaitForLoadedClient(WebViewOnUiThread onUiThread) {
+            mOnUiThread = onUiThread;
+        }
+
+        @Override
+        public void onPageFinished(WebView view, String url) {
+            super.onPageFinished(view, url);
+            mOnUiThread.onPageFinished();
+        }
+
+        @Override
+        public void onPageStarted(WebView view, String url, Bitmap favicon) {
+            super.onPageStarted(view, url, favicon);
+            mOnUiThread.onPageStarted();
+        }
+    }
+
+    /**
+     * A PictureListener that captures the onNewPicture for use in
+     * waitForLoadCompletion. Using initializeWebView sets the PictureListener
+     * into the WebView. If a test needs to set a specific PictureListener and
+     * needs the waitForCompletion capability then it should call
+     * WebViewOnUiThread.onNewPicture.
+     */
+    private class WaitForNewPicture implements PictureListener {
+        @Override
+        public void onNewPicture(WebView view, Picture picture) {
+            WebViewOnUiThread.this.onNewPicture();
+        }
+    }
+}
diff --git a/libs/deviceutillegacy-axt/src/com/android/compatibility/common/util/SynchronousPixelCopy.java b/libs/deviceutillegacy-axt/src/com/android/compatibility/common/util/SynchronousPixelCopy.java
new file mode 100644
index 0000000..7ba8646
--- /dev/null
+++ b/libs/deviceutillegacy-axt/src/com/android/compatibility/common/util/SynchronousPixelCopy.java
@@ -0,0 +1,99 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.compatibility.common.util;
+
+import static org.junit.Assert.fail;
+
+import android.graphics.Bitmap;
+import android.graphics.Rect;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.view.PixelCopy;
+import android.view.Surface;
+import android.view.Window;
+import android.view.PixelCopy.OnPixelCopyFinishedListener;
+import android.view.SurfaceView;
+
+public class SynchronousPixelCopy implements OnPixelCopyFinishedListener {
+    private static Handler sHandler;
+    static {
+        HandlerThread thread = new HandlerThread("PixelCopyHelper");
+        thread.start();
+        sHandler = new Handler(thread.getLooper());
+    }
+
+    private int mStatus = -1;
+
+    public int request(Surface source, Bitmap dest) {
+        synchronized (this) {
+            PixelCopy.request(source, dest, this, sHandler);
+            return getResultLocked();
+        }
+    }
+
+    public int request(Surface source, Rect srcRect, Bitmap dest) {
+        synchronized (this) {
+            PixelCopy.request(source, srcRect, dest, this, sHandler);
+            return getResultLocked();
+        }
+    }
+
+    public int request(SurfaceView source, Bitmap dest) {
+        synchronized (this) {
+            PixelCopy.request(source, dest, this, sHandler);
+            return getResultLocked();
+        }
+    }
+
+    public int request(SurfaceView source, Rect srcRect, Bitmap dest) {
+        synchronized (this) {
+            PixelCopy.request(source, srcRect, dest, this, sHandler);
+            return getResultLocked();
+        }
+    }
+
+    public int request(Window source, Bitmap dest) {
+        synchronized (this) {
+            PixelCopy.request(source, dest, this, sHandler);
+            return getResultLocked();
+        }
+    }
+
+    public int request(Window source, Rect srcRect, Bitmap dest) {
+        synchronized (this) {
+            PixelCopy.request(source, srcRect, dest, this, sHandler);
+            return getResultLocked();
+        }
+    }
+
+    private int getResultLocked() {
+        try {
+            this.wait(250);
+        } catch (InterruptedException e) {
+            fail("PixelCopy request didn't complete within 250ms");
+        }
+        return mStatus;
+    }
+
+    @Override
+    public void onPixelCopyFinished(int copyResult) {
+        synchronized (this) {
+            mStatus = copyResult;
+            this.notify();
+        }
+    }
+}
diff --git a/tests/core/runner-axt/Android.mk b/tests/core/runner-axt/Android.mk
new file mode 100644
index 0000000..4ab91b81
--- /dev/null
+++ b/tests/core/runner-axt/Android.mk
@@ -0,0 +1,51 @@
+# Copyright (C) 2009 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.
+
+LOCAL_PATH:= $(call my-dir)
+
+#==========================================================
+# Build the core runner.
+#==========================================================
+
+# Build library
+include $(CLEAR_VARS)
+
+LOCAL_MODULE_TAGS := optional
+LOCAL_SRC_FILES := $(call all-java-files-under,src)
+LOCAL_MODULE := cts-core-test-runner-axt
+LOCAL_STATIC_JAVA_LIBRARIES := \
+    compatibility-device-util-axt \
+    androidx.test.rules \
+    vogarexpect \
+    testng
+
+LOCAL_JAVA_LIBRARIES := android.test.runner.stubs
+LOCAL_SDK_VERSION := current
+
+include $(BUILD_STATIC_JAVA_LIBRARY)
+
+#==========================================================
+# Build the run listener
+#==========================================================
+
+# Build library
+include $(CLEAR_VARS)
+
+LOCAL_MODULE_TAGS := optional
+LOCAL_SRC_FILES := $(call all-java-files-under,src/com/android/cts/runner)
+LOCAL_MODULE := cts-test-runner-axt
+LOCAL_STATIC_JAVA_LIBRARIES := androidx.test.rules
+LOCAL_SDK_VERSION := current
+
+include $(BUILD_STATIC_JAVA_LIBRARY)
diff --git a/tests/core/runner-axt/AndroidManifest.xml b/tests/core/runner-axt/AndroidManifest.xml
new file mode 100644
index 0000000..a825501
--- /dev/null
+++ b/tests/core/runner-axt/AndroidManifest.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ * Copyright (C) 2007 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.
+ -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="android.core.tests.runner">
+    <uses-permission android:name="android.permission.INTERNET" />
+    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
+
+    <application>
+        <uses-library android:name="android.test.runner" />
+    </application>
+
+    <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+                     android:targetPackage="android.core.tests.runner"
+                     android:label="cts framework tests"/>
+
+</manifest>
diff --git a/tests/core/runner-axt/src/com/android/cts/core/runner/ExpectationBasedFilter.java b/tests/core/runner-axt/src/com/android/cts/core/runner/ExpectationBasedFilter.java
new file mode 100644
index 0000000..f409dbd
--- /dev/null
+++ b/tests/core/runner-axt/src/com/android/cts/core/runner/ExpectationBasedFilter.java
@@ -0,0 +1,145 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.cts.core.runner;
+
+import android.os.Bundle;
+import android.util.Log;
+import com.google.common.base.Splitter;
+import java.io.IOException;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Set;
+import javax.annotation.Nullable;
+import org.junit.runner.Description;
+import org.junit.runner.manipulation.Filter;
+import org.junit.runners.ParentRunner;
+import org.junit.runners.Suite;
+import vogar.Expectation;
+import vogar.ExpectationStore;
+import vogar.ModeId;
+import vogar.Result;
+
+/**
+ * Filter out tests/classes that are not requested or which are expected to fail.
+ *
+ * <p>This filter has to handle both a hierarchy of {@code Description descriptions} that looks
+ * something like this:
+ * <pre>
+ * Suite
+ *     Suite
+ *         Suite
+ *             ParentRunner
+ *                 Test
+ *                 ...
+ *             ...
+ *         ParentRunner
+ *             Test
+ *             ...
+ *         ...
+ *     Suite
+ *         ParentRunner
+ *             Test
+ *             ...
+ *         ...
+ *     ...
+ * </pre>
+ *
+ * <p>It cannot filter out the non-leaf nodes in the hierarchy, i.e. {@link Suite} and
+ * {@link ParentRunner}, as that would prevent it from traversing the hierarchy and finding
+ * the leaf nodes.
+ */
+class ExpectationBasedFilter extends Filter {
+
+    static final String TAG = "ExpectationBasedFilter";
+
+    private static final String ARGUMENT_EXPECTATIONS = "core-expectations";
+
+    private static final Splitter CLASS_LIST_SPLITTER = Splitter.on(',').trimResults();
+
+    private final ExpectationStore expectationStore;
+
+    private static List<String> getExpectationResourcePaths(Bundle args) {
+        return CLASS_LIST_SPLITTER.splitToList(args.getString(ARGUMENT_EXPECTATIONS));
+    }
+
+    public ExpectationBasedFilter(Bundle args) {
+        ExpectationStore expectationStore = null;
+        try {
+            // Get the set of resource names containing the expectations.
+            Set<String> expectationResources = new LinkedHashSet<>(
+                getExpectationResourcePaths(args));
+            Log.i(TAG, "Loading expectations from: " + expectationResources);
+            expectationStore = ExpectationStore.parseResources(
+                getClass(), expectationResources, ModeId.DEVICE);
+        } catch (IOException e) {
+            Log.e(TAG, "Could not initialize ExpectationStore: ", e);
+        }
+
+        this.expectationStore = expectationStore;
+    }
+
+    @Override
+    public boolean shouldRun(Description description) {
+        // Only filter leaf nodes. The description is for a test if and only if it is a leaf node.
+        // Non-leaf nodes must not be filtered out as that would prevent leaf nodes from being
+        // visited in the case when we are traversing the hierarchy of classes.
+        Description testDescription = getTestDescription(description);
+        if (testDescription != null) {
+            String className = testDescription.getClassName();
+            String methodName = testDescription.getMethodName();
+            String testName = className + "#" + methodName;
+
+            if (expectationStore != null) {
+                Expectation expectation = expectationStore.get(testName);
+                if (expectation.getResult() != Result.SUCCESS) {
+                    Log.d(TAG, "Excluding test " + testDescription
+                            + " as it matches expectation: " + expectation);
+                    return false;
+                }
+            }
+        }
+
+        return true;
+    }
+
+    private Description getTestDescription(Description description) {
+        List<Description> children = description.getChildren();
+        // An empty description is by definition a test.
+        if (children.isEmpty()) {
+            return description;
+        }
+
+        // Handle initialization errors that were wrapped in an ErrorReportingRunner as a special
+        // case. This is needed because ErrorReportingRunner is treated as a suite of Throwables,
+        // (where each Throwable corresponds to a test called initializationError) and so its
+        // description contains children, one for each Throwable, and so is not treated as a test
+        // to filter. Unfortunately, it does not support Filterable so this filter is never applied
+        // to its children.
+        // See https://github.com/junit-team/junit/issues/1253
+        Description child = children.get(0);
+        String methodName = child.getMethodName();
+        if ("initializationError".equals(methodName)) {
+            return child;
+        }
+
+        return null;
+    }
+
+    @Override
+    public String describe() {
+        return "TestFilter";
+    }
+}
diff --git a/tests/core/runner-axt/src/com/android/cts/core/runner/support/SingleTestNGTestRunListener.java b/tests/core/runner-axt/src/com/android/cts/core/runner/support/SingleTestNGTestRunListener.java
new file mode 100644
index 0000000..7a68a8b
--- /dev/null
+++ b/tests/core/runner-axt/src/com/android/cts/core/runner/support/SingleTestNGTestRunListener.java
@@ -0,0 +1,124 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.cts.core.runner.support;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+/**
+ * Listener for TestNG runs that provides gtest-like console output.
+ *
+ * Prints a message like [RUN], [OK], [ERROR], [SKIP] to stdout
+ * as tests are being executed with their status.
+ *
+ * This output is also saved as the device logs (logcat) when the test is run through
+ * cts-tradefed.
+ */
+public class SingleTestNGTestRunListener implements org.testng.ITestListener {
+    private int mTestStarted = 0;
+
+    private Map<String, Throwable> failures = new LinkedHashMap<>();
+
+    private static class Prefixes {
+        @SuppressWarnings("unused")
+        private static final String INFORMATIONAL_MARKER =  "[----------]";
+        private static final String START_TEST_MARKER =     "[ RUN      ]";
+        private static final String OK_TEST_MARKER =        "[       OK ]";
+        private static final String ERROR_TEST_RUN_MARKER = "[    ERROR ]";
+        private static final String SKIPPED_TEST_MARKER =   "[     SKIP ]";
+        private static final String TEST_RUN_MARKER =       "[==========]";
+    }
+
+    // How many tests did TestNG *actually* try to run?
+    public int getNumTestStarted() {
+      return mTestStarted;
+    }
+
+    public Map<String, Throwable> getFailures() {
+        return Collections.unmodifiableMap(failures);
+    }
+
+    @Override
+    public void onFinish(org.testng.ITestContext context) {
+        System.out.println(String.format("%s", Prefixes.TEST_RUN_MARKER));
+    }
+
+    @Override
+    public void onStart(org.testng.ITestContext context) {
+        System.out.println(String.format("%s", Prefixes.INFORMATIONAL_MARKER));
+    }
+
+    @Override
+    public void onTestFailedButWithinSuccessPercentage(org.testng.ITestResult result) {
+        onTestFailure(result);
+    }
+
+    @Override
+    public void onTestFailure(org.testng.ITestResult result) {
+        // All failures are coalesced into one '[ FAILED ]' message at the end
+        // This is because a single test method can run multiple times with different parameters.
+        // Since we only test a single method, it's safe to combine all failures into one
+        // failure at the end.
+        //
+        // The big pass/fail is printed from SingleTestNGTestRunner, not from the listener.
+        String id = getId(result);
+        Throwable throwable = result.getThrowable();
+        System.out.println(String.format("%s %s ::: %s", Prefixes.ERROR_TEST_RUN_MARKER,
+                id, stringify(throwable)));
+        failures.put(id, throwable);
+    }
+
+    @Override
+    public void onTestSkipped(org.testng.ITestResult result) {
+        System.out.println(String.format("%s %s", Prefixes.SKIPPED_TEST_MARKER,
+              getId(result)));
+    }
+
+    @Override
+    public void onTestStart(org.testng.ITestResult result) {
+        mTestStarted++;
+        System.out.println(String.format("%s %s", Prefixes.START_TEST_MARKER,
+              getId(result)));
+    }
+
+    @Override
+    public void onTestSuccess(org.testng.ITestResult result) {
+        System.out.println(String.format("%s", Prefixes.OK_TEST_MARKER));
+    }
+
+    private String getId(org.testng.ITestResult test) {
+        // TestNG is quite complicated since tests can have arbitrary parameters.
+        // Use its code to stringify a result name instead of doing it ourselves.
+
+        org.testng.remote.strprotocol.TestResultMessage msg =
+                new org.testng.remote.strprotocol.TestResultMessage(
+                    null, /*suite name*/
+                    null, /*test name -- display the test method name instead */
+                    test);
+
+        String className = test.getTestClass().getName();
+        //String name = test.getMethod().getMethodName();
+        return String.format("%s#%s", className, msg.toDisplayString());
+
+    }
+
+    private String stringify(Throwable error) {
+        return Arrays.toString(error.getStackTrace()).replaceAll("\n", " ");
+    }
+}
diff --git a/tests/core/runner-axt/src/com/android/cts/core/runner/support/SingleTestNgTestExecutor.java b/tests/core/runner-axt/src/com/android/cts/core/runner/support/SingleTestNgTestExecutor.java
new file mode 100644
index 0000000..deb18df
--- /dev/null
+++ b/tests/core/runner-axt/src/com/android/cts/core/runner/support/SingleTestNgTestExecutor.java
@@ -0,0 +1,132 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.cts.core.runner.support;
+
+import android.util.Log;
+
+import org.testng.TestNG;
+import org.testng.xml.XmlClass;
+import org.testng.xml.XmlInclude;
+import org.testng.xml.XmlSuite;
+import org.testng.xml.XmlTest;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Test executor to run a single TestNG test method.
+ */
+public class SingleTestNgTestExecutor {
+    // Execute any method which is in the class klass.
+    // The klass is passed in separately to handle inherited methods only.
+    // Returns true if all tests pass, false otherwise.
+    public static Result execute(Class<?> klass, String methodName) {
+        if (klass == null) {
+          throw new NullPointerException("klass must not be null");
+        }
+
+        if (methodName == null) {
+          throw new NullPointerException("methodName must not be null");
+        }
+
+        //if (!method.getDeclaringClass().isAssignableFrom(klass)) {
+        //  throw new IllegalArgumentException("klass must match method's declaring class");
+        //}
+
+        SingleTestNGTestRunListener listener = new SingleTestNGTestRunListener();
+
+        // Although creating a new testng "core" every time might seem heavyweight, in practice
+        // it seems to take a mere few milliseconds at most.
+        // Since we're running all the parameteric combinations of a test,
+        // this ends up being neglible relative to that.
+        TestNG testng = createTestNG(klass.getName(), methodName, listener);
+        testng.run();
+
+        if (listener.getNumTestStarted() <= 0) {
+          // It's possible to be invoked here with an arbitrary method name
+          // so print out a warning incase TestNG actually had a no-op.
+          Log.w("TestNgExec", "execute class " + klass.getName() + ", method " + methodName +
+              " had 0 tests executed. Not a test method?");
+        }
+
+        return new Result(testng.hasFailure(), listener.getFailures());
+    }
+
+    private static org.testng.TestNG createTestNG(String klass, String method,
+            SingleTestNGTestRunListener listener) {
+        org.testng.TestNG testng = new org.testng.TestNG();
+        testng.setUseDefaultListeners(false);  // Don't create the testng-specific HTML/XML reports.
+        // It still prints the X/Y tests succeeded/failed summary to stdout.
+
+        // We don't strictly need this listener for CTS, but having it print SUCCESS/FAIL
+        // makes it easier to diagnose which particular combination of a test method had failed
+        // from looking at device logcat.
+        testng.addListener(listener);
+
+        /* Construct the following equivalent XML configuration:
+         *
+         * <!DOCTYPE suite SYSTEM "http://testng.org/testng-1.0.dtd" >
+         * <suite>
+         *   <test>
+         *     <classes>
+         *       <class name="$klass">
+         *         <include name="$method" />
+         *       </class>
+         *     </classes>
+         *   </test>
+         * </suite>
+         *
+         * This will ensure that only a single klass/method is being run by testng.
+         * (It can still be run multiple times due to @DataProvider, with different parameters
+         * each time)
+         */
+        List<XmlSuite> suites = new ArrayList<>();
+        XmlSuite the_suite = new XmlSuite();
+        XmlTest the_test = new XmlTest(the_suite);
+        XmlClass the_class = new XmlClass(klass);
+        XmlInclude the_include = new XmlInclude(method);
+
+        the_class.getIncludedMethods().add(the_include);
+        the_test.getXmlClasses().add(the_class);
+        suites.add(the_suite);
+        testng.setXmlSuites(suites);
+
+        return testng;
+    }
+
+    public static class Result {
+        private final boolean hasFailure;
+        private final Map<String,Throwable> failures;
+
+
+        Result(boolean hasFailure, Map<String, Throwable> failures) {
+            this.hasFailure = hasFailure;
+            this.failures = Collections.unmodifiableMap(new LinkedHashMap<>(failures));
+        }
+
+        public boolean hasFailure() {
+            return hasFailure;
+        }
+
+        public Map<String, Throwable> getFailures() {
+            return failures;
+        }
+    }
+}
diff --git a/tests/core/runner-axt/src/com/android/cts/core/runner/support/TestNgRunner.java b/tests/core/runner-axt/src/com/android/cts/core/runner/support/TestNgRunner.java
new file mode 100644
index 0000000..d9bf037
--- /dev/null
+++ b/tests/core/runner-axt/src/com/android/cts/core/runner/support/TestNgRunner.java
@@ -0,0 +1,235 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.cts.core.runner.support;
+
+import android.util.Log;
+
+import org.junit.runner.Description;
+import org.junit.runner.Runner;
+import org.junit.runner.manipulation.Filter;
+import org.junit.runner.manipulation.Filterable;
+import org.junit.runner.manipulation.NoTestsRemainException;
+import org.junit.runner.notification.Failure;
+import org.junit.runner.notification.RunNotifier;
+
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+import java.util.HashSet;
+import java.util.Map;
+
+/**
+ * A {@link Runner} that can TestNG tests.
+ *
+ * <p>Implementation note: Avoid extending ParentRunner since that also has
+ * logic to handle BeforeClass/AfterClass and other junit-specific functionality
+ * that would be invalid for TestNG.</p>
+ */
+class TestNgRunner extends Runner implements Filterable {
+
+  private static final boolean DEBUG = false;
+
+  private Description mDescription;
+  /** Class name for debugging. */
+  private String mClassName;
+  /** Don't include the same method names twice. */
+  private HashSet<String> mMethodSet = new HashSet<>();
+
+  /**
+   * @param testClass the test class to run
+   */
+  TestNgRunner(Class<?> testClass) {
+    mDescription = generateTestNgDescription(testClass);
+    mClassName = testClass.getName();
+  }
+
+  // Runner implementation
+  @Override
+  public Description getDescription() {
+    return mDescription;
+  }
+
+  // Runner implementation
+  @Override
+  public int testCount() {
+    if (!descriptionHasChildren(getDescription())) {  // Avoid NPE when description is null.
+      return 0;
+    }
+
+    // We always follow a flat Parent->Leaf hierarchy, so no recursion necessary.
+    return getDescription().testCount();
+  }
+
+  // Filterable implementation
+  @Override
+  public void filter(Filter filter) throws NoTestsRemainException {
+    mDescription = filterDescription(mDescription, filter);
+
+    if (!descriptionHasChildren(getDescription())) {  // Avoid NPE when description is null.
+      if (DEBUG) {
+        Log.d("TestNgRunner",
+            "Filtering has removed all tests :( for class " + mClassName);
+      }
+      throw new NoTestsRemainException();
+    }
+
+    if (DEBUG) {
+      Log.d("TestNgRunner",
+          "Filtering has retained " + testCount() + " tests for class " + mClassName);
+    }
+  }
+
+  // Filterable implementation
+  @Override
+  public void run(RunNotifier notifier) {
+    if (!descriptionHasChildren(getDescription())) {  // Avoid NPE when description is null.
+      // Nothing to do.
+      return;
+    }
+
+    for (Description child : getDescription().getChildren()) {
+      String className = child.getClassName();
+      String methodName = child.getMethodName();
+
+      Class<?> klass;
+      try {
+        klass = Class.forName(className, false, Thread.currentThread().getContextClassLoader());
+      } catch (ClassNotFoundException e) {
+        throw new AssertionError(e);
+      }
+
+      notifier.fireTestStarted(child);
+
+      // Avoid looking at all the methods by just using the string method name.
+      SingleTestNgTestExecutor.Result result = SingleTestNgTestExecutor.execute(klass, methodName);
+      if (result.hasFailure()) {
+        // TODO: get the error messages from testng somehow.
+        notifier.fireTestFailure(new Failure(child, extractException(result.getFailures())));
+      }
+
+      notifier.fireTestFinished(child);
+      // TODO: Check @Test(enabled=false) and invoke #fireTestIgnored instead.
+    }
+  }
+
+  private Throwable extractException(Map<String, Throwable> failures) {
+    if (failures.isEmpty()) {
+      return new AssertionError();
+    }
+    if (failures.size() == 1) {
+      return failures.values().iterator().next();
+    }
+
+    StringBuilder errorMessage = new StringBuilder("========== Multiple Failures ==========");
+    for (Map.Entry<String, Throwable> failureEntry : failures.entrySet()) {
+      errorMessage.append("\n\n=== "). append(failureEntry.getKey()).append(" ===\n");
+      Throwable throwable = failureEntry.getValue();
+      errorMessage
+              .append(throwable.getClass()).append(": ")
+              .append(throwable.getMessage());
+      for (StackTraceElement e : throwable.getStackTrace()) {
+        if (e.getClassName().equals(getClass().getName())) {
+          break;
+        }
+        errorMessage.append("\n  at ").append(e);
+      }
+    }
+    errorMessage.append("\n=======================================\n\n");
+    return new AssertionError(errorMessage.toString());
+  }
+
+
+  /**
+   * Recursively (preorder traversal) apply the filter to all the descriptions.
+   *
+   * @return null if the filter rejects the whole tree.
+   */
+  private static Description filterDescription(Description desc, Filter filter) {
+    if (!filter.shouldRun(desc)) {  // XX: Does the filter itself do the recursion?
+      return null;
+    }
+
+    Description newDesc = desc.childlessCopy();
+
+    // Return leafs.
+    if (!descriptionHasChildren(desc)) {
+      return newDesc;
+    }
+
+    // Filter all subtrees, only copying them if the filter accepts them.
+    for (Description child : desc.getChildren()) {
+      Description filteredChild = filterDescription(child, filter);
+
+      if (filteredChild != null) {
+        newDesc.addChild(filteredChild);
+      }
+    }
+
+    return newDesc;
+  }
+
+  private Description generateTestNgDescription(Class<?> cls) {
+    // Add the overall class description as the parent.
+    Description parent = Description.createSuiteDescription(cls);
+
+    if (DEBUG) {
+      Log.d("TestNgRunner", "Generating TestNg Description for class " + cls.getName());
+    }
+
+    // Add each test method as a child.
+    for (Method m : cls.getDeclaredMethods()) {
+
+      // Filter to only 'public void' signatures.
+      if ((m.getModifiers() & Modifier.PUBLIC) == 0) {
+        continue;
+      }
+
+      if (!m.getReturnType().equals(Void.TYPE)) {
+        continue;
+      }
+
+      // Note that TestNG methods may actually have parameters
+      // (e.g. with @DataProvider) which TestNG will populate itself.
+
+      // Add [Class, MethodName] as a Description leaf node.
+      String name = m.getName();
+
+      if (!mMethodSet.add(name)) {
+        // Overloaded methods have the same name, don't add them twice.
+        if (DEBUG) {
+          Log.d("TestNgRunner", "Already added child " + cls.getName() + "#" + name);
+        }
+        continue;
+      }
+
+      Description child = Description.createTestDescription(cls, name);
+
+      parent.addChild(child);
+
+      if (DEBUG) {
+        Log.d("TestNgRunner", "Add child " + cls.getName() + "#" + name);
+      }
+    }
+
+    return parent;
+  }
+
+  private static boolean descriptionHasChildren(Description desc) {
+    // Note: Although "desc.isTest()" is equivalent to "!desc.getChildren().isEmpty()"
+    // we add the pre-requisite 2 extra null checks to avoid throwing NPEs.
+    return desc != null && desc.getChildren() != null && !desc.getChildren().isEmpty();
+  }
+}
diff --git a/tests/core/runner-axt/src/com/android/cts/core/runner/support/TestNgRunnerBuilder.java b/tests/core/runner-axt/src/com/android/cts/core/runner/support/TestNgRunnerBuilder.java
new file mode 100644
index 0000000..2f084b3
--- /dev/null
+++ b/tests/core/runner-axt/src/com/android/cts/core/runner/support/TestNgRunnerBuilder.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.cts.core.runner.support;
+
+import java.lang.reflect.Method;
+
+import org.junit.runner.Runner;
+import org.junit.runners.model.RunnerBuilder;
+
+import org.testng.annotations.Test;
+
+/**
+ * A {@link RunnerBuilder} that can handle TestNG tests.
+ */
+public class TestNgRunnerBuilder extends RunnerBuilder {
+  // Returns a TestNG runner for this class, only if it is a class
+  // annotated with testng's @Test or has any methods with @Test in it.
+  @Override
+  public Runner runnerForClass(Class<?> testClass) {
+    if (isTestNgTestClass(testClass)) {
+      return new TestNgRunner(testClass);
+    }
+
+    return null;
+  }
+
+  private static boolean isTestNgTestClass(Class<?> cls) {
+    // TestNG test is either marked @Test at the class
+    if (cls.getAnnotation(Test.class) != null) {
+      return true;
+    }
+
+    // Or It's marked @Test at the method level
+    for (Method m : cls.getDeclaredMethods()) {
+      if (m.getAnnotation(Test.class) != null) {
+        return true;
+      }
+    }
+
+    return false;
+  }
+}
diff --git a/tests/core/runner-axt/src/com/android/cts/runner/CtsTestRunListener.java b/tests/core/runner-axt/src/com/android/cts/runner/CtsTestRunListener.java
new file mode 100644
index 0000000..abda5a7
--- /dev/null
+++ b/tests/core/runner-axt/src/com/android/cts/runner/CtsTestRunListener.java
@@ -0,0 +1,262 @@
+/*
+ * Copyright (C) 2014 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.cts.runner;
+
+import android.app.ActivityManager;
+import android.app.Instrumentation;
+import android.app.KeyguardManager;
+import android.content.Context;
+import android.content.pm.PackageManager;
+import androidx.test.internal.runner.listener.InstrumentationRunListener;
+import android.text.TextUtils;
+import android.util.Log;
+
+import junit.framework.TestCase;
+
+import org.junit.runner.Description;
+import org.junit.runner.notification.RunListener;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.lang.Class;
+import java.lang.ReflectiveOperationException;
+import java.lang.reflect.Field;
+import java.lang.reflect.Modifier;
+import java.net.Authenticator;
+import java.net.CookieHandler;
+import java.net.ResponseCache;
+import java.text.DateFormat;
+import java.util.Locale;
+import java.util.Properties;
+import java.util.TimeZone;
+
+import javax.net.ssl.HostnameVerifier;
+import javax.net.ssl.HttpsURLConnection;
+import javax.net.ssl.SSLSocketFactory;
+
+/**
+ * A {@link RunListener} for CTS. Sets the system properties necessary for many
+ * core tests to run. This is needed because there are some core tests that need
+ * writing access to the file system.
+ * Finally, we add a means to free memory allocated by a TestCase after its
+ * execution.
+ */
+public class CtsTestRunListener extends InstrumentationRunListener {
+
+    private static final String TAG = "CtsTestRunListener";
+
+    private TestEnvironment mEnvironment;
+    private Class<?> lastClass;
+
+    @Override
+    public void testRunStarted(Description description) throws Exception {
+        mEnvironment = new TestEnvironment(getInstrumentation().getTargetContext());
+
+        // We might want to move this to /sdcard, if is is mounted/writable.
+        File cacheDir = getInstrumentation().getTargetContext().getCacheDir();
+        System.setProperty("java.io.tmpdir", cacheDir.getAbsolutePath());
+
+        // attempt to disable keyguard, if current test has permission to do so
+        // TODO: move this to a better place, such as InstrumentationTestRunner
+        // ?
+        if (getInstrumentation().getContext().checkCallingOrSelfPermission(
+                android.Manifest.permission.DISABLE_KEYGUARD)
+                == PackageManager.PERMISSION_GRANTED) {
+            Log.i(TAG, "Disabling keyguard");
+            KeyguardManager keyguardManager =
+                    (KeyguardManager) getInstrumentation().getContext().getSystemService(
+                            Context.KEYGUARD_SERVICE);
+            keyguardManager.newKeyguardLock("cts").disableKeyguard();
+        } else {
+            Log.i(TAG, "Test lacks permission to disable keyguard. " +
+                    "UI based tests may fail if keyguard is up");
+        }
+    }
+
+    @Override
+    public void testStarted(Description description) throws Exception {
+        if (description.getTestClass() != lastClass) {
+            lastClass = description.getTestClass();
+            printMemory(description.getTestClass());
+        }
+
+        mEnvironment.reset();
+    }
+
+    @Override
+    public void testFinished(Description description) {
+        // no way to implement this in JUnit4...
+        // offending test cases that need this logic should probably be cleaned
+        // up individually
+        // if (test instanceof TestCase) {
+        // cleanup((TestCase) test);
+        // }
+    }
+
+    /**
+     * Dumps some memory info.
+     */
+    private void printMemory(Class<?> testClass) {
+        Runtime runtime = Runtime.getRuntime();
+
+        long total = runtime.totalMemory();
+        long free = runtime.freeMemory();
+        long used = total - free;
+
+        Log.d(TAG, "Total memory  : " + total);
+        Log.d(TAG, "Used memory   : " + used);
+        Log.d(TAG, "Free memory   : " + free);
+
+        String tempdir = System.getProperty("java.io.tmpdir", "");
+        // TODO: Remove these extra Logs added to debug a specific timeout problem.
+        Log.d(TAG, "java.io.tmpdir is:" + tempdir);
+
+        if (!TextUtils.isEmpty(tempdir)) {
+            String[] commands = {"df", tempdir};
+            BufferedReader in = null;
+            try {
+                Log.d(TAG, "About to .exec df");
+                Process proc = runtime.exec(commands);
+                Log.d(TAG, ".exec returned");
+                in = new BufferedReader(new InputStreamReader(proc.getInputStream()));
+                Log.d(TAG, "Stream reader created");
+                String line;
+                while ((line = in.readLine()) != null) {
+                    Log.d(TAG, line);
+                }
+            } catch (IOException e) {
+                Log.d(TAG, "Exception: " + e.toString());
+                // Well, we tried
+            } finally {
+                Log.d(TAG, "In finally");
+                if (in != null) {
+                    try {
+                        in.close();
+                    } catch (IOException e) {
+                        // Meh
+                    }
+                }
+            }
+        }
+
+        Log.d(TAG, "Now executing : " + testClass.getName());
+    }
+
+    /**
+     * Nulls all non-static reference fields in the given test class. This
+     * method helps us with those test classes that don't have an explicit
+     * tearDown() method. Normally the garbage collector should take care of
+     * everything, but since JUnit keeps references to all test cases, a little
+     * help might be a good idea.
+     */
+    private void cleanup(TestCase test) {
+        Class<?> clazz = test.getClass();
+
+        while (clazz != TestCase.class) {
+            Field[] fields = clazz.getDeclaredFields();
+            for (int i = 0; i < fields.length; i++) {
+                Field f = fields[i];
+                if (!f.getType().isPrimitive() &&
+                        !Modifier.isStatic(f.getModifiers())) {
+                    try {
+                        f.setAccessible(true);
+                        f.set(test, null);
+                    } catch (Exception ignored) {
+                        // Nothing we can do about it.
+                    }
+                }
+            }
+
+            clazz = clazz.getSuperclass();
+        }
+    }
+
+    // http://code.google.com/p/vogar/source/browse/trunk/src/vogar/target/TestEnvironment.java
+    static class TestEnvironment {
+        private static final Field sDateFormatIs24HourField;
+        static {
+            try {
+                Class<?> dateFormatClass = Class.forName("java.text.DateFormat");
+                sDateFormatIs24HourField = dateFormatClass.getDeclaredField("is24Hour");
+            } catch (ReflectiveOperationException e) {
+                throw new AssertionError("Missing DateFormat.is24Hour", e);
+            }
+        }
+
+        private final Locale mDefaultLocale;
+        private final TimeZone mDefaultTimeZone;
+        private final HostnameVerifier mHostnameVerifier;
+        private final SSLSocketFactory mSslSocketFactory;
+        private final Properties mProperties = new Properties();
+        private final Boolean mDefaultIs24Hour;
+
+        TestEnvironment(Context context) {
+            mDefaultLocale = Locale.getDefault();
+            mDefaultTimeZone = TimeZone.getDefault();
+            mHostnameVerifier = HttpsURLConnection.getDefaultHostnameVerifier();
+            mSslSocketFactory = HttpsURLConnection.getDefaultSSLSocketFactory();
+
+            mProperties.setProperty("user.home", "");
+            mProperties.setProperty("java.io.tmpdir", context.getCacheDir().getAbsolutePath());
+            // The CDD mandates that devices that support WiFi are the only ones that will have
+            // multicast.
+            PackageManager pm = context.getPackageManager();
+            mProperties.setProperty("android.cts.device.multicast",
+                    Boolean.toString(pm.hasSystemFeature(PackageManager.FEATURE_WIFI)));
+            mDefaultIs24Hour = getDateFormatIs24Hour();
+
+            // There are tests in libcore that should be disabled for low ram devices. They can't
+            // access ActivityManager to call isLowRamDevice, but can read system properties.
+            ActivityManager activityManager =
+                    (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
+            mProperties.setProperty("android.cts.device.lowram",
+                    Boolean.toString(activityManager.isLowRamDevice()));
+        }
+
+        void reset() {
+            System.setProperties(null);
+            System.setProperties(mProperties);
+            Locale.setDefault(mDefaultLocale);
+            TimeZone.setDefault(mDefaultTimeZone);
+            Authenticator.setDefault(null);
+            CookieHandler.setDefault(null);
+            ResponseCache.setDefault(null);
+            HttpsURLConnection.setDefaultHostnameVerifier(mHostnameVerifier);
+            HttpsURLConnection.setDefaultSSLSocketFactory(mSslSocketFactory);
+            setDateFormatIs24Hour(mDefaultIs24Hour);
+        }
+
+        private static Boolean getDateFormatIs24Hour() {
+            try {
+                return (Boolean) sDateFormatIs24HourField.get(null);
+            } catch (ReflectiveOperationException e) {
+                throw new AssertionError("Unable to get java.text.DateFormat.is24Hour", e);
+            }
+        }
+
+        private static void setDateFormatIs24Hour(Boolean value) {
+            try {
+                sDateFormatIs24HourField.set(null, value);
+            } catch (ReflectiveOperationException e) {
+                throw new AssertionError("Unable to set java.text.DateFormat.is24Hour", e);
+            }
+        }
+    }
+
+}