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 >= 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);
+ }
+ }
+ }
+
+}