aboutsummaryrefslogtreecommitdiff
path: root/tools/tradefed-host/src/com/android/afwtest
diff options
context:
space:
mode:
Diffstat (limited to 'tools/tradefed-host/src/com/android/afwtest')
-rw-r--r--tools/tradefed-host/src/com/android/afwtest/tradefed/TestConfig.java186
-rw-r--r--tools/tradefed-host/src/com/android/afwtest/tradefed/targetprep/AfwTestAdbRootOptionPreparer.java62
-rw-r--r--tools/tradefed-host/src/com/android/afwtest/tradefed/targetprep/AfwTestAppUninstaller.java110
-rw-r--r--tools/tradefed-host/src/com/android/afwtest/tradefed/targetprep/AfwTestEncryptDevice.java155
-rw-r--r--tools/tradefed-host/src/com/android/afwtest/tradefed/targetprep/AfwTestEnvDumper.java183
-rw-r--r--tools/tradefed-host/src/com/android/afwtest/tradefed/targetprep/AfwTestFactoryReset.java179
-rw-r--r--tools/tradefed-host/src/com/android/afwtest/tradefed/targetprep/AfwTestGoogleAccountRemover.java201
-rw-r--r--tools/tradefed-host/src/com/android/afwtest/tradefed/targetprep/AfwTestManagedProfileCreator.java89
-rw-r--r--tools/tradefed-host/src/com/android/afwtest/tradefed/targetprep/AfwTestPrivAppInstaller.java118
-rw-r--r--tools/tradefed-host/src/com/android/afwtest/tradefed/targetprep/AfwTestPullExternalFile.java78
-rw-r--r--tools/tradefed-host/src/com/android/afwtest/tradefed/targetprep/AfwTestSyncTime.java45
-rw-r--r--tools/tradefed-host/src/com/android/afwtest/tradefed/targetprep/AfwTestTargetPreparer.java204
-rw-r--r--tools/tradefed-host/src/com/android/afwtest/tradefed/targetprep/AfwTestUserRemover.java114
-rw-r--r--tools/tradefed-host/src/com/android/afwtest/tradefed/targetprep/AfwTestWifiPreparer.java305
-rw-r--r--tools/tradefed-host/src/com/android/afwtest/tradefed/targetprep/metrics/AfwTestMetricsCollectorTargetPreparer.java216
-rw-r--r--tools/tradefed-host/src/com/android/afwtest/tradefed/targetprep/metrics/LogcatMetricsCollector.java171
-rw-r--r--tools/tradefed-host/src/com/android/afwtest/tradefed/targetprep/metrics/MemoryMetricsCollector.java81
-rw-r--r--tools/tradefed-host/src/com/android/afwtest/tradefed/targetprep/metrics/MetricsCollector.java45
-rw-r--r--tools/tradefed-host/src/com/android/afwtest/tradefed/targetprep/metrics/MetricsCollectorException.java48
-rw-r--r--tools/tradefed-host/src/com/android/afwtest/tradefed/targetprep/metrics/MetricsUtils.java48
-rw-r--r--tools/tradefed-host/src/com/android/afwtest/tradefed/targetprep/metrics/memory/MemoryByProcessGrouper.java56
-rw-r--r--tools/tradefed-host/src/com/android/afwtest/tradefed/targetprep/metrics/memory/MemoryByUserGrouper.java61
-rw-r--r--tools/tradefed-host/src/com/android/afwtest/tradefed/targetprep/metrics/memory/MemoryCollectorTask.java110
-rw-r--r--tools/tradefed-host/src/com/android/afwtest/tradefed/targetprep/metrics/memory/MemoryInfoGrouper.java49
-rw-r--r--tools/tradefed-host/src/com/android/afwtest/tradefed/targetprep/metrics/metrics.proto100
-rw-r--r--tools/tradefed-host/src/com/android/afwtest/tradefed/testtype/AfwAndroidJUnitTest.java54
-rw-r--r--tools/tradefed-host/src/com/android/afwtest/tradefed/testtype/AfwTest.java161
-rw-r--r--tools/tradefed-host/src/com/android/afwtest/tradefed/testtype/ScreenshotListener.java93
-rw-r--r--tools/tradefed-host/src/com/android/afwtest/tradefed/utils/AdbShellUtils.java191
-rw-r--r--tools/tradefed-host/src/com/android/afwtest/tradefed/utils/ReflectionUtils.java75
-rw-r--r--tools/tradefed-host/src/com/android/afwtest/tradefed/utils/StreamDiscarder.java56
-rw-r--r--tools/tradefed-host/src/com/android/afwtest/tradefed/utils/TimeUtil.java66
32 files changed, 3710 insertions, 0 deletions
diff --git a/tools/tradefed-host/src/com/android/afwtest/tradefed/TestConfig.java b/tools/tradefed-host/src/com/android/afwtest/tradefed/TestConfig.java
new file mode 100644
index 0000000..1b1dbbc
--- /dev/null
+++ b/tools/tradefed-host/src/com/android/afwtest/tradefed/TestConfig.java
@@ -0,0 +1,186 @@
+/*
+ * 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.afwtest.tradefed;
+
+import com.android.tradefed.log.LogUtil.CLog;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Properties;
+
+/**
+ * A singleton class that reads properties from afw-test.props and provides interfaces
+ * to get test configurations.
+ */
+public class TestConfig {
+ /**
+ * Property key of factory reset timeout in minutes.
+ */
+ public static final String KEY_FACTORY_RESET_TIMEOUT_MIN = "factory_reset_timeout_min";
+
+ /**
+ * Property key of Timeout size, used in the test configuration file.
+ */
+ public static final String KEY_TIMEOUT_SIZE = "timeout_size";
+
+ /**
+ * Mapping from timeout size to its integer value.
+ */
+ private static final Map<String, Integer> TIMEOUT_SIZE_MAPPING =
+ new HashMap<String, Integer>() {{
+ put("S", 1);
+ put("M", 2);
+ put("L", 3);
+ put("XL", 5);
+ put("XXL", 8);
+ }};
+
+ /**
+ * Property key of test timeout in minute, used in the test configuration file.
+ */
+ public static final String KEY_TEST_TIMEOUT_MIN = "test_timeout_min";
+
+ /**
+ * Singleton of this class.
+ */
+ private static TestConfig sTestConfig;
+
+ // Stores configurations loaded from a file.
+ private final Properties mProps;
+
+ /**
+ * Creates a new object by loading configurations from file.
+ *
+ * @param configFile test configuration file
+ */
+ private TestConfig(File configFile) throws IOException {
+ mProps = new Properties();
+ mProps.load(new FileInputStream(configFile));
+ }
+
+ /**
+ * Inits a {@link TestConfig} from a file.
+ *
+ * @param configFile configuration file to read
+ */
+ public static void init(File configFile) throws IOException {
+ sTestConfig = new TestConfig(configFile);
+ }
+
+ /**
+ * Gets the singleton of this class.
+ *
+ * @return {@link TestConfig} singleton, null if it's not initialized.
+ */
+ public static TestConfig getInstance() {
+ return sTestConfig;
+ }
+
+ /**
+ * Gets the property of a specific key.
+ *
+ * @param key property key
+ * @return propery value of the given key
+ * If the key doesn't exist in the configuration file, null will be returned;
+ * If the value of the key is empty, empty string will be returned
+ */
+ public String getProperty(String key) {
+ return mProps.getProperty(key);
+ }
+
+ /**
+ * Gets the property of a specific key.
+ *
+ * @param key property key
+ * @param defaultValue default value if given key is not found
+ * @return property value of the given key;
+ * if given key not found, {@link #defaultValue} is returned
+ */
+ public String getProperty(String key, String defaultValue) {
+ return mProps.getProperty(key, defaultValue);
+ }
+
+ /**
+ * Gets factory reset timeout in minute.
+ *
+ * @return timeout in minute or -1 if either timeout is not specified or invalid
+ */
+ public int getFactoryResetTimeoutMin() {
+ return getFactoryResetTimeoutMin(-1);
+ }
+
+ /**
+ * Gets factory reset timeout in minute.
+ *
+ * @param defaultValue default value if test timeout not specified
+ * @return factory reset timeout in minute
+ */
+ public int getFactoryResetTimeoutMin(int defaultValue) {
+ String value = mProps.getProperty(KEY_FACTORY_RESET_TIMEOUT_MIN);
+ if (value == null || value.isEmpty()) {
+ return defaultValue;
+ }
+
+ return Integer.valueOf(value);
+ }
+
+ /**
+ * Gets timeout size value from props file.
+ *
+ * <p>Possible size values are strings "S", "M", "L", "XL" or "XXL".</p>
+ *
+ * @return integer value of the timeout size.
+ */
+ public int getTimeoutSize() {
+ // Default to M
+ String timeoutSize = getProperty(KEY_TIMEOUT_SIZE, "M");
+ if (!TIMEOUT_SIZE_MAPPING.containsKey(timeoutSize)) {
+ CLog.w("Invalid timeout size, defaulting to M");
+ timeoutSize = "M";
+ }
+
+ return TIMEOUT_SIZE_MAPPING.get(timeoutSize);
+ }
+
+ /**
+ * Gets test timeout in minute.
+ *
+ * @return test timeout in minute or -1 if either timeout is not specified or invalid
+ */
+ public int getTestTimeoutMin() {
+ return getTestTimeoutMin(-1);
+ }
+
+ /**
+ * Gets test timeout in minute.
+ *
+ * @param defaultValue default value if test timeout not specified
+ * @return test timeout in minute
+ */
+ public int getTestTimeoutMin(int defaultValue) {
+ String value = mProps.getProperty(KEY_TEST_TIMEOUT_MIN);
+ if (value == null || value.isEmpty()) {
+ return defaultValue;
+ }
+
+ return Integer.valueOf(value);
+ }
+}
+
diff --git a/tools/tradefed-host/src/com/android/afwtest/tradefed/targetprep/AfwTestAdbRootOptionPreparer.java b/tools/tradefed-host/src/com/android/afwtest/tradefed/targetprep/AfwTestAdbRootOptionPreparer.java
new file mode 100644
index 0000000..74c4480
--- /dev/null
+++ b/tools/tradefed-host/src/com/android/afwtest/tradefed/targetprep/AfwTestAdbRootOptionPreparer.java
@@ -0,0 +1,62 @@
+/*
+ * 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.afwtest.tradefed.targetprep;
+
+import com.android.tradefed.build.IBuildInfo;
+import com.android.tradefed.config.Option;
+import com.android.tradefed.config.OptionClass;
+import com.android.tradefed.device.DeviceNotAvailableException;
+import com.android.tradefed.device.ITestDevice;
+import com.android.tradefed.targetprep.ITargetCleaner;
+import com.android.tradefed.targetprep.TargetSetupError;
+
+/**
+ * A {@link ITargetPreparer} that sets enable-root option of the testing device.
+ */
+@OptionClass(alias = "afw-test-adb-root-option")
+public class AfwTestAdbRootOptionPreparer implements ITargetCleaner {
+
+ @Option(name = "enable-root-option",
+ description = "Whether to enable adb root option on the testing device.")
+ private boolean mEnableAdbRootOption = true;
+
+ /**
+ * Original adb root option.
+ */
+ private Boolean mOriginalRootOption = null;
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void setUp(ITestDevice device, IBuildInfo buildInfo) throws TargetSetupError,
+ DeviceNotAvailableException {
+ mOriginalRootOption = device.getOptions().isEnableAdbRoot();
+ device.getOptions().setEnableAdbRoot(mEnableAdbRootOption);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void tearDown(ITestDevice device, IBuildInfo buildInfo, Throwable e)
+ throws DeviceNotAvailableException {
+ if (mOriginalRootOption != null) {
+ device.getOptions().setEnableAdbRoot(mOriginalRootOption);
+ }
+ }
+}
diff --git a/tools/tradefed-host/src/com/android/afwtest/tradefed/targetprep/AfwTestAppUninstaller.java b/tools/tradefed-host/src/com/android/afwtest/tradefed/targetprep/AfwTestAppUninstaller.java
new file mode 100644
index 0000000..599e993
--- /dev/null
+++ b/tools/tradefed-host/src/com/android/afwtest/tradefed/targetprep/AfwTestAppUninstaller.java
@@ -0,0 +1,110 @@
+/*
+ * 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.afwtest.tradefed.targetprep;
+
+import com.android.tradefed.build.IBuildInfo;
+import com.android.tradefed.config.Option;
+import com.android.tradefed.config.OptionClass;
+import com.android.tradefed.device.DeviceNotAvailableException;
+import com.android.tradefed.device.ITestDevice;
+import com.android.tradefed.log.LogUtil.CLog;
+import com.android.tradefed.targetprep.ITargetCleaner;
+import com.android.tradefed.targetprep.TargetSetupError;
+
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * A {@link ITargetPreparer} that helps uninstalling apps before or after the test.
+ */
+@OptionClass(alias="afw-test-app-uninstaller")
+public class AfwTestAppUninstaller implements ITargetCleaner {
+
+ @Option(name = "before-test", description = "packages to uninstall before test")
+ private List<String> mPackageNamesBeforeTest = new LinkedList<>();
+
+ @Option(name = "after-test", description = "packages to uninstall after test")
+ private List<String> mPackageNamesAfterTest = new LinkedList<>();
+
+ @Option(name = "reboot-before-uninstall",
+ description = "if reboot the device before uinstallation")
+ private boolean mRebootBeforeUninstall = false;
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void setUp(ITestDevice device, IBuildInfo buildInfo)
+ throws TargetSetupError, DeviceNotAvailableException {
+ String result = uninstall(device, mPackageNamesBeforeTest);
+
+ if (result != null) {
+ throw new TargetSetupError(String.format("Failed to uninstall %s on %s",
+ result, device.getSerialNumber()));
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void tearDown(ITestDevice device, IBuildInfo buildInfo, Throwable e)
+ throws DeviceNotAvailableException {
+
+ String result = uninstall(device, mPackageNamesAfterTest);
+
+ if (result != null) {
+ throw new RuntimeException(String.format("Failed to uninstall %s on %s",
+ result, device.getSerialNumber()));
+ }
+ }
+
+ /**
+ * Uninstalls list of packages.
+ *
+ * @param device the testing device
+ * @param packages packages to uninstall
+ * @return {@code null} if all given packages are uninstalled, or the name of the first package
+ * that failed to be uninstalled
+ */
+ private String uninstall(ITestDevice device, List<String> packages)
+ throws DeviceNotAvailableException {
+ Set<String> installedPackages = device.getInstalledPackageNames();
+ installedPackages.retainAll(packages);
+
+ if (installedPackages.isEmpty()) {
+ return null;
+ }
+
+ if (mRebootBeforeUninstall) {
+ device.reboot();
+ }
+
+ for (String pkgName : installedPackages) {
+ String result = device.uninstallPackage(pkgName);
+ if (result != null) {
+ return pkgName;
+ }
+
+ CLog.i(String.format("Successfully uninstalled %s on %s",
+ pkgName, device.getSerialNumber()));
+ }
+
+ return null;
+ }
+}
diff --git a/tools/tradefed-host/src/com/android/afwtest/tradefed/targetprep/AfwTestEncryptDevice.java b/tools/tradefed-host/src/com/android/afwtest/tradefed/targetprep/AfwTestEncryptDevice.java
new file mode 100644
index 0000000..7de4ace
--- /dev/null
+++ b/tools/tradefed-host/src/com/android/afwtest/tradefed/targetprep/AfwTestEncryptDevice.java
@@ -0,0 +1,155 @@
+/*
+ * 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.afwtest.tradefed.targetprep;
+
+import com.android.tradefed.build.IBuildInfo;
+import com.android.tradefed.config.Option;
+import com.android.tradefed.config.OptionClass;
+import com.android.tradefed.device.DeviceNotAvailableException;
+import com.android.tradefed.device.ITestDevice;
+import com.android.tradefed.log.LogUtil.CLog;
+import com.android.tradefed.targetprep.ITargetPreparer;
+import com.android.tradefed.targetprep.TargetSetupError;
+import com.android.tradefed.device.CollectingOutputReceiver;
+import com.android.ddmlib.NullOutputReceiver;
+
+
+import java.util.concurrent.TimeUnit;
+
+/**
+ * A {@link ITargetPreparer} that encrypts the testing device.
+ *
+ * <p>Requires a device where 'adb root' is possible, typically a userdebug build type.</p>
+ *
+ * <p>If the device is already encrypted, nothing will be done. </p>
+ */
+@OptionClass(alias = "afw-test-encrypt-device")
+public class AfwTestEncryptDevice extends AfwTestTargetPreparer implements ITargetPreparer {
+
+ @Option(name = "inplace",
+ description = "Whether to encrypt the device inplace or by wipe")
+ private Boolean mInplace = true;
+
+ @Option(name = "timeout-secs",
+ description = "Timeout in seconds.")
+ private Long mTimeoutSecs = TimeUnit.MINUTES.toSeconds(5);
+
+ @Option(name = "attempts",
+ description = "Number of attempts to encrypt the device")
+ private int mAttempts = 3;
+
+ /** Adb command timeout, in milliseconds. */
+ private static final int CMD_TIMEOUT = (int)TimeUnit.MINUTES.toMillis(2);
+
+ /** The password for encrypting and decrypting the device. */
+ private static final String ENCRYPTION_PASSWORD = "android";
+
+ /** Encrypting with inplace can take up to 2 hours. */
+ private static final int ENCRYPTION_INPLACE_TIMEOUT_MIN = 2 * 60;
+
+ /** Encrypting with wipe can take up to 20 minutes. */
+ private static final long ENCRYPTION_WIPE_TIMEOUT_MIN = 20;
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void setUp(ITestDevice device, IBuildInfo buildInfo) throws TargetSetupError,
+ DeviceNotAvailableException {
+
+ for (int i = 0; i < mAttempts; ++i) {
+ CLog.i(String.format("Encrypting device %s: #%d", device.getSerialNumber(), i+1));
+
+ // Exit from loop if the device is encrypted successfully
+ if (encryptDevice(device)) {
+ break;
+ }
+
+ // The device may become unavailable, wait for it.
+ device.waitForDeviceAvailable(TimeUnit.SECONDS.toMillis(10));
+ }
+
+ // Fail the target preparer if encryption failed
+ if (!device.isDeviceEncrypted()) {
+ throw new TargetSetupError(
+ "Failed to encrypt device " + device.getSerialNumber());
+ }
+
+ device.waitForDeviceAvailable(TimeUnit.SECONDS.toMillis(mTimeoutSecs) * getTimeoutSize());
+ CLog.i(String.format("Device %s is encrypted successfully", device.getSerialNumber()));
+
+ postDeviceEncryption(device);
+ }
+
+ /**
+ * Encrypts the device.
+ *
+ * @param device test device
+ * @return {@code true} if device was encrypted successfully; {@code false} otherwise
+ */
+ protected boolean encryptDevice(ITestDevice device) throws DeviceNotAvailableException {
+ if (!device.isEncryptionSupported()) {
+ throw new UnsupportedOperationException(String.format("Can't encrypt device %s: "
+ + "encryption not supported", device.getSerialNumber()));
+ }
+
+ if (device.isDeviceEncrypted()) {
+ CLog.d("Device %s is already encrypted, skipping...", device.getSerialNumber());
+ return true;
+ }
+
+ String encryptMethod;
+ long timeout;
+ if (mInplace) {
+ encryptMethod = "inplace";
+ timeout = ENCRYPTION_INPLACE_TIMEOUT_MIN;
+ } else {
+ encryptMethod = "wipe";
+ timeout = ENCRYPTION_WIPE_TIMEOUT_MIN;
+ }
+
+ CLog.i("Encrypting device %s via %s", device.getSerialNumber(), encryptMethod);
+
+ // enable crypto takes one of the following formats:
+ // cryptfs enablecrypto <wipe|inplace> <passwd>
+ // cryptfs enablecrypto <wipe|inplace> default|password|pin|pattern [passwd]
+ // Try the first one first, if it outputs "500 <process_id> Usage: ...", try the second.
+ CollectingOutputReceiver receiver = new CollectingOutputReceiver();
+ String command = String.format("vdc cryptfs enablecrypto %s \"%s\"", encryptMethod,
+ ENCRYPTION_PASSWORD);
+ device.executeShellCommand(command, receiver, timeout, TimeUnit.MINUTES, 1);
+ if (receiver.getOutput().split(":")[0].matches("500 \\d+ Usage")) {
+ command = String.format("vdc cryptfs enablecrypto %s default", encryptMethod);
+ device.executeShellCommand(command,
+ new NullOutputReceiver(), timeout, TimeUnit.MINUTES, 1);
+ }
+
+ device.waitForDeviceNotAvailable(CMD_TIMEOUT);
+ device.waitForDeviceOnline();
+
+ return device.isDeviceEncrypted();
+ }
+
+ /**
+ * Actions after encryption.
+ *
+ * @param device test device
+ */
+ protected void postDeviceEncryption(ITestDevice device) throws DeviceNotAvailableException {
+ // By default do nothing.
+ }
+}
diff --git a/tools/tradefed-host/src/com/android/afwtest/tradefed/targetprep/AfwTestEnvDumper.java b/tools/tradefed-host/src/com/android/afwtest/tradefed/targetprep/AfwTestEnvDumper.java
new file mode 100644
index 0000000..226dbb7
--- /dev/null
+++ b/tools/tradefed-host/src/com/android/afwtest/tradefed/targetprep/AfwTestEnvDumper.java
@@ -0,0 +1,183 @@
+/*
+ * 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.afwtest.tradefed.targetprep;
+
+import com.google.common.base.Joiner;
+
+import com.android.afwtest.tradefed.utils.TimeUtil;
+import com.android.tradefed.build.IBuildInfo;
+import com.android.tradefed.config.Option;
+import com.android.tradefed.config.OptionClass;
+import com.android.tradefed.device.DeviceNotAvailableException;
+import com.android.tradefed.device.ITestDevice;
+import com.android.tradefed.device.PackageInfo;
+import com.android.tradefed.log.LogUtil.CLog;
+import com.android.tradefed.targetprep.ITargetCleaner;
+import com.android.tradefed.targetprep.TargetSetupError;
+import com.android.tradefed.util.StreamUtil;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * A target preparer that dumps testing environment info to a text file.
+ *
+ * <p>
+ * The file is saved at:
+ * ${CTS_ROOT}/android-cts/repository/logs/{given-prefix}_EnvDump_{TimeStamp}.txt.
+ * Generally {given-prefix} should be the test apk file name.
+ * </p>
+ */
+@OptionClass(alias = "afw-test-env-dumper")
+public class AfwTestEnvDumper extends AfwTestTargetPreparer implements ITargetCleaner {
+
+ private static final String DUMP_FILE_NAME_SUFFIX = "EnvDump";
+ private static final String GET_ANDROID_BUILD_FINGERPRINT_CMD =
+ "getprop ro.build.fingerprint";
+
+ @Option(name = "file-name-prefix",
+ description = "Dump file name prefix, suggested to be test apk file name",
+ mandatory = true)
+ private String mFileNamePrefix = null;
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void setUp(ITestDevice device, IBuildInfo buildInfo)
+ throws TargetSetupError,
+ DeviceNotAvailableException {
+ // Do nothing, dump after the test only
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void tearDown(ITestDevice device, IBuildInfo buildInfo, Throwable e)
+ throws DeviceNotAvailableException {
+
+ try {
+ String str = getEnv(device);
+
+ // Creates the dump file
+ File dumpFile = getDumpFile(buildInfo);
+
+ OutputStream os = new FileOutputStream(dumpFile);
+ os.write(str.getBytes());
+ StreamUtil.flushAndCloseStream(os);
+
+ CLog.i(String.format("Dumped file: %s", dumpFile.getAbsolutePath()));
+ } catch (IOException exception) {
+ CLog.e("Failed to dump app versions to file", e);
+ }
+ }
+
+ /**
+ * Gets environment configurations as string.
+ *
+ * @param device testing device
+ * @return environment configuration as string
+ */
+ protected String getEnv(ITestDevice device) throws DeviceNotAvailableException {
+ String androidBuild = getAndroidBuildFingerprint(device);
+ String appVersions = getAppVersions(device);
+
+ return Joiner.on(System.lineSeparator()).join(androidBuild, appVersions);
+ }
+
+ /**
+ * Gets device's Android build fingerprint.
+ *
+ * @param device testing device
+ * @return device's Android build fingerprint.
+ * @throws DeviceNotAvailableException if connection to the device was lost during execution of
+ * the command.
+ */
+ private String getAndroidBuildFingerprint(ITestDevice device)
+ throws DeviceNotAvailableException {
+ return device.executeShellCommand(GET_ANDROID_BUILD_FINGERPRINT_CMD);
+ }
+
+ /**
+ * Gets a {@link File} to dump environment info.
+ *
+ * @param buildInfo reference to {@link IBuildInfo}
+ * @return {@link File} object of a unique file
+ */
+ private File getDumpFile(IBuildInfo buildInfo) throws FileNotFoundException {
+ return new File(getCtsBuildHelper(buildInfo).getLogsDir(),
+ String.format("%s_%s_%s.txt",
+ mFileNamePrefix,
+ DUMP_FILE_NAME_SUFFIX,
+ TimeUtil.getResultTimestamp()));
+ }
+
+ /**
+ * Gets versions of all {@link App} as a string.
+ *
+ * @param device reference to {@link ITestDevice}
+ * @return versions of all {@link App} as string
+ */
+ private String getAppVersions(ITestDevice device) throws DeviceNotAvailableException {
+ Set<String> installPkgs = device.getInstalledPackageNames();
+
+ List<String> appVersions = new ArrayList<String>();
+
+ for (Map.Entry<String, String> pkg: getPackagesToDump().entrySet()) {
+ String pkgName = pkg.getKey();
+ String appName = pkg.getValue();
+ // Gets versions of install app only
+ if (installPkgs.contains(pkgName)) {
+ PackageInfo pkgInfo = device.getAppPackageInfo(pkgName);
+ String appVer = String.format("%s: Ver %s", appName, pkgInfo.getVersionName());
+
+ // Log it to tradefed console for debugging purpose
+ CLog.i(appVer);
+
+ appVersions.add(appVer);
+ }
+ }
+
+ return Joiner.on("\n").join(appVersions.iterator());
+ }
+
+
+ /**
+ * Gets set of packages whose info should be dumped.
+ *
+ * @return a {@link Map} with key=full package name and value=app representation name
+ */
+ protected Map<String, String> getPackagesToDump() {
+ Map<String, String> pkgs = new HashMap<String, String>();
+
+ pkgs.put("com.google.android.gms", "Google Mobile Service");
+ pkgs.put("com.android.managedprovisioning", "Managed Provisioning");
+ pkgs.put("com.afwsamples.testdpc", "TestDPC");
+
+ return pkgs;
+ }
+}
+
diff --git a/tools/tradefed-host/src/com/android/afwtest/tradefed/targetprep/AfwTestFactoryReset.java b/tools/tradefed-host/src/com/android/afwtest/tradefed/targetprep/AfwTestFactoryReset.java
new file mode 100644
index 0000000..5ee82fa
--- /dev/null
+++ b/tools/tradefed-host/src/com/android/afwtest/tradefed/targetprep/AfwTestFactoryReset.java
@@ -0,0 +1,179 @@
+/*
+ * 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.afwtest.tradefed.targetprep;
+
+import com.android.afwtest.tradefed.utils.TimeUtil;
+import com.android.tradefed.build.IBuildInfo;
+import com.android.tradefed.config.Option;
+import com.android.tradefed.config.OptionClass;
+import com.android.tradefed.device.DeviceNotAvailableException;
+import com.android.tradefed.device.ITestDevice;
+import com.android.tradefed.log.LogUtil.CLog;
+import com.android.tradefed.targetprep.ITargetPreparer;
+import com.android.tradefed.targetprep.TargetSetupError;
+
+import java.util.concurrent.TimeUnit;
+
+/**
+ * A {@link ITargetPreparer} that factory resets the device by sending an intent
+ * to package com.android.afwtest.deviceadmin (AfwThDeviceAdmin.apk).
+ *
+ * <p>Requires a device where 'adb' is available after factory reset, typically
+ * a userdebug build type.
+ * </p>
+ */
+@OptionClass(alias = "afw-test-factory-reset")
+public class AfwTestFactoryReset extends AfwTestTargetPreparer implements ITargetPreparer {
+
+ @Option(name = "use-fastboot",
+ description = "Whether to use 'fastboot format' to do factory reset")
+ private boolean mUseFastboot = false;
+
+ @Option(name = "timeout-secs",
+ description = "Factory reset timeout, in seconds.")
+ private Long mTimeoutSecs = TimeUnit.MINUTES.toSeconds(15);
+
+ @Option(name = "attempts",
+ description = "Number of attempts to start factory reset")
+ private int mAttempts = 2;
+
+ @Option (name = "wipe-protection-data",
+ description = "If factory reset protection data should be wiped")
+ private boolean mWipeProtectionData = false;
+
+ // Command string to change system locale to en_US
+ private static final String CHANGE_LOCALE_TO_EN_US_CMD =
+ "am instrument -w -e locale en_US com.android.afwtest.systemutil/.ChangeLocale";
+
+ // After sending the factory reset intent out, time allowed for
+ // the device to start rebooting
+ private static final long REBOOT_WAIT_TIME_MS = TimeUnit.MINUTES.toMillis(2);
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void setUp(ITestDevice device, IBuildInfo buildInfo) throws TargetSetupError,
+ DeviceNotAvailableException {
+ if (getUseFastboot(device)) {
+ CLog.i("Factory resetting device via fastboot");
+ wipeDevice(device);
+ } else {
+ CLog.i("Factory resetting device via DeviceAdmin app");
+ doFactoryReset(device);
+ }
+
+ // Wait for the device to be available
+ long factoryResetTimeoutMs = getFactoryResetTimeout();
+ CLog.i(String.format("Waiting for factory reset to finish: timeout=%d seconds",
+ factoryResetTimeoutMs / 1000));
+ device.waitForDeviceAvailable(factoryResetTimeoutMs);
+
+ // re-enable adb root
+ enableAdbRoot(device);
+
+ CLog.i(String.format("Device %s is factory reset successfully", device.getSerialNumber()));
+
+ postFactoryReset(device);
+ }
+
+ /**
+ * Gets if configured to use fastboot to do factory reset.
+ *
+ * @return {@code true} if use fastboot, {@code false} otherwise
+ */
+ protected boolean getUseFastboot(ITestDevice device) throws DeviceNotAvailableException {
+ return mUseFastboot;
+ }
+
+ /**
+ * Executes actions after factory reset.
+ */
+ protected void postFactoryReset(ITestDevice device)
+ throws DeviceNotAvailableException, TargetSetupError {
+ // Sync time with the host
+ TimeUtil.syncHostTime(device);
+
+ CLog.i("Disabling auto-rotate");
+ device.executeShellCommand("settings put system accelerometer_rotation 0");
+
+ // Change system locale to en_US
+ CLog.i("Changing system locale to en_US");
+ String result = device.executeShellCommand(CHANGE_LOCALE_TO_EN_US_CMD);
+ if (result == null || !result.contains("result=SUCCESS")) {
+ CLog.e(result);
+ } else {
+ CLog.i("System locale changed to en_US");
+ }
+ }
+
+ /**
+ * Gets configured factory reset time out, in ms.
+ *
+ * @return factory reset timeout, in ms
+ */
+ private long getFactoryResetTimeout() {
+ long result = 0;
+ // Get from afw-test.props file
+ if (getTestConfig() != null) {
+ result = TimeUnit.MINUTES.toMillis(getTestConfig().getFactoryResetTimeoutMin(0));
+ }
+ // If not specified in afw-test.props, use mTimeoutSecs
+ if (result == 0) {
+ result = TimeUnit.SECONDS.toMillis(mTimeoutSecs);
+ }
+
+ return result;
+ }
+
+ /**
+ * Does factory reset with device admin.
+ *
+ * @param device test device
+ */
+ private void doFactoryReset(ITestDevice device)
+ throws DeviceNotAvailableException, TargetSetupError {
+ int count = 0;
+ boolean factoryResetStarted;
+ do {
+ count++;
+ CLog.i(String.format("Factory resetting device %s, #%d",
+ device.getSerialNumber(), count));
+
+ CLog.i(device.executeShellCommand(getFactoryResetCommand()));
+ factoryResetStarted = device.waitForDeviceNotAvailable(REBOOT_WAIT_TIME_MS);
+ } while (count < mAttempts && !factoryResetStarted);
+
+ // Wait for the device to reboot
+ if (!factoryResetStarted) {
+ throw new TargetSetupError(
+ "Failed to start factory reset on device " + device.getSerialNumber());
+ }
+ }
+
+ /**
+ * Gets factory reset command string.
+ */
+ private String getFactoryResetCommand() {
+ String result = "am start -n com.android.afwtest.deviceadmin/.FactoryResetActivity";
+ if (mWipeProtectionData) {
+ result += " --ez afwtest.wipe.protection.data true";
+ }
+ return result;
+ }
+}
+
diff --git a/tools/tradefed-host/src/com/android/afwtest/tradefed/targetprep/AfwTestGoogleAccountRemover.java b/tools/tradefed-host/src/com/android/afwtest/tradefed/targetprep/AfwTestGoogleAccountRemover.java
new file mode 100644
index 0000000..aee6e9d
--- /dev/null
+++ b/tools/tradefed-host/src/com/android/afwtest/tradefed/targetprep/AfwTestGoogleAccountRemover.java
@@ -0,0 +1,201 @@
+/*
+ * 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.afwtest.tradefed.targetprep;
+
+import com.android.tradefed.build.IBuildInfo;
+import com.android.tradefed.config.Option;
+import com.android.tradefed.config.OptionClass;
+import com.android.tradefed.device.DeviceNotAvailableException;
+import com.android.tradefed.device.ITestDevice;
+import com.android.tradefed.log.LogUtil.CLog;
+import com.android.tradefed.targetprep.ITargetCleaner;
+import com.android.tradefed.targetprep.TargetSetupError;
+import com.android.tradefed.util.RunUtil;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Target preparer to remove Google accounts.
+ *
+ * <p>Requires a device where 'adb root' is possible, typically a userdebug build.</p>
+ *
+ * <p>Requires com.anroid.afwtest.systemutil to be pre-installed as privileged app.</p>
+ */
+@OptionClass(alias = "afw-test-google-account-remover")
+public class AfwTestGoogleAccountRemover extends AfwTestTargetPreparer implements ITargetCleaner {
+
+ /**
+ * Package name of the system util app.
+ */
+ private static final String SYSTEM_UTIL_PKG_NAME = "com.android.afwtest.systemutil";
+
+ /**
+ * Action to remove Google account.
+ */
+ private static final String REMOVE_GOOGLE_ACCOUNT_CLASS = ".RemoveGoogleAccount";
+
+ /**
+ * Waiting time for system util app.
+ */
+ private static final long SYSTEM_UTIL_PKG_WAIT_TIME_MS = TimeUnit.MINUTES.toMillis(3);
+
+ @Option(name = "google-account",
+ description = "Google accounts to remove, remove all Google accounts if not none given")
+ private Collection<String> mGoogleAccounts = new ArrayList<String>();
+
+ @Option(name = "remove-before-test",
+ description = "Remove given Google account or all Google accounts before test.")
+ private boolean mRemoveBeforeTest = true;
+
+ @Option(name = "remove-after-test",
+ description = "Remove given Google account or all Google accounts after the test.")
+ private boolean mRemoveAfterTest = true;
+
+ @Option(name = "attempts",
+ description = "Number of attempts")
+ private int mAttempts = 3;
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void setUp(ITestDevice device, IBuildInfo buildInfo)
+ throws TargetSetupError, DeviceNotAvailableException {
+ if (waitForAppPkgInfo(device, SYSTEM_UTIL_PKG_NAME, SYSTEM_UTIL_PKG_WAIT_TIME_MS) == null) {
+ throw new TargetSetupError(String.format("%s not installed successfully.",
+ SYSTEM_UTIL_PKG_NAME));
+ }
+
+ if (mRemoveBeforeTest) {
+ doRemoval(device);
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void tearDown(ITestDevice device, IBuildInfo buildInfo, Throwable e)
+ throws DeviceNotAvailableException {
+ if (waitForAppPkgInfo(device, SYSTEM_UTIL_PKG_NAME, SYSTEM_UTIL_PKG_WAIT_TIME_MS) == null) {
+ throw new RuntimeException(String.format("%s not installed successfully.",
+ SYSTEM_UTIL_PKG_NAME));
+ }
+
+ if (mRemoveAfterTest) {
+ try {
+ doRemoval(device);
+ } catch (TargetSetupError ex) {
+ throw new RuntimeException(ex);
+ }
+ }
+ }
+
+ /**
+ * Help function to remove requested Google accounts.
+ *
+ * @param device test device
+ */
+ private void doRemoval(ITestDevice device)
+ throws TargetSetupError, DeviceNotAvailableException {
+ if (mGoogleAccounts.isEmpty()) {
+ removeAllAccounts(device);
+ } else {
+ for (String account : mGoogleAccounts) {
+ removeAccount(device, account);
+ }
+ }
+ }
+
+ /**
+ * Remove all Google accounts.
+ *
+ * @param device test device
+ */
+ private void removeAllAccounts(ITestDevice device)
+ throws TargetSetupError, DeviceNotAvailableException {
+ String removeCmd = String.format("am instrument -w %s/%s",
+ SYSTEM_UTIL_PKG_NAME,
+ REMOVE_GOOGLE_ACCOUNT_CLASS);
+
+ if (executeRemoveAccountCommand(device, removeCmd)) {
+ CLog.i(String.format("All Google accounts were removed from device %s.",
+ device.getSerialNumber()));
+ } else {
+ throw new TargetSetupError(
+ String.format("Failed to remove all Google accounts from device %s.",
+ device.getSerialNumber()));
+ }
+ }
+
+ /**
+ * Remove a Google account.
+ *
+ * @param device test device
+ * @param account account to remove
+ */
+ private void removeAccount(ITestDevice device, String account)
+ throws TargetSetupError, DeviceNotAvailableException {
+ String removeCmd = String.format("am instrument -w -e account \"%s\" %s/%s",
+ account, SYSTEM_UTIL_PKG_NAME, REMOVE_GOOGLE_ACCOUNT_CLASS);
+ if (executeRemoveAccountCommand(device, removeCmd)) {
+ CLog.i(String.format("Google account %s was removed from device %s.",
+ account, device.getSerialNumber()));
+ } else {
+ throw new TargetSetupError(
+ String.format("Failed to remove Google account %s from device %s.",
+ account, device.getSerialNumber()));
+ }
+ }
+
+ /**
+ * Executes shell command to remove Google account.
+ *
+ * @param device test device
+ * @param cmd command to execute
+ * @return {@code true} if success, {@code false} otherwise
+ */
+ private boolean executeRemoveAccountCommand(ITestDevice device, String cmd)
+ throws DeviceNotAvailableException {
+ CLog.v(cmd);
+ for (int i = 0; i < mAttempts; ++i) {
+ CLog.i(String.format("Removing account on device %s, #%d",
+ device.getSerialNumber(), i + 1));
+
+ String result = device.executeShellCommand(cmd);
+ // expected result format is (on success):
+ //
+ // INSTRUMENTATION_RESULT: result=SUCCESS
+ // INSTRUMENTATION_CODE: -1
+ //
+ if (result != null && result.contains("result=SUCCESS")) {
+ return true;
+ } else {
+ CLog.e(String.format("Failed to remove account from device %s: %s.",
+ device.getSerialNumber(), result));
+ // Sleep for 15 seconds before next attempt
+ RunUtil.getDefault().sleep(TimeUnit.SECONDS.toMillis(15));
+ }
+ }
+
+ // OK, failed after all attempts
+ return false;
+ }
+}
+
diff --git a/tools/tradefed-host/src/com/android/afwtest/tradefed/targetprep/AfwTestManagedProfileCreator.java b/tools/tradefed-host/src/com/android/afwtest/tradefed/targetprep/AfwTestManagedProfileCreator.java
new file mode 100644
index 0000000..1bf5a92
--- /dev/null
+++ b/tools/tradefed-host/src/com/android/afwtest/tradefed/targetprep/AfwTestManagedProfileCreator.java
@@ -0,0 +1,89 @@
+package com.android.afwtest.tradefed.targetprep;
+
+import com.android.tradefed.build.IBuildInfo;
+import com.android.tradefed.config.Option;
+import com.android.tradefed.config.OptionClass;
+import com.android.tradefed.device.DeviceNotAvailableException;
+import com.android.tradefed.device.ITestDevice;
+import com.android.tradefed.log.LogUtil.CLog;
+import com.android.tradefed.targetprep.ITargetCleaner;
+import com.android.tradefed.targetprep.ITargetPreparer;
+import com.android.tradefed.targetprep.TargetSetupError;
+
+/**
+ * A {@link ITargetPreparer} that creates a managed profile in the testing device.
+ */
+@OptionClass(alias = "afw-test-create-managed-profile")
+public class AfwTestManagedProfileCreator extends AfwTestTargetPreparer
+ implements ITargetCleaner {
+
+ @Option(name = "remove-after-test",
+ description = "Remove work profile after test.")
+ private boolean mRemoveAfterTest = true;
+
+ @Option(name = "profile-owner-component",
+ description = "Profile owner component to set; optional")
+ private String mProfileOwnerComponent = null;
+
+ @Option(name = "profile-owner-apk",
+ description = "Profile owner apk path; optional")
+ private String mProfileOwnerApk = null;
+
+ /** UserID for user in managed profile.*/
+ private int mManagedProfileUserId;
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void setUp(ITestDevice device, IBuildInfo buildInfo)
+ throws TargetSetupError, DeviceNotAvailableException {
+
+ String pmCommand = "pm create-user --profileOf 0 --managed "
+ + "TestProfile_" + System.currentTimeMillis();
+ String pmCommandOutput = device.executeShellCommand(pmCommand);
+
+ // Extract the id of the new user.
+ String[] pmCmdTokens = pmCommandOutput.split("\\s+");
+ if (!pmCmdTokens[0].contains("Success:")) {
+ throw new TargetSetupError("Managed profile creation failed.");
+ }
+ mManagedProfileUserId = Integer.parseInt(pmCmdTokens[pmCmdTokens.length-1]);
+
+ // Start managed profile user.
+ device.startUser(mManagedProfileUserId);
+
+ CLog.i(String.format("New user created: %d", mManagedProfileUserId));
+
+ if (mProfileOwnerComponent != null && mProfileOwnerApk != null) {
+ device.installPackageForUser(
+ getApk(buildInfo, mProfileOwnerApk), true, mManagedProfileUserId);
+ String command = String.format("dpm set-profile-owner --user %d %s",
+ mManagedProfileUserId, mProfileOwnerComponent);
+ String commandOutput = device.executeShellCommand(command);
+ String[] cmdTokens = commandOutput.split("\\s+");
+ if (!cmdTokens[0].contains("Success:")) {
+ throw new RuntimeException(String.format("Fail to setup %s as profile owner.",
+ mProfileOwnerComponent));
+ }
+
+ CLog.i(String.format("%s was set as profile owner of user %d",
+ mProfileOwnerComponent, mManagedProfileUserId));
+ }
+
+ // Reboot device to create the apps in managed profile.
+ device.reboot();
+ device.waitForDeviceAvailable();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void tearDown(ITestDevice testDevice, IBuildInfo buildInfo, Throwable throwable)
+ throws DeviceNotAvailableException {
+ if (mRemoveAfterTest) {
+ testDevice.removeUser(mManagedProfileUserId);
+ }
+ }
+}
diff --git a/tools/tradefed-host/src/com/android/afwtest/tradefed/targetprep/AfwTestPrivAppInstaller.java b/tools/tradefed-host/src/com/android/afwtest/tradefed/targetprep/AfwTestPrivAppInstaller.java
new file mode 100644
index 0000000..303b31d
--- /dev/null
+++ b/tools/tradefed-host/src/com/android/afwtest/tradefed/targetprep/AfwTestPrivAppInstaller.java
@@ -0,0 +1,118 @@
+/*
+ * 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.afwtest.tradefed.targetprep;
+
+import com.android.tradefed.build.IBuildInfo;
+import com.android.tradefed.config.Option;
+import com.android.tradefed.device.DeviceNotAvailableException;
+import com.android.tradefed.device.ITestDevice;
+import com.android.tradefed.log.LogUtil.CLog;
+import com.android.tradefed.targetprep.ITargetCleaner;
+import com.android.tradefed.targetprep.TargetSetupError;
+
+import java.util.ArrayList;
+import java.util.Collection;
+
+/**
+ * Target preparer to install an app as privileged app by pushing it to /system/priv-app.
+ *
+ * <p>Requires a device where 'adb root' is possible, typically a userdebug build.</p>
+ */
+public class AfwTestPrivAppInstaller extends AfwTestTargetPreparer implements ITargetCleaner {
+
+ /**
+ * System folder for privileged apps and forlder for permissions.
+ */
+ private static final String PRIV_APP_DIR = "/system/priv-app";
+ private static final String PERMISSIONS_APP_DIR = "/etc/permissions";
+
+ @Option(name = "app-filename",
+ description = "app to install")
+ private Collection<String> mApps = new ArrayList<String>();
+
+ @Option(name = "permission-file",
+ description = "Permissions file required for the app")
+ private Collection<String> mPermissionsFiles = new ArrayList<String>();
+
+ @Option(name = "cleanup",
+ description = "true to delete the apk in teardown")
+ private boolean mCleanup = true;
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void setUp(ITestDevice device, IBuildInfo buildInfo)
+ throws TargetSetupError, DeviceNotAvailableException {
+
+ if (mApps.isEmpty()) {
+ return;
+ }
+
+ // Enable adb root and remount system partition
+ enableAdbRoot(device);
+ device.remountSystemWritable();
+
+ for (String app : mApps) {
+ String privApp = String.format("%s/%s", PRIV_APP_DIR, app);
+ if (device.pushFile(getApk(buildInfo, app), privApp)) {
+ CLog.i(String.format("Privileged app installed: %s", privApp));
+ } else {
+ throw new TargetSetupError(String.format("Failed to install %s", privApp));
+ }
+ }
+
+ // Copy permission files (Required for API 25)
+ for (String file : mPermissionsFiles) {
+ String permissionFile = String.format("%s/%s", PERMISSIONS_APP_DIR, file);
+ if (device.pushFile(getApk(buildInfo, file), permissionFile)) {
+ CLog.i(String.format("Permissions file copied: %s", permissionFile));
+ } else {
+ throw new TargetSetupError(String.format("Failed to copy %s", permissionFile));
+ }
+ }
+
+ // Reboot to activate the installed apps
+ device.reboot();
+ device.waitForDeviceAvailable();
+ // Enable adb root again
+ enableAdbRoot(device);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void tearDown(ITestDevice device, IBuildInfo buildInfo, Throwable e)
+ throws DeviceNotAvailableException {
+ if (!mCleanup || mApps.isEmpty()) {
+ return;
+ }
+
+ if (!device.enableAdbRoot()) {
+ throw new RuntimeException(
+ String.format("Failed to enable adb root: %s", device.getSerialNumber()));
+ }
+
+ device.remountSystemWritable();
+
+ for (String app : mApps) {
+ device.executeShellCommand(String.format("rm %s/%s", PRIV_APP_DIR, app));
+ }
+ }
+}
+
diff --git a/tools/tradefed-host/src/com/android/afwtest/tradefed/targetprep/AfwTestPullExternalFile.java b/tools/tradefed-host/src/com/android/afwtest/tradefed/targetprep/AfwTestPullExternalFile.java
new file mode 100644
index 0000000..7a068b2
--- /dev/null
+++ b/tools/tradefed-host/src/com/android/afwtest/tradefed/targetprep/AfwTestPullExternalFile.java
@@ -0,0 +1,78 @@
+/*
+ * 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.afwtest.tradefed.targetprep;
+
+import com.android.tradefed.build.IBuildInfo;
+import com.android.tradefed.config.Option;
+import com.android.tradefed.config.OptionClass;
+import com.android.tradefed.device.DeviceNotAvailableException;
+import com.android.tradefed.device.ITestDevice;
+import com.android.tradefed.log.LogUtil.CLog;
+import com.android.tradefed.targetprep.ITargetCleaner;
+import com.android.tradefed.targetprep.TargetSetupError;
+import com.android.tradefed.util.FileUtil;
+
+import java.io.File;
+
+/**
+ * A target cleaner to copy file from the external storage of the device to host after the
+ * test execution.
+ */
+@OptionClass(alias = "afw-test-pull-external-file")
+public class AfwTestPullExternalFile extends AfwTestTargetPreparer implements ITargetCleaner {
+
+ @Option(name = "remote-file",
+ description = "File to copy from external storage.",
+ mandatory = true)
+ private String mRemoteFile = null;
+
+ @Option(name = "local-file",
+ description = "File path relative to android-cts/repository.",
+ mandatory = true)
+ private String mLocalFile = null;
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void setUp(ITestDevice device, IBuildInfo buildInfo) throws TargetSetupError,
+ DeviceNotAvailableException {
+ // Do nothing.
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void tearDown(ITestDevice device, IBuildInfo buildInfo, Throwable e)
+ throws DeviceNotAvailableException {
+
+ try {
+ File localFile = new File(getRepositoryDir(buildInfo), mLocalFile);
+ if (localFile.getParentFile().mkdirs()) {
+ localFile.createNewFile();
+ }
+ FileUtil.copyFile(device.pullFileFromExternal(mRemoteFile), localFile);
+ } catch (DeviceNotAvailableException e1) {
+ throw e1;
+ } catch (Exception e2) {
+ // Log error but not failing the target preparer.
+ CLog.e(String.format("Failed to copy remote file.", e2));
+ }
+ }
+}
+
diff --git a/tools/tradefed-host/src/com/android/afwtest/tradefed/targetprep/AfwTestSyncTime.java b/tools/tradefed-host/src/com/android/afwtest/tradefed/targetprep/AfwTestSyncTime.java
new file mode 100644
index 0000000..8f4fbcd
--- /dev/null
+++ b/tools/tradefed-host/src/com/android/afwtest/tradefed/targetprep/AfwTestSyncTime.java
@@ -0,0 +1,45 @@
+/*
+ * 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.afwtest.tradefed.targetprep;
+
+import com.android.afwtest.tradefed.utils.TimeUtil;
+import com.android.tradefed.build.IBuildInfo;
+import com.android.tradefed.config.OptionClass;
+import com.android.tradefed.device.DeviceNotAvailableException;
+import com.android.tradefed.device.ITestDevice;
+import com.android.tradefed.targetprep.ITargetPreparer;
+import com.android.tradefed.targetprep.TargetSetupError;
+
+/**
+ * A {@link ITargetPreparer} that syncs device time with the running host.
+ *
+ * <p>Requires a device where 'adb root' is possible, typically a userdebug build</p>
+ */
+@OptionClass(alias = "afw-test-sync-time")
+public class AfwTestSyncTime extends AfwTestTargetPreparer implements ITargetPreparer {
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void setUp(ITestDevice device, IBuildInfo buildInfo) throws TargetSetupError,
+ DeviceNotAvailableException {
+ enableAdbRoot(device);
+ TimeUtil.syncHostTime(device);
+ }
+}
+
diff --git a/tools/tradefed-host/src/com/android/afwtest/tradefed/targetprep/AfwTestTargetPreparer.java b/tools/tradefed-host/src/com/android/afwtest/tradefed/targetprep/AfwTestTargetPreparer.java
new file mode 100644
index 0000000..684fe1f
--- /dev/null
+++ b/tools/tradefed-host/src/com/android/afwtest/tradefed/targetprep/AfwTestTargetPreparer.java
@@ -0,0 +1,204 @@
+/*
+ * 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.afwtest.tradefed.targetprep;
+
+import com.android.afwtest.tradefed.TestConfig;
+import com.android.compatibility.common.tradefed.build.CompatibilityBuildHelper;
+import com.android.tradefed.build.IBuildInfo;
+import com.android.tradefed.device.DeviceNotAvailableException;
+import com.android.tradefed.device.ITestDevice;
+import com.android.tradefed.device.PackageInfo;
+import com.android.tradefed.log.LogUtil.CLog;
+import com.android.tradefed.targetprep.TargetSetupError;
+import com.android.tradefed.util.CommandResult;
+import com.android.tradefed.util.CommandStatus;
+import com.android.tradefed.util.RunUtil;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.util.concurrent.TimeUnit;
+
+
+/**
+ * Abstract class to keep common functionality.
+ */
+public abstract class AfwTestTargetPreparer {
+
+ private CompatibilityBuildHelper mBuildHelper;
+ private Long mTimeoutSecs = TimeUnit.MINUTES.toSeconds(5);
+
+ /**
+ * Gets an instance of {@link CompatibilityBuildHelper}.
+ *
+ * @param buildInfo reference to {@link IBuildInfo}
+ * @return reference to {@link CompatibilityBuildHelper} instance
+ */
+ protected CompatibilityBuildHelper getCtsBuildHelper(IBuildInfo buildInfo) {
+ if (mBuildHelper == null) {
+ mBuildHelper = new CompatibilityBuildHelper(buildInfo);
+ }
+ return mBuildHelper;
+ }
+
+ /**
+ * Gets repository directory.
+ *
+ * @param buildInfo reference to {@link IBuildInfo}
+ * @return File path to repository folder.
+ */
+ protected File getRepositoryDir(IBuildInfo buildInfo) throws FileNotFoundException {
+ return getCtsBuildHelper(buildInfo).getDir();
+ }
+
+ /**
+ * Gets apk file from testcases dir.
+ *
+ * @param buildInfo reference to {@link IBuildInfo}
+ * @param fileName apk file name
+ * @return {@link File} of the apk
+ */
+ protected File getApk(IBuildInfo buildInfo, String fileName) throws TargetSetupError {
+ try {
+ return new File(getCtsBuildHelper(buildInfo).getTestsDir(), fileName);
+ } catch (FileNotFoundException e) {
+ throw new TargetSetupError(String.format("Couldn't get apk: %s", fileName), e);
+ }
+ }
+
+ /**
+ * Enable adb root.
+ *
+ * @param device test device
+ * @throws TargetSetupError if failed to enable adb root
+ * @throws DeviceNotAvailableException if test device becomes unavailable
+ */
+ protected void enableAdbRoot(ITestDevice device)
+ throws TargetSetupError, DeviceNotAvailableException {
+ // Enable adb root
+ if (!device.enableAdbRoot()) {
+ throw new TargetSetupError(
+ String.format("Failed to enable adb root: %s", device.getSerialNumber()));
+ }
+ }
+
+ /**
+ * Waits and gets certain app package info.
+ *
+ * @param device test device
+ * @param pkgName app package name
+ * @param timeoutMs timeout in ms
+ * @return App package info of given package if found, null otherwise
+ * @throws DeviceNotAvailableException if test device becomes unavailable
+ */
+ protected PackageInfo waitForAppPkgInfo(ITestDevice device, String pkgName, long timeoutMs)
+ throws DeviceNotAvailableException {
+
+ long threadSleepTimeMs = TimeUnit.SECONDS.toMillis(5);
+
+ PackageInfo pkgInfo = device.getAppPackageInfo(pkgName);
+
+ while (pkgInfo == null && timeoutMs > 0) {
+ RunUtil.getDefault().sleep(threadSleepTimeMs);
+ timeoutMs -= threadSleepTimeMs;
+
+ pkgInfo = device.getAppPackageInfo(pkgName);
+ }
+
+ return pkgInfo;
+ }
+
+ /**
+ * Gets {@link TestConfig} instance.
+ *
+ * @return {@link TestConfig} instance or null if not found;
+ */
+ protected TestConfig getTestConfig() {
+ return TestConfig.getInstance();
+ }
+
+ /**
+ * Gets timeout size from config file.
+ *
+ * @return timeout size in integer
+ */
+ protected int getTimeoutSize() {
+ TestConfig testConfig = TestConfig.getInstance();
+
+ // Target prepares should still work without test config file;
+ if (testConfig == null) {
+ return 1;
+ }
+
+ return TestConfig.getInstance().getTimeoutSize();
+ }
+
+ /**
+ * Wipes device via fastboot.
+ *
+ * <p>
+ * Refers to com.android.tradefed.targetprep.DeviceWiper
+ * </p>
+ *
+ * @param device test device
+ */
+ protected void wipeDevice(ITestDevice device)
+ throws DeviceNotAvailableException, TargetSetupError {
+
+ CLog.i(String.format("Wiping device %s".format(device.getSerialNumber())));
+
+ device.rebootIntoBootloader();
+ fastbootFormat(device, "cache");
+ fastbootFormat(device, "userdata");
+ device.executeFastbootCommand("reboot");
+ }
+
+
+ /**
+ * Performs fastboot erase/format operation on certain partition
+ *
+ * @param device test device
+ * @param partition android partition, e.g. userdata, cache, system
+ */
+ protected void fastbootFormat(ITestDevice device, String partition)
+ throws DeviceNotAvailableException, TargetSetupError {
+
+ CLog.i(String.format("Attempting: fastboot format %s", partition));
+ CommandResult r = device.executeLongFastbootCommand("format", partition);
+ if (r.getStatus() != CommandStatus.SUCCESS) {
+ throw new TargetSetupError(
+ String.format("format %s failed: %s", partition, r.getStderr()));
+ }
+ }
+
+ /**
+ * "Soft" reboot the device by adb shell stop/start.
+ *
+ * @param device test device
+ */
+ protected void softReboot(ITestDevice device)
+ throws DeviceNotAvailableException, TargetSetupError {
+
+ CLog.i(String.format("Soft reboot device %s".format(device.getSerialNumber())));
+
+ device.executeShellCommand("stop");
+ device.executeShellCommand("start");
+ device.waitForDeviceAvailable();
+ // enableAdbRoot() will check if device.isEnableRoot()
+ device.enableAdbRoot();
+ }
+}
+
diff --git a/tools/tradefed-host/src/com/android/afwtest/tradefed/targetprep/AfwTestUserRemover.java b/tools/tradefed-host/src/com/android/afwtest/tradefed/targetprep/AfwTestUserRemover.java
new file mode 100644
index 0000000..c21c8e6
--- /dev/null
+++ b/tools/tradefed-host/src/com/android/afwtest/tradefed/targetprep/AfwTestUserRemover.java
@@ -0,0 +1,114 @@
+/*
+ * 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.afwtest.tradefed.targetprep;
+
+import com.android.tradefed.build.IBuildInfo;
+import com.android.tradefed.config.Option;
+import com.android.tradefed.config.OptionClass;
+import com.android.tradefed.device.DeviceNotAvailableException;
+import com.android.tradefed.device.ITestDevice;
+import com.android.tradefed.log.LogUtil.CLog;
+import com.android.tradefed.targetprep.ITargetCleaner;
+import com.android.tradefed.targetprep.TargetSetupError;
+import com.android.tradefed.util.RunUtil;
+
+import java.util.ArrayList;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * A target preparer that can remove all non-primary users (user id not 0).
+ */
+@OptionClass(alias = "afw-test-user-remover")
+public class AfwTestUserRemover implements ITargetCleaner {
+
+ @Option(name = "remove-users-before-test",
+ description = "Remove all non-primary users before the test.")
+ private boolean mRemoveUsersBeforeTest = false;
+
+ @Option(name = "remove-users-after-test",
+ description = "Remove all non-primary users after the test.")
+ private boolean mRemoveUsersAfterTest = true;
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void setUp(ITestDevice device, IBuildInfo buildInfo) throws TargetSetupError,
+ DeviceNotAvailableException {
+ if (mRemoveUsersBeforeTest && !removeUsers(device)) {
+ throw new TargetSetupError(
+ "Failed to remove all non-primary users on device " + device.getSerialNumber());
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void tearDown(ITestDevice device, IBuildInfo buildInfo, Throwable e)
+ throws DeviceNotAvailableException {
+ if (mRemoveUsersAfterTest && !removeUsers(device)) {
+ throw new RuntimeException(
+ "Failed to remove all non-primary users on device " + device.getSerialNumber());
+ }
+ }
+
+ /**
+ * Remove all non-primary users.
+ *
+ * @param device the testing device
+ * @return {@code true} if all non-primary users are removed, {@code false} otherwise
+ */
+ private boolean removeUsers(ITestDevice device) throws DeviceNotAvailableException {
+ // Ensure package manager is running
+ device.waitForDeviceAvailable();
+
+ ArrayList<Integer> users = device.listUsers();
+
+ if (users == null) {
+ return true;
+ }
+
+ boolean success = true;
+
+ for (Integer userId : users) {
+ if (userId == 0) {
+ // Can't remove user 0, so don't try.
+ continue;
+ }
+
+ if (device.removeUser(userId)) {
+ CLog.i(String.format("Successfully removed user %d on %s",
+ userId,
+ device.getSerialNumber()));
+ } else {
+ CLog.e(String.format("Failed to remove user %d on %s",
+ userId,
+ device.getSerialNumber()));
+ success = false;
+ }
+ }
+
+ RunUtil.getDefault().sleep(TimeUnit.SECONDS.toMillis(60));
+
+ // Make sure device is still available
+ device.waitForDeviceAvailable();
+
+ return success;
+ }
+}
+
diff --git a/tools/tradefed-host/src/com/android/afwtest/tradefed/targetprep/AfwTestWifiPreparer.java b/tools/tradefed-host/src/com/android/afwtest/tradefed/targetprep/AfwTestWifiPreparer.java
new file mode 100644
index 0000000..c071bd1
--- /dev/null
+++ b/tools/tradefed-host/src/com/android/afwtest/tradefed/targetprep/AfwTestWifiPreparer.java
@@ -0,0 +1,305 @@
+/*
+ * 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.afwtest.tradefed.targetprep;
+
+import com.android.tradefed.build.IBuildInfo;
+import com.android.tradefed.config.Option;
+import com.android.tradefed.config.OptionClass;
+import com.android.tradefed.device.DeviceNotAvailableException;
+import com.android.tradefed.device.ITestDevice;
+import com.android.tradefed.log.LogUtil.CLog;
+import com.android.tradefed.targetprep.ITargetCleaner;
+import com.android.tradefed.targetprep.TargetSetupError;
+import com.android.tradefed.util.RunUtil;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Properties;
+
+/**
+ * A {@link ITargetPreparer} that connects to wifi network with configurations read from file.
+ */
+@OptionClass(alias = "afw-test-wifi")
+public class AfwTestWifiPreparer extends AfwTestTargetPreparer implements ITargetCleaner {
+
+ /**
+ * Package name of the system util app.
+ */
+ private static final String UTIL_PKG_NAME = "com.android.afwtest.util";
+
+ /** Shell command template for connect/disconnect wifi. */
+ private static final String ADB_SHELL_CMD_TEMPL =
+ "am instrument -w %s com.android.afwtest.util/.Wifi";
+
+ @Option(name = "wifi-config-file",
+ description = "The file name containing wifi configuration.")
+ private String mConfigFileName = null;
+
+ @Option(name = "wifi-ssid-key",
+ description = "The key of wifi ssid in the config file.")
+ private String mWifiSsidKey = null;
+
+ @Option(name = "wifi-security-type-key",
+ description = "The key of wifi security type in the config file.")
+ private String mWifiSecurityType = null;
+
+ @Option(name = "wifi-password-key",
+ description = "The key of wifi password in the config file.")
+ private String mWifiPasswordKey = null;
+
+ @Option(name = "wifi-attempts",
+ description = "Number of attempts to connect to wifi network.")
+ private int mWifiAttempts = 5;
+
+ @Option(name = "disconnect-wifi-after-test",
+ description = "Disconnect from wifi network after test completes.")
+ private boolean mDisconnectWifiAfterTest = true;
+
+ /** Whether to connect with AfwThUtil app. */
+ private boolean mWifiConnectedWithUtilApp = false;
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void setUp(ITestDevice device, IBuildInfo buildInfo) throws TargetSetupError,
+ DeviceNotAvailableException {
+
+ WifiConfig wifiConfig = getWifiConfig(buildInfo);
+
+ // tradefed doesn't support WEP wifi; connect to it by AfwThUtil
+ if ("WEP".equals(wifiConfig.getSecurityType())) {
+ mWifiConnectedWithUtilApp = true;
+ connectWifiWithUtilApp(device,
+ wifiConfig.getSSID(),
+ wifiConfig.getSecurityType(),
+ wifiConfig.getSecurityKey());
+ return;
+ }
+
+ // Implement retry.
+ for (int i = 0; i < mWifiAttempts; ++i) {
+ try {
+ if (device.connectToWifiNetworkIfNeeded(
+ wifiConfig.getSSID(), wifiConfig.getSecurityKey())) {
+ return;
+ }
+ } catch (Exception e) {
+ CLog.e(e);
+ }
+ boolean lastAttempt = (i + 1) == mWifiAttempts;
+ if (!lastAttempt) {
+ RunUtil.getDefault().sleep(device.getOptions().getWifiRetryWaitTime());
+ }
+ }
+
+ throw new TargetSetupError(String.format("Failed to connect to wifi network %s on %s",
+ wifiConfig.getSSID(), device.getSerialNumber()));
+ }
+
+ /**
+ * Loads configuration of Wi-Fi to be used.
+ *
+ * @param buildInfo data about the build under test.
+ * @return loaded Wi-Fi configuration.
+ */
+ protected WifiConfig getWifiConfig(IBuildInfo buildInfo) throws TargetSetupError {
+ if (mConfigFileName == null) {
+ throw new TargetSetupError("wifi-config-file not specified");
+ }
+
+ if (mWifiSsidKey == null) {
+ throw new TargetSetupError("wifi-ssid-key not specified");
+ }
+
+ File configFile;
+ try {
+ configFile = new File(getCtsBuildHelper(buildInfo).getTestsDir(), mConfigFileName);
+ } catch (FileNotFoundException e) {
+ throw new TargetSetupError("Couldn't find tests directory");
+ }
+ Properties props;
+ try {
+ props = readProperties(configFile);
+ } catch (IOException e) {
+ throw new TargetSetupError(
+ String.format("Failed to read prop file: %s", configFile.getAbsolutePath()));
+ }
+
+ String wifiSsid = props.getProperty(mWifiSsidKey, "");
+ String wifiSecurityType = props.getProperty(mWifiSecurityType, "");
+ String wifiPassword = props.getProperty(mWifiPasswordKey, "");
+
+ if (wifiSsid.isEmpty()) {
+ throw new TargetSetupError(String.format(
+ "%s not specified in file %s", mWifiSsidKey, configFile.getAbsolutePath()));
+ }
+
+ return new WifiConfig(wifiSsid, wifiSecurityType, wifiPassword);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void tearDown(ITestDevice device, IBuildInfo buildInfo, Throwable e)
+ throws DeviceNotAvailableException {
+
+ if (mDisconnectWifiAfterTest && device.isWifiEnabled()) {
+ if (mWifiConnectedWithUtilApp) {
+ disconnectWifiWithUtilApp(device);
+ } else {
+ if (device.disconnectFromWifi()) {
+ CLog.i("Successfully disconnected from wifi network on %s",
+ device.getSerialNumber());
+ } else {
+ CLog.w("Failed to disconnect from wifi network on %s",
+ device.getSerialNumber());
+ }
+ }
+ }
+ }
+
+ /**
+ * Connects to wifi with AfwThUtil app.
+ *
+ * @param device test device
+ * @param wifiSSID WIFI SSID
+ * @param securityType WIFI security type
+ * @param password WIFI password
+ */
+ private void connectWifiWithUtilApp(ITestDevice device,
+ String wifiSSID,
+ String securityType,
+ String password)
+ throws TargetSetupError, DeviceNotAvailableException {
+ String args = String.format("-e action connect -e ssid %s", wifiSSID);
+ if (securityType != null && !securityType.isEmpty()) {
+ args = String.format("%s -e security_type %s", args, securityType);
+ }
+ if (password != null && !password.isEmpty()) {
+ args = String.format("%s -e password %s", args, password);
+ }
+
+ String cmdStr = String.format(ADB_SHELL_CMD_TEMPL, args);
+ CLog.i(String.format("Shell: %s", cmdStr));
+ String result = device.executeShellCommand(cmdStr);
+ if (result != null && result.contains("result=SUCCESS")) {
+ CLog.i(String.format(
+ "Successfully connected to wifi network %s(security_type=%s) on %s",
+ wifiSSID,
+ securityType,
+ device.getSerialNumber()));
+ } else {
+ throw new TargetSetupError(String.format(
+ "Failed to connected to wifi network %s(security_type=%s) on %s: %s",
+ wifiSSID,
+ securityType,
+ device.getSerialNumber(),
+ result));
+ }
+ }
+
+ /**
+ * Disconnects from Wifi with AfwThUtil app.
+ *
+ * @param device test device
+ */
+ private void disconnectWifiWithUtilApp(ITestDevice device) throws DeviceNotAvailableException {
+ String cmdStr = String.format(ADB_SHELL_CMD_TEMPL, "-e action disconnect");
+ CLog.i(String.format("Shell: %s", cmdStr));
+ String result = device.executeShellCommand(cmdStr);
+ if (result != null && result.contains("result=SUCCESS")) {
+ CLog.i(String.format("Successfully disconnected from wifi network on %s",
+ device.getSerialNumber()));
+ } else {
+ CLog.w(String.format("Failed to disconnect from wifi network on %s: %s",
+ device.getSerialNumber(),
+ result));
+ }
+ }
+
+ /**
+ * Help function to read {@link Properties} from a file.
+ *
+ * @param file {@link File} to read properties from
+ * @return {@link Properties} read from given file
+ *
+ * @throws IOException if read failed
+ */
+ private static Properties readProperties(File file) throws IOException {
+ InputStream input = null;
+
+ try {
+ input = new FileInputStream(file);
+ Properties props = new Properties();
+ props.load(input);
+ return props;
+ } finally {
+ if (input != null) {
+ input.close();
+ }
+ }
+ }
+
+ /**
+ * Helper class to hold Wi-Fi configuration.
+ */
+ protected final static class WifiConfig {
+ private final String mSSID;
+ private final String mSecurityType;
+ private final String mSecurityKey;
+
+ /**
+ * Constructs new instance of Wi-Fi configuration holder.
+ *
+ * @param ssid SSID of Wi-Fi access point.
+ * @param securityType security type used by Wi-Fi access point.
+ * @param securityKey security key used by Wi-Fi access point.
+ */
+ protected WifiConfig(String ssid, String securityType, String securityKey) {
+ this.mSSID = ssid;
+ this.mSecurityType = securityType;
+ this.mSecurityKey = securityKey;
+ }
+
+ /**
+ * Returns SSID of Wi-Fi access point.
+ */
+ protected String getSSID() {
+ return mSSID;
+ }
+
+ /**
+ * Returns security type used by Wi-Fi access point.
+ */
+ protected String getSecurityType() {
+ return mSecurityType;
+ }
+
+ /**
+ * Returns security key used by Wi-Fi access point.
+ */
+ protected String getSecurityKey() {
+ return mSecurityKey;
+ }
+ }
+}
+
diff --git a/tools/tradefed-host/src/com/android/afwtest/tradefed/targetprep/metrics/AfwTestMetricsCollectorTargetPreparer.java b/tools/tradefed-host/src/com/android/afwtest/tradefed/targetprep/metrics/AfwTestMetricsCollectorTargetPreparer.java
new file mode 100644
index 0000000..10f3adb
--- /dev/null
+++ b/tools/tradefed-host/src/com/android/afwtest/tradefed/targetprep/metrics/AfwTestMetricsCollectorTargetPreparer.java
@@ -0,0 +1,216 @@
+/*
+ * 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.afwtest.tradefed.targetprep.metrics;
+
+import static com.android.afwtest.tradefed.targetprep.metrics.nano.Metrics.BuildFingerprint;
+import static com.android.afwtest.tradefed.targetprep.metrics.nano.Metrics.DeviceInfo;
+import static com.android.afwtest.tradefed.targetprep.metrics.nano.Metrics.InvocationMetrics;
+import static com.android.afwtest.tradefed.targetprep.metrics.nano.Metrics.InvocationMetrics.InvocationMetric;
+
+import com.android.afwtest.tradefed.utils.ReflectionUtils;
+import com.android.compatibility.common.tradefed.build.CompatibilityBuildHelper;
+import com.android.tradefed.build.IBuildInfo;
+import com.android.tradefed.config.Option;
+import com.android.tradefed.device.DeviceNotAvailableException;
+import com.android.tradefed.device.ITestDevice;
+import com.android.tradefed.log.LogUtil.CLog;
+import com.android.tradefed.targetprep.BuildError;
+import com.android.tradefed.targetprep.ITargetCleaner;
+import com.android.tradefed.targetprep.TargetSetupError;
+import com.google.protobuf.nano.MessageNano;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+import java.util.concurrent.locks.ReadWriteLock;
+import java.util.concurrent.locks.ReentrantReadWriteLock;
+import java.util.function.Consumer;
+import javax.annotation.concurrent.GuardedBy;
+
+/** {@link ITargetCleaner} to collect metrics during test invocation. */
+public class AfwTestMetricsCollectorTargetPreparer implements ITargetCleaner {
+
+ @Option(
+ name = "collector-class-names",
+ description = "Class names of metrics collectors to use."
+ )
+ private final List<String> mCollectorClassNames = new ArrayList<>();
+
+ // List of metrics collectors.
+ private final List<MetricsCollector> mCollectors = new ArrayList<>();
+
+ // Common list of collected metrics.
+ @GuardedBy("mMetricsLock")
+ private final List<InvocationMetric> mMetrics = new ArrayList<>();
+
+ // Lock to synchronize access to common list of collected metrics
+ // from multiple metrics collectors.
+ // Lock is used instead of synchronized collections to ensure fairness of the access.
+ private final ReadWriteLock mMetricsLock = new ReentrantReadWriteLock(true);
+
+ /** {@inheritDoc} */
+ @Override
+ public void setUp(ITestDevice device, IBuildInfo buildInfo)
+ throws TargetSetupError, BuildError, DeviceNotAvailableException {
+ if (!isCollectionEnabled()) {
+ return;
+ }
+
+ try {
+ createCollectors();
+ for (MetricsCollector collector : mCollectors) {
+ collector.init(device, buildInfo, this::addMetricToList);
+ }
+ } catch (MetricsCollectorException e) {
+ throw new TargetSetupError(
+ "Exception was thrown during metrics collectors initialization", e, null);
+ }
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public void tearDown(ITestDevice device, IBuildInfo buildInfo, Throwable e)
+ throws DeviceNotAvailableException {
+ if (!isCollectionEnabled()) {
+ return;
+ }
+
+ for (MetricsCollector collector : mCollectors) {
+ collector.destroy();
+ }
+ try {
+ writeMetrics(device, buildInfo);
+ } catch (MetricsCollectorException ex) {
+ CLog.e(ex);
+ }
+ }
+
+ /**
+ * Checks if metrics collection is enabled for current invocation.
+ *
+ * @return {@code true} if metrics collection is enabled, {@code false} otherwise.
+ */
+ protected boolean isCollectionEnabled() {
+ return true;
+ }
+
+ /**
+ * {@link Consumer} passed to collectors to ensure synchronized access to the list of collected
+ * metrics.
+ *
+ * @param metric metric to add to the list of collected metrics.
+ */
+ private void addMetricToList(InvocationMetric metric) {
+ mMetricsLock.writeLock().lock();
+ mMetrics.add(metric);
+ mMetricsLock.writeLock().unlock();
+ }
+
+ /**
+ * Collects information about the device on which invocation happened.
+ *
+ * @param device current {@link ITestDevice}.
+ * @param buildInfo current {@link IBuildInfo}.
+ * @return information about the device on which invocation happened.
+ */
+ private DeviceInfo getDeviceInfo(ITestDevice device, IBuildInfo buildInfo) {
+ final Map<String, String> buildAttrs = buildInfo.getBuildAttributes();
+ BuildFingerprint buildFingerprint = new BuildFingerprint();
+ buildFingerprint.productBrand = buildAttrs.get("cts:build_brand");
+ buildFingerprint.productName = buildAttrs.get("cts:build_product");
+ buildFingerprint.productDevice = buildAttrs.get("cts:build_device");
+ buildFingerprint.platformVersion = buildAttrs.get("cts:build_version_release");
+ buildFingerprint.release = buildAttrs.get("cts:build_id");
+ buildFingerprint.versionIncremental = buildAttrs.get("cts:build_version_incremental");
+ buildFingerprint.type = buildAttrs.get("cts:build_type");
+ buildFingerprint.tags = buildAttrs.get("cts:build_tags");
+
+ DeviceInfo deviceInfo = new DeviceInfo();
+ deviceInfo.buildFingerprint = buildFingerprint;
+ deviceInfo.apiLevel = Long.parseLong(buildAttrs.get("cts:build_version_sdk"));
+ try {
+ deviceInfo.gmsVersion =
+ device.getAppPackageInfo("com.google.android.gms").getVersionName();
+ } catch (DeviceNotAvailableException e) {
+ CLog.e(e);
+ deviceInfo.gmsVersion = "N/A";
+ }
+
+ return deviceInfo;
+ }
+
+ /**
+ * Writes collected metrics to file.
+ *
+ * @param buildInfo current {@link IBuildInfo}.
+ * @throws MetricsCollectorException if any error has occurred upon writing collected metrics.
+ */
+ private void writeMetrics(ITestDevice device, IBuildInfo buildInfo)
+ throws MetricsCollectorException {
+ CompatibilityBuildHelper buildHelper = new CompatibilityBuildHelper(buildInfo);
+ FileOutputStream os = null;
+ try {
+ InvocationMetrics metrics = new InvocationMetrics();
+ metrics.id = UUID.randomUUID().toString();
+ metrics.timestampMillis = System.currentTimeMillis();
+ metrics.deviceInfo = getDeviceInfo(device, buildInfo);
+
+ mMetricsLock.readLock().lock();
+ metrics.metrics = mMetrics.toArray(InvocationMetric.emptyArray());
+ mMetricsLock.readLock().unlock();
+
+ if (metrics.metrics.length == 0) {
+ return;
+ }
+
+ File logsDir = buildHelper.getLogsDir();
+ // createTempFile generates unique file name.
+ File resultFile = File.createTempFile("provisioning_metrics_", ".pbuf", logsDir);
+ os = new FileOutputStream(resultFile);
+ os.write(MessageNano.toByteArray(metrics));
+ } catch (IOException e) {
+ throw new MetricsCollectorException("Cannot save collected metrics.", e);
+ } finally {
+ if (os != null) {
+ try {
+ os.close();
+ } catch (IOException ignore) {
+ }
+ }
+ }
+ }
+
+ /**
+ * Creates instances of metrics collectors whose class names are specified in
+ * 'collector-class-names' option.
+ *
+ * @throws MetricsCollectorException if an exception occurs during collectors instantiation.
+ */
+ private void createCollectors() throws MetricsCollectorException {
+ for (String className : mCollectorClassNames) {
+ try {
+ mCollectors.add(ReflectionUtils.getInstanceOf(className, MetricsCollector.class));
+ } catch (ReflectiveOperationException e) {
+ throw new MetricsCollectorException(e);
+ }
+ }
+ }
+}
diff --git a/tools/tradefed-host/src/com/android/afwtest/tradefed/targetprep/metrics/LogcatMetricsCollector.java b/tools/tradefed-host/src/com/android/afwtest/tradefed/targetprep/metrics/LogcatMetricsCollector.java
new file mode 100644
index 0000000..79feb60
--- /dev/null
+++ b/tools/tradefed-host/src/com/android/afwtest/tradefed/targetprep/metrics/LogcatMetricsCollector.java
@@ -0,0 +1,171 @@
+/*
+ * 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.afwtest.tradefed.targetprep.metrics;
+
+import static com.android.afwtest.tradefed.targetprep.metrics.nano.Metrics.InvocationMetrics;
+import static com.android.afwtest.tradefed.targetprep.metrics.nano.Metrics.InvocationMetrics.InvocationMetric;
+import static com.android.afwtest.tradefed.targetprep.metrics.nano.Metrics.InvocationMetrics.MetricValue;
+import static com.android.internal.logging.nano.MetricsProto.MetricsEvent;
+
+import com.android.afwtest.tradefed.utils.StreamDiscarder;
+import com.android.tradefed.build.IBuildInfo;
+import com.android.tradefed.device.ITestDevice;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.function.BiConsumer;
+import java.util.function.Consumer;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/** Collector of the metrics from logcat events. */
+public class LogcatMetricsCollector implements MetricsCollector {
+
+ // Logcat process from which events are read.
+ private Process mLogcatProcess;
+
+ // Thread of the event parser.
+ private Thread mParserMain;
+
+ // Thread to read and discard logcat's stderr to prevent pipe buffer overflow.
+ private Thread mLogcatErrDiscarder;
+
+ // Map of logcat events to collect to AfW TH specific metric types.
+ private final Map<Integer, Integer> mEventsToCollect = new HashMap<>();
+
+ // Initializer of set of logcat event to collect as metrics.
+ {
+ mEventsToCollect.put(
+ MetricsEvent.PROVISIONING_TOTAL_TASK_TIME_MS,
+ InvocationMetrics.TOTAL_PROVISIONING_TIME_MS);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public void init(
+ ITestDevice iTestDevice,
+ IBuildInfo iBuildInfo,
+ Consumer<InvocationMetric> metricsConsumer)
+ throws MetricsCollectorException {
+
+ try {
+ startLogcat(iTestDevice.getSerialNumber());
+ mParserMain =
+ new Thread(
+ new LogcatEventParserMain(
+ mLogcatProcess.getInputStream(),
+ (eventId, value) -> {
+ if (mEventsToCollect.containsKey(eventId)) {
+ final InvocationMetric metric = new InvocationMetric();
+ final MetricValue metricValue = new MetricValue();
+
+ metricValue.setStringValue(value);
+
+ metric.metricType = mEventsToCollect.get(eventId);
+ metric.setScalarValue(metricValue);
+ metricsConsumer.accept(metric);
+ }
+ }));
+ mParserMain.start();
+ mLogcatErrDiscarder = new Thread(new StreamDiscarder(mLogcatProcess.getErrorStream()));
+ mLogcatErrDiscarder.start();
+ } catch (Exception e) {
+ destroy();
+ throw new MetricsCollectorException(
+ String.format("Failed to initialize %s.", getClass().getName()), e);
+ }
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public void destroy() {
+ if (mLogcatProcess != null) {
+ mLogcatProcess.destroy();
+ try {
+ mLogcatProcess.waitFor();
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ }
+ }
+ if (mParserMain != null) {
+ try {
+ mParserMain.join();
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ }
+ }
+ if (mLogcatErrDiscarder != null) {
+ try {
+ mLogcatErrDiscarder.join();
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ }
+ }
+ }
+
+ /**
+ * Starts new logcat process to capture events from device.
+ *
+ * @param serial serial number of a device.
+ * @throws MetricsCollectorException if could not create new logcat process.
+ */
+ private void startLogcat(String serial) throws MetricsCollectorException {
+ ProcessBuilder pb = new ProcessBuilder("adb", "-s", serial, "logcat", "-b", "events");
+ try {
+ mLogcatProcess = pb.start();
+ } catch (IOException e) {
+ throw new MetricsCollectorException("Could not start logcat to capture events.", e);
+ }
+ }
+
+ /** {@link Runnable} for a thread to parse logcat output. */
+ private static final class LogcatEventParserMain implements Runnable {
+
+ // Sample event string:
+ // 03-01 14:30:12.634 788 2845 I sysui_action: [325,4706]
+ private static final Pattern EVENT_PATTERN =
+ Pattern.compile("sysui_action: \\[(\\d+),(.+)\\]");
+
+ private final InputStream mStream;
+ private final BiConsumer<Integer, String> mEventConsumer;
+
+ LogcatEventParserMain(InputStream stream, BiConsumer<Integer, String> eventConsumer) {
+ mStream = stream;
+ mEventConsumer = eventConsumer;
+ }
+
+ @Override
+ public void run() {
+ try (BufferedReader reader = new BufferedReader(new InputStreamReader(mStream))) {
+ String line;
+ while ((line = reader.readLine()) != null) {
+ Matcher matcher = EVENT_PATTERN.matcher(line);
+ if (!matcher.find()) {
+ continue;
+ }
+ mEventConsumer.accept(Integer.parseInt(matcher.group(1)), matcher.group(2));
+ }
+ } catch (IOException e) {
+ throw new RuntimeException("Cannot read logcat output.", e);
+ }
+ }
+ }
+}
diff --git a/tools/tradefed-host/src/com/android/afwtest/tradefed/targetprep/metrics/MemoryMetricsCollector.java b/tools/tradefed-host/src/com/android/afwtest/tradefed/targetprep/metrics/MemoryMetricsCollector.java
new file mode 100644
index 0000000..7b87341
--- /dev/null
+++ b/tools/tradefed-host/src/com/android/afwtest/tradefed/targetprep/metrics/MemoryMetricsCollector.java
@@ -0,0 +1,81 @@
+/*
+ * 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.afwtest.tradefed.targetprep.metrics;
+
+import static com.android.afwtest.tradefed.targetprep.metrics.nano.Metrics.InvocationMetrics.InvocationMetric;
+
+import com.android.afwtest.tradefed.targetprep.metrics.memory.MemoryByUserGrouper;
+import com.android.afwtest.tradefed.targetprep.metrics.memory.MemoryCollectorTask;
+import com.android.tradefed.build.IBuildInfo;
+import com.android.tradefed.device.ITestDevice;
+
+import java.util.Arrays;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.ScheduledThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicLong;
+import java.util.function.Consumer;
+import java.util.function.Supplier;
+
+/**
+ * Collector of memory usage metrics.
+ */
+public class MemoryMetricsCollector implements MetricsCollector {
+
+ /**
+ * Executor to run scheduled metric collection task.
+ */
+ private ScheduledExecutorService mExecutor;
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void init(ITestDevice testDevice, IBuildInfo iBuildInfo,
+ Consumer<InvocationMetric> metricsConsumer)
+ throws MetricsCollectorException {
+
+ AtomicLong tickCounter = new AtomicLong(0L);
+
+ mExecutor = new ScheduledThreadPoolExecutor(1);
+ mExecutor.scheduleWithFixedDelay(getCollectorTask(testDevice, metricsConsumer,
+ tickCounter::getAndIncrement), 0, 500, TimeUnit.MILLISECONDS);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void destroy() {
+ mExecutor.shutdownNow();
+ }
+
+ /**
+ * Gets {@link Runnable} to process a single iteration of memory information collection.
+ *
+ * @param testDevice current {@link ITestDevice}.
+ * @param metricsConsumer {@link Consumer} to emit collected metrics.
+ * @param tickCounter tick counter.
+ * @return {@link Runnable} to process a single iteration of memory information collection.
+ */
+ protected Runnable getCollectorTask(ITestDevice testDevice,
+ Consumer<InvocationMetric> metricsConsumer,
+ Supplier<Long> tickCounter) {
+ return new MemoryCollectorTask(testDevice, metricsConsumer, tickCounter,
+ Arrays.asList(new MemoryByUserGrouper()));
+ }
+} \ No newline at end of file
diff --git a/tools/tradefed-host/src/com/android/afwtest/tradefed/targetprep/metrics/MetricsCollector.java b/tools/tradefed-host/src/com/android/afwtest/tradefed/targetprep/metrics/MetricsCollector.java
new file mode 100644
index 0000000..d28c837
--- /dev/null
+++ b/tools/tradefed-host/src/com/android/afwtest/tradefed/targetprep/metrics/MetricsCollector.java
@@ -0,0 +1,45 @@
+/*
+ * 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.afwtest.tradefed.targetprep.metrics;
+
+import static com.android.afwtest.tradefed.targetprep.metrics.nano.Metrics.InvocationMetrics.InvocationMetric;
+
+import com.android.tradefed.build.IBuildInfo;
+import com.android.tradefed.device.ITestDevice;
+
+import java.util.function.Consumer;
+
+/** Collects metrics from a device during test invocation. */
+public interface MetricsCollector {
+
+ /**
+ * Initializes metrics collector.
+ *
+ * @param iTestDevice current {@link ITestDevice}.
+ * @param iBuildInfo current {@link IBuildInfo}.
+ * @param metricsConsumer {@link Consumer} to emit collected metrics.
+ * @throws MetricsCollectorException if an error occurred during initialization.
+ */
+ void init(
+ ITestDevice iTestDevice,
+ IBuildInfo iBuildInfo,
+ Consumer<InvocationMetric> metricsConsumer)
+ throws MetricsCollectorException;
+
+ /** Cleans up metrics collector. */
+ void destroy();
+}
diff --git a/tools/tradefed-host/src/com/android/afwtest/tradefed/targetprep/metrics/MetricsCollectorException.java b/tools/tradefed-host/src/com/android/afwtest/tradefed/targetprep/metrics/MetricsCollectorException.java
new file mode 100644
index 0000000..5d4da4e
--- /dev/null
+++ b/tools/tradefed-host/src/com/android/afwtest/tradefed/targetprep/metrics/MetricsCollectorException.java
@@ -0,0 +1,48 @@
+/*
+ * 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.afwtest.tradefed.targetprep.metrics;
+
+/**
+ * Custom checked exception to wrap exceptions from com.android.afwtest.tradefed.targetprep.metrics
+ * package.
+ */
+public final class MetricsCollectorException extends Exception {
+
+ public MetricsCollectorException() {
+ super();
+ }
+
+ public MetricsCollectorException(String message) {
+ super(message);
+ }
+
+ public MetricsCollectorException(String message, Throwable cause) {
+ super(message, cause);
+ }
+
+ public MetricsCollectorException(Throwable cause) {
+ super(cause);
+ }
+
+ protected MetricsCollectorException(
+ String message,
+ Throwable cause,
+ boolean enableSuppression,
+ boolean writableStackTrace) {
+ super(message, cause, enableSuppression, writableStackTrace);
+ }
+}
diff --git a/tools/tradefed-host/src/com/android/afwtest/tradefed/targetprep/metrics/MetricsUtils.java b/tools/tradefed-host/src/com/android/afwtest/tradefed/targetprep/metrics/MetricsUtils.java
new file mode 100644
index 0000000..e9e58ab
--- /dev/null
+++ b/tools/tradefed-host/src/com/android/afwtest/tradefed/targetprep/metrics/MetricsUtils.java
@@ -0,0 +1,48 @@
+/*
+ * 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.afwtest.tradefed.targetprep.metrics;
+
+import static com.android.afwtest.tradefed.targetprep.metrics.nano.Metrics.InvocationMetrics.MetricValueMap;
+import static com.android.afwtest.tradefed.targetprep.metrics.nano.Metrics.InvocationMetrics.MetricValue;
+
+/**
+ * Utility class containing methods related to metrics collection classes.
+ */
+public final class MetricsUtils {
+
+ /** Prevent class from instantiation. */
+ private MetricsUtils() {
+ }
+
+ /**
+ * Creates an instance of {@link MetricValueMap.ValuesEntry} from given key and value.
+ *
+ * @param key key of the entry to add.
+ * @param value value of the entry to add.
+ * @return created instance of {@link MetricValueMap.ValuesEntry}.
+ */
+ public static MetricValueMap.ValuesEntry toMetricValueMapEntry(String key, long value) {
+ MetricValue metricValue = new MetricValue();
+ metricValue.setUintValue(value);
+
+ MetricValueMap.ValuesEntry valuesEntry = new MetricValueMap.ValuesEntry();
+ valuesEntry.key = key;
+ valuesEntry.value = metricValue;
+
+ return valuesEntry;
+ }
+}
diff --git a/tools/tradefed-host/src/com/android/afwtest/tradefed/targetprep/metrics/memory/MemoryByProcessGrouper.java b/tools/tradefed-host/src/com/android/afwtest/tradefed/targetprep/metrics/memory/MemoryByProcessGrouper.java
new file mode 100644
index 0000000..6b5190e
--- /dev/null
+++ b/tools/tradefed-host/src/com/android/afwtest/tradefed/targetprep/metrics/memory/MemoryByProcessGrouper.java
@@ -0,0 +1,56 @@
+/*
+ * 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.afwtest.tradefed.targetprep.metrics.memory;
+
+import static com.android.afwtest.tradefed.targetprep.metrics.MetricsUtils.toMetricValueMapEntry;
+import static com.android.afwtest.tradefed.targetprep.metrics.nano.Metrics.InvocationMetrics;
+import static com.android.afwtest.tradefed.targetprep.metrics.nano.Metrics.InvocationMetrics.MetricValueMap;
+import static com.android.afwtest.tradefed.utils.AdbShellUtils.ProcessInfo;
+
+import static java.util.stream.Collectors.toList;
+
+import java.util.Collection;
+import java.util.Map;
+
+/**
+ * {@link MemoryInfoGrouper} grouping memory data into a map of process name to total PSS consumed
+ * by the process.
+ */
+public class MemoryByProcessGrouper implements MemoryInfoGrouper {
+
+ private Collection<String> mProcesses;
+
+ public MemoryByProcessGrouper(Collection<String> processes) {
+ mProcesses = processes;
+ }
+
+ @Override
+ public Collection<MetricValueMap.ValuesEntry> group(Collection<ProcessInfo> ps,
+ Map<Long, Long> memoryInfo) {
+ return ps.stream()
+ .filter(processInfo -> mProcesses.contains(processInfo.getName()))
+ .map(processInfo ->
+ toMetricValueMapEntry(processInfo.getName(), memoryInfo.getOrDefault(
+ processInfo.getPID(), 0L)))
+ .collect(toList());
+ }
+
+ @Override
+ public int getMetricType() {
+ return InvocationMetrics.PSS_BY_PROCESS;
+ }
+}
diff --git a/tools/tradefed-host/src/com/android/afwtest/tradefed/targetprep/metrics/memory/MemoryByUserGrouper.java b/tools/tradefed-host/src/com/android/afwtest/tradefed/targetprep/metrics/memory/MemoryByUserGrouper.java
new file mode 100644
index 0000000..a5a45dc
--- /dev/null
+++ b/tools/tradefed-host/src/com/android/afwtest/tradefed/targetprep/metrics/memory/MemoryByUserGrouper.java
@@ -0,0 +1,61 @@
+/*
+ * 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.afwtest.tradefed.targetprep.metrics.memory;
+
+import static com.android.afwtest.tradefed.targetprep.metrics.MetricsUtils.toMetricValueMapEntry;
+import static com.android.afwtest.tradefed.targetprep.metrics.nano.Metrics.InvocationMetrics;
+import static com.android.afwtest.tradefed.targetprep.metrics.nano.Metrics.InvocationMetrics.MetricValueMap;
+import static com.android.afwtest.tradefed.utils.AdbShellUtils.ProcessInfo;
+
+import static java.util.stream.Collectors.groupingBy;
+import static java.util.stream.Collectors.summingLong;
+import static java.util.stream.Collectors.toList;
+
+import java.util.Collection;
+import java.util.Map;
+
+/**
+ * {@link MemoryInfoGrouper} grouping memory data into a map of user id to total PSS consumed by all
+ * the processes run by the user.
+ */
+public class MemoryByUserGrouper implements MemoryInfoGrouper {
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public Collection<MetricValueMap.ValuesEntry> group(Collection<ProcessInfo> ps,
+ Map<Long, Long> memoryInfo) {
+ return ps.stream()
+ .collect(
+ groupingBy(ProcessInfo::getAndroidUserId,
+ summingLong(processInfo ->
+ memoryInfo.getOrDefault(
+ processInfo.getPID(), 0L))))
+ .entrySet().stream()
+ .map(e -> toMetricValueMapEntry("user_" + e.getKey(), e.getValue()))
+ .collect(toList());
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public int getMetricType() {
+ return InvocationMetrics.TOTAL_PSS_BY_USER;
+ }
+}
diff --git a/tools/tradefed-host/src/com/android/afwtest/tradefed/targetprep/metrics/memory/MemoryCollectorTask.java b/tools/tradefed-host/src/com/android/afwtest/tradefed/targetprep/metrics/memory/MemoryCollectorTask.java
new file mode 100644
index 0000000..5d10d26
--- /dev/null
+++ b/tools/tradefed-host/src/com/android/afwtest/tradefed/targetprep/metrics/memory/MemoryCollectorTask.java
@@ -0,0 +1,110 @@
+/*
+ * 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.afwtest.tradefed.targetprep.metrics.memory;
+
+import static com.android.afwtest.tradefed.targetprep.metrics.MetricsUtils.toMetricValueMapEntry;
+import static com.android.afwtest.tradefed.targetprep.metrics.nano.Metrics.InvocationMetrics.InvocationMetric;
+import static com.android.afwtest.tradefed.targetprep.metrics.nano.Metrics.InvocationMetrics.MetricValueMap;
+
+import com.android.afwtest.tradefed.utils.AdbShellUtils;
+import com.android.tradefed.device.DeviceNotAvailableException;
+import com.android.tradefed.device.ITestDevice;
+
+import java.util.Collection;
+import java.util.Map;
+import java.util.Set;
+import java.util.function.Consumer;
+import java.util.function.Supplier;
+
+/**
+ * {@link Runnable} of the task that is executed in equal time intervals and collects memory
+ * usage information.
+ */
+public class MemoryCollectorTask implements Runnable {
+
+ /**
+ * {@link MemoryInfoGrouper} collection to be used to emit metrics.
+ */
+ private Collection<MemoryInfoGrouper> mMemoryInfoGroupers;
+
+ /**
+ * Current {@link ITestDevice}.
+ */
+ private ITestDevice mTestDevice;
+
+ /**
+ * {@link Consumer} of collected information.
+ */
+ private Consumer<InvocationMetric> mConsumer;
+
+ /**
+ * Tick counter.
+ */
+ private Supplier<Long> mTickCounter;
+
+ /**
+ * Constructs a new instance of the task.
+ *
+ * @param testDevice current {@link ITestDevice}.
+ * @param consumer {@link Consumer} of collected information.
+ * @param tickCounter tick counter.
+ * @param groupers {@link MemoryInfoGrouper} collection to be used to emit metrics.
+ */
+ public MemoryCollectorTask(ITestDevice testDevice,
+ Consumer<InvocationMetric> consumer,
+ Supplier<Long> tickCounter,
+ Collection<MemoryInfoGrouper> groupers) {
+ mTestDevice = testDevice;
+ mConsumer = consumer;
+ mTickCounter = tickCounter;
+ mMemoryInfoGroupers = groupers;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void run() {
+ try {
+ Set<AdbShellUtils.ProcessInfo> ps = AdbShellUtils.getPS(mTestDevice);
+ Map<Long, Long> memoryInfo = AdbShellUtils.getMemoryInfo(mTestDevice);
+ MetricValueMap.ValuesEntry tickEntry =
+ toMetricValueMapEntry("tick", mTickCounter.get());
+
+ mMemoryInfoGroupers
+ .stream()
+ .map(grouper -> {
+ Collection<MetricValueMap.ValuesEntry> entries =
+ grouper.group(ps, memoryInfo);
+ entries.add(tickEntry);
+
+ MetricValueMap.ValuesEntry[] entriesArray =
+ entries.toArray(new MetricValueMap.ValuesEntry[entries.size()]);
+
+ MetricValueMap mapValue = new MetricValueMap();
+ mapValue.values = entriesArray;
+
+ InvocationMetric metric = new InvocationMetric();
+ metric.metricType = grouper.getMetricType();
+ metric.setMapValue(mapValue);
+
+ return metric;
+ }).forEach(mConsumer);
+
+ } catch (DeviceNotAvailableException ignore) {}
+ }
+}
diff --git a/tools/tradefed-host/src/com/android/afwtest/tradefed/targetprep/metrics/memory/MemoryInfoGrouper.java b/tools/tradefed-host/src/com/android/afwtest/tradefed/targetprep/metrics/memory/MemoryInfoGrouper.java
new file mode 100644
index 0000000..d1cfa95
--- /dev/null
+++ b/tools/tradefed-host/src/com/android/afwtest/tradefed/targetprep/metrics/memory/MemoryInfoGrouper.java
@@ -0,0 +1,49 @@
+/*
+ * 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.afwtest.tradefed.targetprep.metrics.memory;
+
+import static com.android.afwtest.tradefed.targetprep.metrics.nano.Metrics.InvocationMetrics.MetricValueMap;
+import static com.android.afwtest.tradefed.utils.AdbShellUtils.ProcessInfo;
+
+import java.util.Collection;
+import java.util.Map;
+
+/**
+ * An object to group memory and process information collected from the device into
+ * performance metrics.
+ */
+public interface MemoryInfoGrouper {
+
+ /**
+ * Groups memory and process information collected from the device into
+ * performance metrics.
+ *
+ * @param ps process information.
+ * @param memoryInfo memory information, map of process id to total PSS consumed be the process.
+ * @return {@link Collection} of performance metrics.
+ */
+ Collection<MetricValueMap.ValuesEntry> group(
+ Collection<ProcessInfo> ps,
+ Map<Long, Long> memoryInfo);
+
+ /**
+ * Gets the type of metrics emitted by the grouper.
+ *
+ * @return the type of metrics emitted by the grouper.
+ */
+ int getMetricType();
+}
diff --git a/tools/tradefed-host/src/com/android/afwtest/tradefed/targetprep/metrics/metrics.proto b/tools/tradefed-host/src/com/android/afwtest/tradefed/targetprep/metrics/metrics.proto
new file mode 100644
index 0000000..bd3cde9
--- /dev/null
+++ b/tools/tradefed-host/src/com/android/afwtest/tradefed/targetprep/metrics/metrics.proto
@@ -0,0 +1,100 @@
+// 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.
+
+syntax = "proto2";
+
+option java_package = "com.android.afwtest.tradefed.targetprep.metrics";
+
+package com_android_afwtest_tradefed_targetprep_metrics;
+
+// Message representing Android Build Fingerprint.
+// google/bullhead/bullhead:7.1.1/N4F26O/3582057:user/release-keys
+message BuildFingerprint {
+ // google
+ required string product_brand = 1;
+ // bullhead
+ required string product_name = 2;
+ // bullhead
+ required string product_device = 3;
+ // 7.1.1
+ required string platform_version = 4;
+ // N4F26O
+ required string release = 5;
+ // 3582057
+ required string version_incremental = 6;
+ // user
+ required string type = 7;
+ // release-keys
+ optional string tags = 8;
+}
+
+// Message describing device on which invocation happened.
+message DeviceInfo {
+ // Fingerprint of the build on the device.
+ required BuildFingerprint build_fingerprint = 1;
+
+ // Version of Google Play Services installed on the device.
+ required string gms_version = 2;
+
+ // API level of the device.
+ required int64 api_level = 3;
+}
+
+// Message describing collection of metrics collected during an invocation.
+message InvocationMetrics {
+
+ // Enum of supported metric types.
+ enum MetricType {
+ UNKNOWN = 1;
+ TOTAL_PROVISIONING_TIME_MS = 2;
+ TOTAL_PSS_BY_USER = 3;
+ PSS_BY_PROCESS = 4;
+ }
+
+ // Value of metric.
+ message MetricValue {
+ oneof value {
+ uint64 uint_value = 1;
+ sint64 sint_value = 2;
+ string string_value = 3;
+ }
+ }
+
+ // Wrapper object to bypass limitation of not being able to have map inside
+ // oneof.
+ message MetricValueMap {
+ map<string, MetricValue> values = 1;
+ }
+
+ // Message describing a single metric collected during an invocation.
+ message InvocationMetric {
+ required MetricType metric_type = 1;
+ oneof metric_value {
+ MetricValue scalar_value = 2;
+ MetricValueMap map_value = 3;
+ }
+ }
+
+ // Unique ID.
+ required string id = 1;
+
+ // Timestamp of the invocation in milliseconds.
+ required int64 timestamp_millis = 2;
+
+ // Information about the device on which invocation happened.
+ required DeviceInfo device_info = 3;
+
+ // Collection of metrics.
+ repeated InvocationMetric metrics = 4;
+} \ No newline at end of file
diff --git a/tools/tradefed-host/src/com/android/afwtest/tradefed/testtype/AfwAndroidJUnitTest.java b/tools/tradefed-host/src/com/android/afwtest/tradefed/testtype/AfwAndroidJUnitTest.java
new file mode 100644
index 0000000..bfa3e29
--- /dev/null
+++ b/tools/tradefed-host/src/com/android/afwtest/tradefed/testtype/AfwAndroidJUnitTest.java
@@ -0,0 +1,54 @@
+/*
+ * 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.afwtest.tradefed.testtype;
+
+import com.android.afwtest.tradefed.TestConfig;
+import com.android.ddmlib.Log;
+import com.android.tradefed.testtype.AndroidJUnitTest;
+
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Test class for Android for Work Test Harness JUnit tests.
+ */
+public class AfwAndroidJUnitTest extends AndroidJUnitTest {
+
+ private static final String LOG_TAG = "afwtest.AfwAndroidJUnitTest";
+
+ // Default execution timeout for afw test packages
+ private static final long TEST_TIMEOUT_MS = TimeUnit.MINUTES.toMillis(10);
+
+ // 1 min in milliseconds
+ private static final long ONE_MIN_MS = TimeUnit.MINUTES.toMillis(1);
+
+ /**
+ * {@inheritDoc}
+ */
+ public AfwAndroidJUnitTest() {
+ super();
+
+ long testTimeout =
+ TimeUnit.MINUTES.toMillis(TestConfig.getInstance().getTestTimeoutMin(0));
+ if (testTimeout == 0) {
+ testTimeout = TEST_TIMEOUT_MS +
+ TestConfig.getInstance().getTimeoutSize() * ONE_MIN_MS * 5;
+ }
+ Log.i(LOG_TAG, String.format("Instrumentation test timeout = %d seconds",
+ TimeUnit.MILLISECONDS.toSeconds(testTimeout)));
+ setTestTimeout((int) testTimeout);
+ }
+}
diff --git a/tools/tradefed-host/src/com/android/afwtest/tradefed/testtype/AfwTest.java b/tools/tradefed-host/src/com/android/afwtest/tradefed/testtype/AfwTest.java
new file mode 100644
index 0000000..b19ea25
--- /dev/null
+++ b/tools/tradefed-host/src/com/android/afwtest/tradefed/testtype/AfwTest.java
@@ -0,0 +1,161 @@
+/*
+ * 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.afwtest.tradefed.testtype;
+
+import com.android.afwtest.tradefed.TestConfig;
+import com.android.compatibility.common.tradefed.build.CompatibilityBuildHelper;
+import com.android.compatibility.common.tradefed.testtype.IModuleRepo;
+import com.android.compatibility.common.tradefed.testtype.CompatibilityTest;
+import com.android.compatibility.common.tradefed.testtype.ModuleRepo;
+import com.android.tradefed.build.IBuildInfo;
+import com.android.tradefed.config.Option;
+import com.android.tradefed.config.OptionClass;
+import com.android.tradefed.device.DeviceNotAvailableException;
+import com.android.tradefed.device.ITestDevice;
+import com.android.tradefed.log.LogUtil.CLog;
+import com.android.tradefed.result.ITestInvocationListener;
+import com.android.tradefed.util.FileUtil;
+
+import java.io.File;
+import java.io.IOException;
+
+/**
+ * Customized {@link CompatibilityTest} for running afw-test tests.
+ * <p>Supports running all the tests contained in an afw-test plan, or individual test packages.
+ * </p>
+ */
+@OptionClass(alias = "compatibility")
+public class AfwTest extends CompatibilityTest {
+
+ private static final String CMD_DISABLE_PKG_VERIFICATION =
+ "settings put global package_verifier_enable 0";
+
+ @Option(name = "afw-test-config-file",
+ description = "Test harness config file to replace afw-test.props.")
+ private String mConfigFile = null;
+
+ @Option(name = "screenshot",
+ description = "Take a screenshot on every test")
+ private boolean mScreenshot = false;
+
+ /**
+ * A {@link CompatibilityBuildHelper} instance to access afw-test build info, such as the
+ * absolute paths of the test plan file and test case apks.
+ */
+ protected CompatibilityBuildHelper mAfwTestBuild = null;
+
+ /**
+ * Internal {@link ITestInvocationListener} wrapping one provided by framework with additional
+ * listeners specific for {@link AfwTest}.
+ */
+ protected ITestInvocationListener mInternalListener = null;
+
+ /**
+ * A {@link ITestDevice} instance representing a device under test.
+ */
+ protected ITestDevice mDevice = null;
+
+ /**
+ * {@inheritDoc}
+ */
+ public AfwTest() {
+ this(1 /*totalShards*/, new ModuleRepo() /*moduleRepo*/, 0 /*shardIndex*/);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public AfwTest(int totalShards, IModuleRepo moduleRepo, Integer shardIndex) {
+ super(totalShards, moduleRepo, shardIndex);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void setBuild(IBuildInfo build) {
+ super.setBuild(build);
+
+ mAfwTestBuild = new CompatibilityBuildHelper(build);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void run(ITestInvocationListener listener) throws DeviceNotAvailableException {
+ mInternalListener = listener;
+
+ beforeTest();
+ super.run(mInternalListener);
+ afterTest();
+ }
+
+ /**
+ * Executes general preparation steps before running the test.
+ *
+ * @throws DeviceNotAvailableException if connection to the device is lost.
+ */
+ protected void beforeTest() throws DeviceNotAvailableException {
+ CLog.i("Running afw-test");
+
+ mDevice = getDevice();
+ if (mDevice == null) {
+ throw new IllegalArgumentException("Device not found");
+ }
+
+ try {
+ // Replace afw-test.props with a customized file if there is.
+ if (mConfigFile != null) {
+ FileUtil.copyFile(new File(mConfigFile), getTestConfigFile());
+ }
+
+ // Init TestConfig singleton.
+ TestConfig.init(getTestConfigFile());
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+
+ // Disable package verification.
+ // This is necessary because CtsTest.run() will push TestDeviceSetup.apk
+ // to the testing device which will trigger package verification dialog.
+ mDevice.executeShellCommand(CMD_DISABLE_PKG_VERIFICATION);
+
+ if (mScreenshot) {
+ mInternalListener = new ScreenshotListener(mInternalListener, mDevice);
+ }
+ }
+
+ /**
+ * Executes general tear down steps after running the test.
+ *
+ * @throws DeviceNotAvailableException if connection to the device is lost.
+ */
+ protected void afterTest() throws DeviceNotAvailableException {
+ // Disable package verification again; it might be enabled by some tests.
+ mDevice.executeShellCommand(CMD_DISABLE_PKG_VERIFICATION);
+ }
+
+ /**
+ * Reads test configuration file, afw-test.props.
+ *
+ * @return {@link File} object keeping loaded content from afw-test.props
+ */
+ protected File getTestConfigFile() throws IOException {
+ return new File(mAfwTestBuild.getTestsDir(), "afw-test.props");
+ }
+}
diff --git a/tools/tradefed-host/src/com/android/afwtest/tradefed/testtype/ScreenshotListener.java b/tools/tradefed-host/src/com/android/afwtest/tradefed/testtype/ScreenshotListener.java
new file mode 100644
index 0000000..e48dd65
--- /dev/null
+++ b/tools/tradefed-host/src/com/android/afwtest/tradefed/testtype/ScreenshotListener.java
@@ -0,0 +1,93 @@
+/*
+ * 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.afwtest.tradefed.testtype;
+
+import com.android.ddmlib.testrunner.TestIdentifier;
+import com.android.tradefed.device.DeviceNotAvailableException;
+import com.android.tradefed.device.ITestDevice;
+import com.android.tradefed.log.LogUtil.CLog;
+import com.android.tradefed.result.FileInputStreamSource;
+import com.android.tradefed.result.ITestInvocationListener;
+import com.android.tradefed.result.InputStreamSource;
+import com.android.tradefed.result.LogDataType;
+import com.android.tradefed.result.ResultForwarder;
+
+import java.io.File;
+import java.util.Map;
+
+/**
+ * Test invocation listener to capture device screenshot after the test
+ */
+public class ScreenshotListener extends ResultForwarder {
+
+ private static final String UIAUTOMATOR = "/system/bin/uiautomator";
+ private static final String UIAUTOMATOR_DUMP_COMMAND = "dump";
+ private static final String UIDUMP_DEVICE_PATH = "/data/local/tmp/uidump.xml";
+
+ final private ITestDevice mDevice;
+ private boolean testFailed = false;
+
+ public ScreenshotListener(ITestInvocationListener listener, ITestDevice device) {
+ super(listener);
+ mDevice = device;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void testEnded(TestIdentifier test, Map<String, String> testMetrics) {
+ super.testEnded(test, testMetrics);
+ CLog.i("Capturing device screenshot after test %s", test.toString());
+ try {
+ final InputStreamSource screenSource = mDevice.getScreenshot("PNG",
+ /* rescale */ !testFailed);
+ super.testLog(String.format("%s-screenshot", test.toString()), LogDataType.PNG,
+ screenSource);
+ screenSource.cancel();
+ } catch (DeviceNotAvailableException e) {
+ CLog.e(e);
+ CLog.e("Device %s became unavailable while capturing screenshot",
+ mDevice.getSerialNumber());
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void testFailed(TestIdentifier test, String trace) {
+ super.testFailed(test, trace);
+
+ testFailed = true;
+
+ CLog.i("Dumping device ui layout after test %s", test.toString());
+ try {
+ mDevice.executeShellCommand(
+ String.format("%s %s %s", UIAUTOMATOR, UIAUTOMATOR_DUMP_COMMAND,
+ UIDUMP_DEVICE_PATH));
+ final File dumpFile = mDevice.pullFile(UIDUMP_DEVICE_PATH);
+ final InputStreamSource dumpSource = new FileInputStreamSource(dumpFile,
+ /* deleteFileOnCancel */ true);
+ super.testLog(String.format("%s-uidump", test.toString()), LogDataType.XML, dumpSource);
+ dumpSource.cancel();
+ } catch (DeviceNotAvailableException e) {
+ CLog.e(e);
+ CLog.e("Device %s became unavailable while capturing ui layout dump",
+ mDevice.getSerialNumber());
+ }
+ }
+}
diff --git a/tools/tradefed-host/src/com/android/afwtest/tradefed/utils/AdbShellUtils.java b/tools/tradefed-host/src/com/android/afwtest/tradefed/utils/AdbShellUtils.java
new file mode 100644
index 0000000..50e7706
--- /dev/null
+++ b/tools/tradefed-host/src/com/android/afwtest/tradefed/utils/AdbShellUtils.java
@@ -0,0 +1,191 @@
+/*
+ * 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.afwtest.tradefed.utils;
+
+import com.android.tradefed.device.DeviceNotAvailableException;
+import com.android.tradefed.device.ITestDevice;
+
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Utility class containing methods related to executing various ADB Shell commands.
+ */
+public final class AdbShellUtils {
+
+ /** Prevent class from instantiation. */
+ private AdbShellUtils() {
+ }
+
+ /**
+ * Gets process list currently running on the device grouped by user.
+ *
+ * @param testDevice device to get process list from.
+ * @return {@link Map} of user ids to a {@link Set} of process ids run by the user.
+ * @throws DeviceNotAvailableException if device became unavailable during the execution of
+ * the method.
+ */
+ public static Set<ProcessInfo> getPS(ITestDevice testDevice)
+ throws DeviceNotAvailableException {
+ Set<ProcessInfo> result = new HashSet<>();
+ String psOutput = testDevice.executeShellCommand("ps -e");
+ for (String line : psOutput.split("\n")) {
+ if (line.contains("USER")) {
+ continue;
+ }
+
+ String[] columns = line.split("\\s+");
+ String user = columns[0];
+ long pid = Long.parseLong(columns[1]);
+ long ppid = Long.parseLong(columns[2]);
+ String name = columns[8];
+
+ result.add(new ProcessInfo(user, pid, ppid, name));
+ }
+ return result;
+ }
+
+ /**
+ * Gets memory consumption information about processes currently running on the device.
+ *
+ * @param testDevice device to get memory information from.
+ * @return {@link Map} of process ids to total PSS of the process.
+ * @throws DeviceNotAvailableException if device became unavailable during the execution of
+ * the method.
+ */
+ public static Map<Long, Long> getMemoryInfo(ITestDevice testDevice)
+ throws DeviceNotAvailableException {
+ Map<Long, Long> result = new HashMap<>();
+ String miOutput = testDevice.executeShellCommand("su system dumpsys meminfo -c");
+ for (String line : miOutput.split("\n")) {
+ String[] columns = line.split(",");
+ if (columns[0].equals("proc")) {
+ long pid = Long.parseLong(columns[3]);
+ long pss = Long.parseLong(columns[4]);
+ result.put(pid, pss);
+ }
+ }
+ return result;
+ }
+
+ /**
+ * Class representing information about process retrieved from ps command.
+ */
+ public static final class ProcessInfo {
+
+ /**
+ * {@link Pattern} of Android user name that encodes user id and activity id.
+ */
+ private static final Pattern APP_PROCESS_USER_PATTERN = Pattern.compile("u(\\d+)_a\\d+");
+
+ private final String mUser;
+ private final long mPID;
+ private final long mPPID;
+ private final String mName;
+
+ private ProcessInfo(String user, long pid, long ppid, String name) {
+ this.mUser = user;
+ this.mPID = pid;
+ this.mPPID = ppid;
+ this.mName = name;
+ }
+
+ /**
+ * Gets user name running the process.
+ *
+ * @return user name running the process.
+ */
+ public String getUser() {
+ return mUser;
+ }
+
+ /**
+ * Gets the id of the process.
+ *
+ * @return the id of the process.
+ */
+ public long getPID() {
+ return mPID;
+ }
+
+ /**
+ * Gets the id of the parent of the process.
+ *
+ * @return the id of the parent of the process.
+ */
+ public long getPPID() {
+ return mPPID;
+ }
+
+ /**
+ * Get the name of the process.
+ *
+ * @return the name of the process.
+ */
+ public String getName() {
+ return mName;
+ }
+
+ /**
+ * Gets the id if Android user running the process, or -1 of the process is run by
+ * the system.
+ *
+ * @return the id if Android user running the process, or -1 of the process is run by
+ * the system.
+ */
+ public long getAndroidUserId() {
+ long userId = -1;
+ Matcher matcher = APP_PROCESS_USER_PATTERN.matcher(mName);
+ if (matcher.find()) {
+ userId = Long.parseLong(matcher.group(1));
+ }
+ return userId;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+
+ ProcessInfo that = (ProcessInfo) o;
+ return mPID == that.mPID &&
+ mPPID == that.mPPID &&
+ Objects.equals(mUser, that.mUser) &&
+ Objects.equals(mName, that.mName);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public int hashCode() {
+ return Objects.hash(mUser, mPID, mPPID, mName);
+ }
+ }
+}
diff --git a/tools/tradefed-host/src/com/android/afwtest/tradefed/utils/ReflectionUtils.java b/tools/tradefed-host/src/com/android/afwtest/tradefed/utils/ReflectionUtils.java
new file mode 100644
index 0000000..e51aa1b
--- /dev/null
+++ b/tools/tradefed-host/src/com/android/afwtest/tradefed/utils/ReflectionUtils.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.afwtest.tradefed.utils;
+
+import java.lang.reflect.Constructor;
+import java.lang.reflect.InvocationTargetException;
+
+/** Set of utility methods for reflection operations. */
+public final class ReflectionUtils {
+
+ /** Prevent class from instantiation. */
+ private ReflectionUtils() {}
+
+ /**
+ * Creates instance of specified class.
+ *
+ * @param className name of the class to instantiate.
+ * @param clazz {@link Class} extended by specified class to cast result to.
+ * @param args arguments of constructor to invoke.
+ * @return instance of specified class.
+ * @throws ClassNotFoundException if specified class cannot be found in classpath.
+ * @throws NoSuchMethodException if no constructor can be found suitable for specified
+ * constructor arguments.
+ * @throws IllegalAccessException if access to suitable constructor is forbidden.
+ * @throws InstantiationException if the class that declares the underlying constructor
+ * represents an abstract class.
+ * @throws InvocationTargetException if the underlying constructor throws an exception.
+ * @throws IllegalArgumentException if class specified by name is not assignable to the class
+ * specified by class instance.
+ */
+ @SuppressWarnings("unchecked")
+ public static <T> T getInstanceOf(String className, Class<T> clazz, Object... args)
+ throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException,
+ InstantiationException, InvocationTargetException {
+
+ Class<?> resolvedClass = clazz.getClassLoader().loadClass(className);
+ if (!clazz.isAssignableFrom(resolvedClass)) {
+ throw new IllegalArgumentException(
+ String.format(
+ "An instance of %s cannot be assigned to %s.",
+ resolvedClass.getName(), clazz.getName()));
+ }
+ Constructor<T> constructor =
+ (Constructor<T>) resolvedClass.getConstructor(getClassesOf(args));
+ return constructor.newInstance(args);
+ }
+
+ /**
+ * Gets classes of specified objects.
+ *
+ * @param objects objects to get classes of.
+ * @return classes of specified objects.
+ */
+ private static Class<?>[] getClassesOf(Object... objects) {
+ Class<?>[] result = new Class[objects.length];
+ for (int i = 0; i < objects.length; i++) {
+ result[i] = objects[i].getClass();
+ }
+ return result;
+ }
+}
diff --git a/tools/tradefed-host/src/com/android/afwtest/tradefed/utils/StreamDiscarder.java b/tools/tradefed-host/src/com/android/afwtest/tradefed/utils/StreamDiscarder.java
new file mode 100644
index 0000000..dd4191c
--- /dev/null
+++ b/tools/tradefed-host/src/com/android/afwtest/tradefed/utils/StreamDiscarder.java
@@ -0,0 +1,56 @@
+/*
+ * 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.afwtest.tradefed.utils;
+
+import com.android.tradefed.log.LogUtil.CLog;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * {@link Runnable} that reads and discards data from specified stream.
+ */
+public final class StreamDiscarder implements Runnable {
+
+ private static final int BATCH_SIZE = 1024;
+
+ private final InputStream mStream;
+ private final byte[] buffer;
+
+ /**
+ * Constructs {@link StreamDiscarder} instance.
+ *
+ * @param is {@link InputStream} to read and discard data from.
+ */
+ public StreamDiscarder(InputStream is) {
+ mStream = is;
+ buffer = new byte[BATCH_SIZE];
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ @SuppressWarnings("StatementWithEmptyBody")
+ public void run() {
+ try {
+ while (!Thread.interrupted() && mStream.read(buffer) > 0) {}
+ } catch (IOException e) {
+ CLog.e(e);
+ }
+ }
+}
diff --git a/tools/tradefed-host/src/com/android/afwtest/tradefed/utils/TimeUtil.java b/tools/tradefed-host/src/com/android/afwtest/tradefed/utils/TimeUtil.java
new file mode 100644
index 0000000..d5730ce
--- /dev/null
+++ b/tools/tradefed-host/src/com/android/afwtest/tradefed/utils/TimeUtil.java
@@ -0,0 +1,66 @@
+/*
+ * 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.afwtest.tradefed.utils;
+
+import com.android.tradefed.device.DeviceNotAvailableException;
+import com.android.tradefed.device.ITestDevice;
+import com.android.tradefed.log.LogUtil.CLog;
+
+import java.text.SimpleDateFormat;
+import java.util.Date;
+
+/**
+ * Time related utils.
+ */
+public class TimeUtil {
+
+ /**
+ * Return the current timestamp in a compressed format, e.g.: 2015.11.25_11.42.12
+ */
+ public static String getResultTimestamp() {
+ SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy.MM.dd_HH.mm.ss");
+ return dateFormat.format(new Date());
+ }
+
+ /**
+ * Sync device time with host.
+ *
+ * @param device test device
+ */
+ public static void syncHostTime(ITestDevice device) throws DeviceNotAvailableException {
+ CLog.i(String.format("Before time sync: %s", device.executeShellCommand("date -u")));
+
+ Date date = new Date();
+ String dateString = null;
+ if (device.getApiLevel() < 23) {
+ // set date in epoch format
+ dateString = Long.toString(date.getTime() / 1000); //ms to s
+ } else {
+ // set date with POSIX like params
+ SimpleDateFormat sdf = new java.text.SimpleDateFormat("MMddHHmmyyyy.ss");
+ sdf.setTimeZone(java.util.TimeZone.getTimeZone("UTC"));
+ dateString = sdf.format(date);
+ }
+ // best effort, no verification
+ String cmd = String.format("date -u %s", dateString);
+ CLog.i(String.format("Setting time: %s", cmd));
+ CLog.i(device.executeShellCommand(cmd));
+
+ CLog.i(String.format("After time sync: %s", device.executeShellCommand("date -u")));
+ }
+
+}