import history from sso://googleplex-android/platform/test/AfwTestHarness oc-dev b/65199386
diff --git a/AfwTestCaseList.mk b/AfwTestCaseList.mk
new file mode 100644
index 0000000..8146e37
--- /dev/null
+++ b/AfwTestCaseList.mk
@@ -0,0 +1,55 @@
+#
+# 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.
+#
+
+# Test packages that require an associated test package XML.
+afw_th_test_packages := \
+    AfwTestNfcProvisioningTestCases \
+    AfwTestNonSuwPoProvisioningTestCases \
+    AfwTestQRCodeProvisioningTestCases \
+    AfwTestSuwDoProvisioningTestCases \
+    AfwTestSuwPoProvisioningTestCases
+
+# Support packages
+afw_th_support_packages := \
+    AfwThDeviceAdmin \
+    AfwThSystemUtil \
+    AfwThUtil
+
+# Additional prebuilt utilities used by tests
+afw_th_prebuilt_utils := \
+    CtsDeviceInfo.apk \
+    TestDpc.apk
+
+afw_th_host_libraries :=
+
+
+# All the test case apk files that will end up under the repository/testcases
+# directory of the final AfW Test distribution.
+AFW_TH_TEST_CASES := $(call afw-th-get-package-paths,$(afw_th_test_packages)) \
+    $(call afw-th-get-lib-paths,$(afw_th_host_libraries))
+
+# All the support apk files that will end up under repository/testcases
+# directory of the final AfW Test distribution.
+AFW_TH_SUPPORT_PACKAGE_APKS := $(call afw-th-get-package-paths,$(afw_th_support_packages))
+
+# All the XMLs that will end up under the repository/testcases
+# and that need to be created before making the final AfW Test Harness distribution.
+AFW_TH_TEST_CONFIGS := $(call afw-th-get-test-configs,$(afw_th_test_packages)) \
+    $(call afw-th-get-test-configs,$(afw_th_host_libraries))
+
+# The following files will be placed in the tools directory of the Harness distribution
+AFW_TH_TOOLS_LIST :=
+
diff --git a/AfwTestHarnessBuild.mk b/AfwTestHarnessBuild.mk
new file mode 100644
index 0000000..5a0293d
--- /dev/null
+++ b/AfwTestHarnessBuild.mk
@@ -0,0 +1,65 @@
+#
+# 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.
+#
+
+-include test/AfwTestHarness/AfwTestCaseList.mk
+
+afw_th_src_dir := test/AfwTestHarness
+afw_th_dir := $(HOST_OUT)/afw-th
+afw_th_tools_src_dir := $(afw_th_src_dir)/tools
+afw_th_tests_src_dir := $(afw_th_src_dir)/tests
+
+TH_COMPATIBILITY_HOST_UTIL_JAR := $(HOST_OUT_JAVA_LIBRARIES)/compatibility-host-util.jar
+TF_JAR := $(HOST_OUT_JAVA_LIBRARIES)/tradefed.jar
+AFW_TH_TF_JAR := $(HOST_OUT_JAVA_LIBRARIES)/afw-test-tradefed.jar
+AFW_TH_TF_EXEC_PATH ?= $(HOST_OUT_EXECUTABLES)/afw-test-tradefed
+AFW_TH_TF_README_PATH := $(afw_th_tools_src_dir)/tradefed-host/README
+
+AFW_TH_PREBUILT_UTILS_PATH := $(foreach prebuilt, $(afw_th_prebuilt_utils), $(afw_th_tools_src_dir)/prebuilt/$(prebuilt))
+
+$(afw_th_dir)/all_afw_th_files_stamp: $(AFW_TH_TEST_CASES) $(AFW_TH_SUPPORT_PACKAGE_APKS) $(TF_JAR) $(AFW_TH_TF_JAR) $(AFW_TH_TF_EXEC_PATH) $(AFW_TH_TF_README_PATH) $(TH_COMPATIBILITY_HOST_UTIL_JAR) $(ACP)
+
+# Make necessary directory for afw-test
+	$(hide) mkdir -p $(TMP_DIR)
+	$(hide) mkdir -p $(PRIVATE_DIR)/tools
+	$(hide) mkdir -p $(PRIVATE_DIR)/testcases
+# Copy executable and JARs to afw-test directory
+	$(hide) $(ACP) -fp $(TF_JAR) $(AFW_TH_TF_JAR) $(AFW_TH_TF_EXEC_PATH) $(AFW_TH_TF_README_PATH) $(TH_COMPATIBILITY_HOST_UTIL_JAR) $(PRIVATE_DIR)/tools
+	$(hide) $(ACP) -fp $(AFW_TH_PREBUILT_UTILS_PATH) $(PRIVATE_DIR)/testcases
+	$(hide) touch $@
+
+
+afw_th_name := android-afw-test-harness
+
+# Package afw-test-harness and clean up.
+#
+INTERNAL_AFW_TH_TARGET := $(afw_th_dir)/$(afw_th_name).zip
+$(INTERNAL_AFW_TH_TARGET): PRIVATE_NAME := android-cts
+$(INTERNAL_AFW_TH_TARGET): PRIVATE_AFW_TH_DIR := $(afw_th_dir)
+$(INTERNAL_AFW_TH_TARGET): PRIVATE_DIR := $(afw_th_dir)/android-cts
+$(INTERNAL_AFW_TH_TARGET): TMP_DIR := $(afw_th_dir)/temp
+$(INTERNAL_AFW_TH_TARGET): $(afw_th_dir)/all_afw_th_files_stamp $(AFW_TH_TEST_CONFIGS)
+	$(hide) echo "Package Android for Work Test Harness: $@"
+	$(hide) $(ACP) -f $(afw_th_src_dir)/afw-test.props $(PRIVATE_DIR)/testcases
+	$(hide) $(ACP) -f $(afw_th_src_dir)/zip_exclude.lst $(afw_th_dir)
+	$(hide) $(ACP) -f $(afw_th_src_dir)/apps/SystemUtil/afw-test-system-util-permissions.xml $(PRIVATE_DIR)/testcases
+	$(hide) cd $(dir $@) && zip -rq $(notdir $@) $(PRIVATE_NAME) -x@zip_exclude.lst
+
+.PHONY: afw-test-harness
+afw-test-harness: $(INTERNAL_AFW_TH_TARGET) adb
+	$(hide) echo "********************************************"
+	$(hide) echo "To start tradefed, run: afw-test-tradefed"
+	$(hide) echo "********************************************"
+$(call dist-for-goals,afw-test-harness,$(INTERNAL_AFW_TH_TARGET))
diff --git a/AfwTestHarnessBuildUtil.mk b/AfwTestHarnessBuildUtil.mk
new file mode 100644
index 0000000..3bb8629
--- /dev/null
+++ b/AfwTestHarnessBuildUtil.mk
@@ -0,0 +1,31 @@
+#
+# 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.
+#
+
+LOCAL_PATH := $(call my-dir)
+
+# Functions to get the paths of the build outputs.
+define afw-th-get-package-paths
+	$(foreach pkg,$(1),$(AFW_TH_TESTCASES_OUT)/$(pkg).apk)
+endef
+
+define afw-th-get-test-configs
+	$(foreach name,$(1),$(AFW_TH_TESTCASES_OUT)/$(name).config)
+endef
+
+define afw-th-get-lib-paths
+	$(foreach lib,$(1),$(AFW_TH_TESTCASES_OUT)/$(lib).jar)
+endef
+
diff --git a/Android.mk b/Android.mk
new file mode 100644
index 0000000..2201d13
--- /dev/null
+++ b/Android.mk
@@ -0,0 +1,21 @@
+#
+# 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.
+#
+
+include test/AfwTestHarness/build/config.mk
+include test/AfwTestHarness/AfwTestHarnessBuildUtil.mk
+include test/AfwTestHarness/AfwTestHarnessBuild.mk
+
+include $(call all-subdir-makefiles)
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..d645695
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,202 @@
+
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+   APPENDIX: How to apply the Apache License to your work.
+
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "[]"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. We also recommend that a
+      file or class name and description of purpose be included on the
+      same "printed page" as the copyright notice for easier
+      identification within third-party archives.
+
+   Copyright [yyyy] [name of copyright owner]
+
+   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.
diff --git a/README b/README
new file mode 100644
index 0000000..0ab5400
--- /dev/null
+++ b/README
@@ -0,0 +1,113 @@
+Android For Work (Afw) Test Harness
+-----------------------------------------------------
+About
+
+Afw Test Harness is designed to validate Android for Work compatibility of
+Android devices. It's a test suite consisting of support apps, test cases,
+test runner and configuration files. The test runner, afw-test-tradefed,
+is built on top of cts-tradefed. The way of building & running Afw Test
+Harness is quite similar to CTS.
+
+
+Host Machine Setup
+------------------
+1. Ensure 'adb' is in your current PATH. adb can be found in the
+Android SDK available from http://developer.android.com
+
+Example:
+  PATH=$PATH:/home/myuser/android-sdk-linux_x86/platform-tools
+
+2. Connect the device to the host machine.
+
+3. Ensure device is visible via 'adb devices'
+
+4. Make sure Java Runtime 1.7 or 1.8 is installed
+
+
+Afw Test Harness Configurations
+-------------------------------
+1. Get Testing Work account
+
+Testing work account is required to run the test harness. Testing work account
+can be obtained from https://android-for-work-test-harness.appspot.com/.
+Please reach out to the Google point of contact within your company to generate
+these accounts.
+Once obtained the testing account, specify it in the test harness configuration
+file, afw-test.props, with the following properties:
+
+work_account_username
+work_account_password
+
+2. Configure WIFI network
+
+Configure the Wi-Fi network in afw-test.props with the following properties:
+
+wifi_ssid
+wifi_password (optional)
+wifi_security_type (optional, available options are: NONE, WEP or WPA)
+
+
+Build Afw Test Harness
+----------------------
+Before building or running the harness, init the environment variables:
+
+  $ source build/envsetup.sh
+  $ lunch
+
+Select a proper device type and press Enter.
+
+To build Afw Test Harness, browse to this directory and then:
+
+  $ make afw-test-harness -j24
+
+Similar to CTS, "make afw-test-harness" will create a directory,
+
+  out/host/<platform>/afw-th/android-cts
+
+which contains all necessary binaries, configuration files and tools to run the
+test suite. This directory is also zipped into a file, android-afw-test-harness.zip,
+for distribution.
+
+
+Run Afw Test Harness
+----------------------------------------------
+1. From building environment, launch the test runner from the command line:
+
+  $ afw-test-tradefed
+
+2. From the unzipped folder of android-afw-test-harness.zip, launch the test
+   runner from the command line:
+
+  $ ./android‐cts/tools/afw-test‐tradefed
+
+  Make sure the ./android‐cts/testcases/afw-test.props has the
+  testing work account and WIFI configuration.
+
+3. Run the test plan "afw-userdebug-build": run afw-userdebug-build
+
+   Type 'list plans' to see all the test plans.
+
+   Plan definitions can be found in:
+     out/host/<platform>/afw-th/android-cts/tools/afw-test-tradefed.jar/config.
+
+  Each test plan is a xml file which contains all or several test packages from
+  AfwTestHarness/tests.
+
+  Plan 'afw-userdebug-build' contains all test packages that require a userdebug build.
+  Plan 'afw-user-build' can run on user build but requires the test device to be setup
+  properly, including completing Setup Wizard and enabling USB debugging.
+
+4. Run a single test module, for example "AfwTestNfcProvisioningTestCases"
+
+   cts-tf > run cts --module AfwTestNfcProvisioningTestCases
+
+   All modules can be found by executing "list modules" command in
+   afw-test-tradefed console.
+
+5. If the device supports both 64 bits & 32 bits abi, you can force the test
+   harness to run on a specific abi, e.g.:
+
+   cts-tf > run afw-userdebug-build --abi arm64-v8a -l DEBUG
+
+6.For more options:
+   cts-tf > run cts --help
diff --git a/afw-test.props b/afw-test.props
new file mode 100644
index 0000000..2e72d51
--- /dev/null
+++ b/afw-test.props
@@ -0,0 +1,17 @@
+device_admin_pkg_name=com.afwsamples.testdpc
+device_admin_receiver=com.afwsamples.testdpc.DeviceAdminReceiver
+device_admin_pkg_location=https://testdpc-latest-apk.appspot.com
+device_admin_pkg_signature_hash=gJD2YwtOiWJHkSMkkIfLRlj-quNqG1fb6v100QmzM9w=
+wifi_ssid=
+wifi_password=
+wifi_security_type=
+work_account_username=
+work_account_password=
+factory_reset_timeout_min=20
+timeout_size=M
+test_timeout_min=
+mute_app_crash_dialogs=true
+app_crash_whitelist=
+oem_widgets=
+leave_all_system_apps_enabled=
+admin_extras_bundle=
diff --git a/apps/Android.mk b/apps/Android.mk
new file mode 100644
index 0000000..6801acf
--- /dev/null
+++ b/apps/Android.mk
@@ -0,0 +1,18 @@
+#
+# 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.
+#
+
+include $(call all-subdir-makefiles)
+
diff --git a/apps/DeviceAdmin/Android.mk b/apps/DeviceAdmin/Android.mk
new file mode 100644
index 0000000..872af31
--- /dev/null
+++ b/apps/DeviceAdmin/Android.mk
@@ -0,0 +1,33 @@
+#
+# 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.
+#
+
+LOCAL_PATH:= $(call my-dir)
+
+include $(CLEAR_VARS)
+
+LOCAL_MODULE_TAGS := optional
+
+LOCAL_MODULE_PATH := $(TARGET_OUT_DATA_APPS)
+
+LOCAL_STATIC_JAVA_LIBRARIES := AfwThCommonLib
+
+LOCAL_SRC_FILES := $(call all-java-files-under, src)
+
+LOCAL_PACKAGE_NAME := AfwThDeviceAdmin
+
+LOCAL_SDK_VERSION := 22
+
+include $(BUILD_AFW_TEST_SUPPORT_PACKAGE)
diff --git a/apps/DeviceAdmin/AndroidManifest.xml b/apps/DeviceAdmin/AndroidManifest.xml
new file mode 100644
index 0000000..990cf22
--- /dev/null
+++ b/apps/DeviceAdmin/AndroidManifest.xml
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="com.android.afwtest.deviceadmin" >
+
+    <uses-sdk android:minSdkVersion="22" android:targetSdkVersion="23"/>
+
+    <application>
+         <receiver
+            android:name=".AdminReceiver"
+            android:permission="android.permission.BIND_DEVICE_ADMIN"
+            android:exported="true">
+            <meta-data android:name="android.app.device_admin"
+                       android:resource="@xml/device_admin" />
+            <intent-filter>
+                <action android:name="android.app.action.DEVICE_ADMIN_ENABLED" />
+            </intent-filter>
+        </receiver>
+
+        <activity
+                android:name="com.android.afwtest.deviceadmin.FactoryResetActivity"
+                android:label="Factory Reset"
+                android:exported="true">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+                <category android:name="android.intent.category.LAUNCHER" />
+            </intent-filter>
+        </activity>
+    </application>
+
+</manifest>
diff --git a/apps/DeviceAdmin/README b/apps/DeviceAdmin/README
new file mode 100644
index 0000000..c618be6
--- /dev/null
+++ b/apps/DeviceAdmin/README
@@ -0,0 +1,13 @@
+- About
+
+  An admin app that helps factory resetting a device.
+
+- Build & Install:
+
+  make AfwThDeviceAdmin
+  adb install -d -r AfwThDeviceAdmin.apk
+
+- Factory reset a device
+
+  adb shell dpm set-active-admin com.android.afwtest.deviceadmin/com.android.afwtest.deviceadmin.AdminReceiver
+  adb shell am start -n com.android.afwtest.deviceadmin/.FactoryResetActivity
diff --git a/apps/DeviceAdmin/res/xml/device_admin.xml b/apps/DeviceAdmin/res/xml/device_admin.xml
new file mode 100644
index 0000000..4ecda10
--- /dev/null
+++ b/apps/DeviceAdmin/res/xml/device_admin.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+
+<device-admin xmlns:android="http://schemas.android.com/apk/res/android" android:visible="false">
+    <uses-policies>
+        <wipe-data />
+    </uses-policies>
+</device-admin>
diff --git a/apps/DeviceAdmin/src/com/android/afwtest/deviceadmin/AdminReceiver.java b/apps/DeviceAdmin/src/com/android/afwtest/deviceadmin/AdminReceiver.java
new file mode 100644
index 0000000..c0215bf
--- /dev/null
+++ b/apps/DeviceAdmin/src/com/android/afwtest/deviceadmin/AdminReceiver.java
@@ -0,0 +1,25 @@
+/*
+ * 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.deviceadmin;
+
+import android.app.admin.DeviceAdminReceiver;
+
+/**
+ * Admin receiver.
+ */
+public class AdminReceiver extends DeviceAdminReceiver {
+}
diff --git a/apps/DeviceAdmin/src/com/android/afwtest/deviceadmin/FactoryResetActivity.java b/apps/DeviceAdmin/src/com/android/afwtest/deviceadmin/FactoryResetActivity.java
new file mode 100644
index 0000000..b5e56b8
--- /dev/null
+++ b/apps/DeviceAdmin/src/com/android/afwtest/deviceadmin/FactoryResetActivity.java
@@ -0,0 +1,65 @@
+/*
+ * 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.deviceadmin;
+
+import android.app.Activity;
+import android.app.admin.DevicePolicyManager;
+import android.content.ComponentName;
+import android.content.Context;
+import android.os.Bundle;
+import android.util.Log;
+
+
+/**
+ * Activity to handle factory reset intent.
+ */
+public class FactoryResetActivity extends Activity {
+
+    private static final String TAG = "afwtest.FactoryResetActivity";
+
+    private static final String EXTRA_WIPE_PROTECTION_DATA = "afwtest.wipe.protection.data";
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+
+        ComponentName admin = new ComponentName(getApplicationContext().getPackageName(),
+                AdminReceiver.class.getName());
+
+        DevicePolicyManager devicePolicyManager =
+                (DevicePolicyManager) getSystemService(Context.DEVICE_POLICY_SERVICE);
+
+        boolean wipeReset = getIntent().getBooleanExtra(EXTRA_WIPE_PROTECTION_DATA, false);
+
+        String adminName = admin.flattenToString();
+        if (!devicePolicyManager.isAdminActive(admin)) {
+            Log.e(TAG, adminName + " is not active admin");
+            Log.e(TAG, "Please run command: adb shell dpm set-active-admin " + adminName);
+        } else if (wipeReset && !devicePolicyManager.isDeviceOwnerApp(getPackageName())) {
+            Log.e(TAG, getPackageName() + " is not device-owner");
+            Log.e(TAG, "Please run command: adb shell dpm set-device-owner " + adminName);
+        } else {
+            Log.d(TAG, "Active admin: " + adminName);
+            if (wipeReset) {
+                devicePolicyManager.wipeData(DevicePolicyManager.WIPE_RESET_PROTECTION_DATA);
+            } else {
+                devicePolicyManager.wipeData(0);
+            }
+        }
+        finish();
+    }
+}
diff --git a/apps/SystemUtil/Android.mk b/apps/SystemUtil/Android.mk
new file mode 100644
index 0000000..6c14d18
--- /dev/null
+++ b/apps/SystemUtil/Android.mk
@@ -0,0 +1,33 @@
+#
+# 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.
+#
+
+LOCAL_PATH:= $(call my-dir)
+
+include $(CLEAR_VARS)
+
+LOCAL_MODULE_TAGS := optional
+
+LOCAL_MODULE_PATH := $(TARGET_OUT_DATA_APPS)
+
+LOCAL_STATIC_JAVA_LIBRARIES := AfwThCommonLib
+
+LOCAL_SRC_FILES := $(call all-java-files-under, src)
+
+LOCAL_PACKAGE_NAME := AfwThSystemUtil
+
+LOCAL_SDK_VERSION := 22
+
+include $(BUILD_AFW_TEST_SUPPORT_PACKAGE)
diff --git a/apps/SystemUtil/AndroidManifest.xml b/apps/SystemUtil/AndroidManifest.xml
new file mode 100644
index 0000000..e770ea4
--- /dev/null
+++ b/apps/SystemUtil/AndroidManifest.xml
@@ -0,0 +1,65 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+          package="com.android.afwtest.systemutil" >
+
+    <uses-sdk android:minSdkVersion="22" android:targetSdkVersion="23"/>
+
+    <uses-permission android:name="android.permission.CHANGE_CONFIGURATION"/>
+    <uses-permission android:name="android.permission.DISPATCH_NFC_MESSAGE" />
+    <uses-permission android:name="android.permission.DISPATCH_PROVISIONING_MESSAGE"/>
+    <uses-permission android:name="android.permission.GET_ACCOUNTS" />
+    <uses-permission android:name="android.permission.MANAGE_ACCOUNTS" />
+
+    <instrumentation
+            android:name=".RemoveGoogleAccount"
+            android:label="Remove Google Account"
+            android:targetPackage="com.android.afwtest.systemutil" >
+    </instrumentation>
+
+    <instrumentation
+            android:name=".ChangeLocale"
+            android:label="Change the device locale"
+            android:targetPackage="com.android.afwtest.systemutil" >
+    </instrumentation>
+
+    <application android:label="Afw Test Harness System Util">
+        <!-- NFC Bump Sender activity -->
+        <activity
+                android:name="com.android.afwtest.systemutil.NfcBumpSender"
+                android:label="NFC Bump Sender"
+                android:exported="true">
+            <intent-filter>
+                <action android:name="com.android.afwtest.systemutil.action.SEND_NFC_BUMP" />
+                <category android:name="android.intent.category.DEFAULT" />
+            </intent-filter>
+        </activity>
+
+        <!-- Start QR Code provisioning activity -->
+        <activity
+                android:name="com.android.afwtest.systemutil.StartQRCodeProvisioning"
+                android:label="QR code provisioning"
+                android:exported="true">
+            <intent-filter>
+                <action android:name="com.android.afwtest.systemutil.action.START_QR_CODE_PROVISIONING" />
+                <category android:name="android.intent.category.DEFAULT" />
+            </intent-filter>
+        </activity>
+
+    </application>
+</manifest>
diff --git a/apps/SystemUtil/README b/apps/SystemUtil/README
new file mode 100644
index 0000000..fc4e3a0
--- /dev/null
+++ b/apps/SystemUtil/README
@@ -0,0 +1,34 @@
+- About
+
+  System util app. It must be pushed to /system/priv-app on the device.
+  Requires a device where 'adb root' is possible, typically a userdebug build.
+
+- Build & Install:
+
+  make AfwThSystemUtil
+  adb root
+  adb remount
+  adb push AfwThSystemUtil.apk /system/priv-app
+  adb reboot
+
+- Usage
+
+  - Remove all Google accounts:
+    adb shell am instrument -w com.android.afwtest.systemutil/.RemoveGoogleAccount
+
+  - Remove a Google account:
+    adb shell am instrument \
+       -w -e account accountName com.android.afwtest.systemutil/.RemoveGoogleAccount
+
+  - Change system locale
+     adb shell am instrument \
+       -w -e locale locale_code com.android.afwtest.systemutil/.ChangeLocale
+
+  - locale_code can be language code only or languageCode_COUNTRYCODE, e.g. en, en_US, en_UK, zh_CN.
+
+  - Simulate a NFC bump
+    Configure the parameters you would like to be sent in the nfc bump by editing afw-test.props
+
+      adb push afw-test.props /data/local/tmp/afw-test.props
+      adb shell am start -n com.android.afwtest.systemutil/.NfcBumpSender
+                         -e NFCBumpFile "/data/local/tmp/afw-test.props"
diff --git a/apps/SystemUtil/afw-test-system-util-permissions.xml b/apps/SystemUtil/afw-test-system-util-permissions.xml
new file mode 100644
index 0000000..48810f7
--- /dev/null
+++ b/apps/SystemUtil/afw-test-system-util-permissions.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2017 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<permissions>
+  <privapp-permissions package="com.android.afwtest.systemutil">
+    <permission name="android.permission.CHANGE_CONFIGURATION"/>
+    <permission name="android.permission.DISPATCH_NFC_MESSAGE"/>
+    <permission name="android.permission.DISPATCH_PROVISIONING_MESSAGE"/>
+  </privapp-permissions>
+</permissions>
diff --git a/apps/SystemUtil/src/com/android/afwtest/systemutil/ChangeLocale.java b/apps/SystemUtil/src/com/android/afwtest/systemutil/ChangeLocale.java
new file mode 100644
index 0000000..34c9da4
--- /dev/null
+++ b/apps/SystemUtil/src/com/android/afwtest/systemutil/ChangeLocale.java
@@ -0,0 +1,113 @@
+/*
+ * 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.systemutil;
+
+import android.app.Activity;
+import android.app.Instrumentation;
+import android.content.res.Configuration;
+import android.os.Bundle;
+import android.util.Log;
+
+import java.lang.reflect.Field;
+import java.lang.reflect.Method;
+import java.util.Locale;
+
+/**
+ * Change the system locale.
+ **/
+public class ChangeLocale extends Instrumentation {
+    private static final String TAG = "afwtest.ChangeLocale";
+
+    // Locale code, expecting something like: en, en_US, en_UK, zh, zh_CN, zh_TW.
+    private String mLocale;
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void onCreate(Bundle arguments) {
+        super.onCreate(arguments);
+        mLocale = arguments.getString("locale", "");
+        start();
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void onStart() {
+        super.onStart();
+
+        // Locale must be specified.
+        if (mLocale.isEmpty()) {
+            handleError("Locale is not specified");
+            return;
+        }
+
+        Log.i(TAG, String.format("Attempting to change locale to %s", mLocale));
+
+        try {
+            // It could be en_US or en.
+            final String[] codes = mLocale.split("_");
+            final Locale locale =
+                    codes.length > 1 ? new Locale(codes[0], codes[1]) : new Locale(codes[0]);
+
+            // Change locale through reflection.
+            final Class amClass = Class.forName("android.app.ActivityManager");
+            final Method getService = amClass.getMethod("getService");
+            getService.setAccessible(true);
+            final Object am = getService.invoke(amClass);
+            final Class iamClass = am.getClass();
+
+            final Method getConfig = iamClass.getMethod("getConfiguration");
+            getConfig.setAccessible(true);
+            final Configuration config = (Configuration) getConfig.invoke(am);
+
+            final Field field = config.getClass().getField("userSetLocale");
+            field.setBoolean(config, true);
+
+            // Set new locale.
+            config.locale = locale;
+
+            // Update configuration.
+            final Method updateConfig =
+                    iamClass.getMethod("updateConfiguration", Configuration.class);
+            updateConfig.setAccessible(true);
+            updateConfig.invoke(am, config);
+
+            // Success
+            final Bundle results = new Bundle();
+            results.putString("result", "SUCCESS");
+            finish(Activity.RESULT_OK, results);
+        } catch (Exception e) {
+            Log.e(TAG, "Failed to change locale", e);
+            handleError(String.format("Failed to change locale: %s", e));
+        }
+    }
+
+    /**
+     * Handles error by exiting instrumentation.
+     *
+     * @param errorMsg error msg
+     */
+    private void handleError(String errorMsg) {
+        Bundle results = new Bundle();
+        results.putString("error", errorMsg);
+        finish(Activity.RESULT_CANCELED, results);
+    }
+
+}
diff --git a/apps/SystemUtil/src/com/android/afwtest/systemutil/NfcBumpSender.java b/apps/SystemUtil/src/com/android/afwtest/systemutil/NfcBumpSender.java
new file mode 100644
index 0000000..5cda272
--- /dev/null
+++ b/apps/SystemUtil/src/com/android/afwtest/systemutil/NfcBumpSender.java
@@ -0,0 +1,61 @@
+/*
+ * 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.systemutil;
+
+import static com.android.afwtest.common.Constants.NFC_BUMP_FILE;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.os.Bundle;
+import android.util.Log;
+
+import com.android.afwtest.common.nfcprovisioning.NfcBumpSimulator;
+
+import java.io.IOException;
+
+/**
+ * Activity to send a nfc bump.
+ */
+public class NfcBumpSender extends Activity {
+
+    private static final String TAG = "afwtest.NfcBumpSender";
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+
+        try {
+            Intent intent = getIntent();
+            // Gets the path of the file that containing the parameters to be sent
+            // in the nfc bump.
+            if (intent.hasExtra(NFC_BUMP_FILE)) {
+                String bumpFileLocation = getIntent().getStringExtra(NFC_BUMP_FILE);
+                NfcBumpSimulator.sendNfcBump(this, bumpFileLocation);
+                Log.d(TAG, String.format("%s=%s", NFC_BUMP_FILE, bumpFileLocation));
+            } else {
+                Log.e(TAG, String.format("Nfc bump not sent."
+                        + "Intent doesn't contain extra %s.", NFC_BUMP_FILE));
+            }
+        } catch (IOException e) {
+            Log.e(TAG, "Failed to send Nfc Bump", e);
+        }
+        finish();
+    }
+}
diff --git a/apps/SystemUtil/src/com/android/afwtest/systemutil/RemoveGoogleAccount.java b/apps/SystemUtil/src/com/android/afwtest/systemutil/RemoveGoogleAccount.java
new file mode 100644
index 0000000..dc57c1d
--- /dev/null
+++ b/apps/SystemUtil/src/com/android/afwtest/systemutil/RemoveGoogleAccount.java
@@ -0,0 +1,151 @@
+/*
+ * 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.systemutil;
+
+import android.accounts.Account;
+import android.accounts.AccountManager;
+import android.accounts.AccountManagerCallback;
+import android.accounts.AccountManagerFuture;
+import android.accounts.AuthenticatorException;
+import android.accounts.OperationCanceledException;
+import android.app.Activity;
+import android.app.Instrumentation;
+import android.os.Bundle;
+import android.util.Log;
+
+import java.io.IOException;
+import java.util.concurrent.Semaphore;
+
+/**
+ * An instrumentation utility to remove Google account from device.
+ */
+public class RemoveGoogleAccount extends Instrumentation {
+
+    private static final String TAG = "afwtest.RemoveGoogleAccount";
+
+    // Account to remove; remove all Google accounts if null.
+    private String mAccount;
+
+    @Override
+    public void onCreate(Bundle arguments) {
+        super.onCreate(arguments);
+        mAccount = arguments.getString("account", null);
+        start();
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void onStart() {
+        super.onStart();
+
+        Log.d(TAG, "Attempting to remove Google accounts");
+        Bundle results = new Bundle();
+
+        AccountManager accountManager = AccountManager.get(getContext());
+        Account[] accounts = accountManager.getAccountsByType("com.google");
+        for (Account account : accounts) {
+            if (mAccount == null || account.name.equals(mAccount)) {
+                Log.i(TAG, String.format("Removing account %s", account.name));
+                RemoveCallback callback = new RemoveCallback();
+                accountManager.removeAccount(account, callback, null /* handler */);
+                if (!callback.waitForRemoveCompletion()) {
+                    String error = String.format("Failed to remove account %s: Reason: %s",
+                            account.name, callback.getErrorMessage());
+                    results.putString("error", error);
+                    finish(Activity.RESULT_CANCELED, results);
+                    return;
+                }
+            }
+        }
+        results.putString("result", "SUCCESS");
+        finish(Activity.RESULT_OK, results);
+    }
+
+    /**
+     * Callback which will block until account removal result is available or exception throws.
+     */
+    private static class RemoveCallback implements AccountManagerCallback<Boolean> {
+
+        private static final String TAG = "afwtest.RemoveCallback";
+
+        /**
+         * Stores the result of account removal.
+         */
+        private boolean mResult = false;
+
+        /**
+         * Error message if any.
+         */
+        private String mErrorMessage = null;
+
+        /**
+         * {@link Semaphore}, indicating result is available if it's value > 0.
+         */
+        private Semaphore mLock = new Semaphore(0);
+
+        /**
+         * Block and wait for the remove callback to complete.
+         *
+         * @return the {@link Bundle} result from the remove op.
+         */
+
+        public Boolean waitForRemoveCompletion() {
+            try {
+                // Blocks until semaphore count > 0
+                mLock.acquire();
+            } catch (InterruptedException e) {
+                handleException(e);
+            }
+            return mResult;
+        }
+
+        /**
+         * {@inheritDoc}
+         */
+        @Override
+        public void run(AccountManagerFuture<Boolean> future) {
+            try {
+                mResult = future.getResult();
+            } catch (OperationCanceledException | IOException | AuthenticatorException e) {
+                handleException(e);
+            } finally {
+                // Increment semaphore count by 1.
+                mLock.release();
+            }
+        }
+
+        /**
+         * Gets the error message.
+         *
+         * @return error message
+         */
+        public String getErrorMessage() {
+            return mErrorMessage;
+        }
+
+        /**
+         * Creates a result bundle for given exception
+         */
+        private void handleException(Exception e) {
+            Log.e(TAG, "Failed to remove account", e);
+            mResult = false;
+            mErrorMessage = e.toString();
+        }
+    }
+}
diff --git a/apps/SystemUtil/src/com/android/afwtest/systemutil/StartQRCodeProvisioning.java b/apps/SystemUtil/src/com/android/afwtest/systemutil/StartQRCodeProvisioning.java
new file mode 100644
index 0000000..e7f04aa
--- /dev/null
+++ b/apps/SystemUtil/src/com/android/afwtest/systemutil/StartQRCodeProvisioning.java
@@ -0,0 +1,58 @@
+/*
+ * 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.systemutil;
+
+import static com.android.afwtest.common.Constants.QR_CODE_FILE;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.os.Bundle;
+import android.util.Log;
+
+import com.android.afwtest.common.qrcodeprovisioning.QRCodeSimulator;
+
+import java.io.IOException;
+
+/**
+ * Activity to start QR code provisioning.
+ */
+public class StartQRCodeProvisioning extends Activity {
+
+    private static final String TAG = "afwtest.StartQRCodeProvisioning";
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+
+        try {
+            Intent intent = getIntent();
+            if (intent.hasExtra(QR_CODE_FILE)) {
+                QRCodeSimulator.sendQRCode(this, intent.getStringExtra(QR_CODE_FILE));
+            } else {
+                Log.e(TAG, String.format(
+                        "QR code provisioning not started. Intent doesn't contain extra %s.",
+                        QR_CODE_FILE));
+            }
+        } catch (IOException e) {
+            Log.e(TAG, "Failed to start QR code provisioning", e);
+        }
+        finish();
+    }
+}
diff --git a/apps/Util/Android.mk b/apps/Util/Android.mk
new file mode 100644
index 0000000..89742c4
--- /dev/null
+++ b/apps/Util/Android.mk
@@ -0,0 +1,33 @@
+#
+# 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.
+#
+
+LOCAL_PATH:= $(call my-dir)
+
+include $(CLEAR_VARS)
+
+LOCAL_MODULE_TAGS := optional
+
+LOCAL_MODULE_PATH := $(TARGET_OUT_DATA_APPS)
+
+LOCAL_STATIC_JAVA_LIBRARIES := AfwThCommonLib
+
+LOCAL_SRC_FILES := $(call all-java-files-under, src)
+
+LOCAL_PACKAGE_NAME := AfwThUtil
+
+LOCAL_SDK_VERSION := 22
+
+include $(BUILD_AFW_TEST_SUPPORT_PACKAGE)
diff --git a/apps/Util/AndroidManifest.xml b/apps/Util/AndroidManifest.xml
new file mode 100644
index 0000000..f270fcc
--- /dev/null
+++ b/apps/Util/AndroidManifest.xml
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+          package="com.android.afwtest.util" >
+
+    <uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
+    <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
+    <uses-permission android:name="android.permission.INTERNET" />
+    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
+
+    <uses-sdk android:minSdkVersion="22" android:targetSdkVersion="22" />
+
+    <instrumentation android:name=".Wifi"
+                     android:targetPackage="com.android.afwtest.util"
+                     android:label="Afw Test Harness Utility App">
+    </instrumentation>
+
+    <application android:label="Afw Test Harness Util">
+    </application>
+</manifest>
diff --git a/apps/Util/README.txt b/apps/Util/README.txt
new file mode 100644
index 0000000..8d17880
--- /dev/null
+++ b/apps/Util/README.txt
@@ -0,0 +1,17 @@
+- About
+
+  Common util app that works on user builds.
+
+- Usage: connect a wifi
+
+  adb shell am instrument -w -e action connect \
+      -e ssid <ssid> -e security_type <security_type> -e password <password> \
+      com.android.afwtest.util/.Wifi
+
+  security_type could be WPA, WEP or NONE. Default to be NONE if no password is specified; default
+  to be WPA if password is given.
+
+- Usage: disconnect from wifi
+
+  adb shell am instrument -w -e action disconnect com.android.afwtest.util/.Wifi
+
diff --git a/apps/Util/src/com/android/afwtest/util/NetworkMonitor.java b/apps/Util/src/com/android/afwtest/util/NetworkMonitor.java
new file mode 100644
index 0000000..b432549
--- /dev/null
+++ b/apps/Util/src/com/android/afwtest/util/NetworkMonitor.java
@@ -0,0 +1,98 @@
+/*
+ * 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.util;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.net.ConnectivityManager;
+import android.util.Log;
+
+import com.android.afwtest.common.NetworkUtils;
+
+/**
+ * Monitor the state of the data network. Invoke a callback when the network is connected
+ */
+public class NetworkMonitor {
+    private static final String TAG = "afwtest.NetworkMonitor";
+
+    /** State notification callback. Expect some duplicate notifications. */
+    public interface Callback {
+        /** Notify on network connected. */
+        void onNetworkConnected();
+        /** Notify on network disconnected. */
+        void onNetworkDisconnected();
+    }
+
+    /** Application context. */
+    private final Context mContext;
+
+    /** Registered callback that is listening to network events. */
+    private final Callback mCallback;
+
+    /** Whether receiver is registered. */
+    private boolean mReceiverRegistered;
+
+    /**
+     * Start watching the network and monitoring the checkin service. Immediately invokes one of the
+     * callback methods to report the current state, and then invokes callback methods over time as
+     * the state changes.
+     *
+     * @param context to use for intent observers and such
+     * @param callback to invoke when the network status changes
+     */
+    public NetworkMonitor(Context context, Callback callback) {
+        mContext = context;
+        mCallback = callback;
+
+        IntentFilter filter = new IntentFilter();
+        filter.addAction(ConnectivityManager.CONNECTIVITY_ACTION);
+
+        context.registerReceiver(mBroadcastReceiver, filter);
+
+        mReceiverRegistered = true;
+    }
+
+    /**
+     * Stop watching the network and checkin service.
+     */
+    public synchronized void close() {
+        if (mReceiverRegistered) {
+            mContext.unregisterReceiver(mBroadcastReceiver);
+            mReceiverRegistered = false;
+        }
+    }
+
+    /**
+     * Broadcast receiver.
+     */
+    public final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            Log.i(TAG, String.format("onReceive: %s", intent.toString()));
+
+            if (intent.getAction().equals(ConnectivityManager.CONNECTIVITY_ACTION)) {
+                if (NetworkUtils.isConnectedToWifi(context)) {
+                    mCallback.onNetworkConnected();
+                } else {
+                    mCallback.onNetworkDisconnected();
+                }
+            }
+        }
+    };
+}
diff --git a/apps/Util/src/com/android/afwtest/util/Wifi.java b/apps/Util/src/com/android/afwtest/util/Wifi.java
new file mode 100644
index 0000000..e55ab40
--- /dev/null
+++ b/apps/Util/src/com/android/afwtest/util/Wifi.java
@@ -0,0 +1,133 @@
+/*
+ * 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.util;
+
+import android.app.Activity;
+import android.app.Instrumentation;
+import android.os.Bundle;
+import android.util.Log;
+
+import com.android.afwtest.common.NetworkUtils;
+
+/**
+ * Adds a wifi network to system.
+ */
+public class Wifi extends Instrumentation {
+
+    private static final String TAG = "afwtest.Wifi";
+
+    private static final String ACTION_CONNECT = "connect";
+    private static final String ACTION_DISCONNECT = "disconnect";
+
+    /** Supported actions: connect, disconnect*/
+    private String mAction;
+
+    private String mSSID, mSecurityType, mPassword;
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void onCreate(Bundle arguments) {
+        super.onCreate(arguments);
+        mAction = arguments.getString("action", "");
+        mSSID = arguments.getString("ssid", "");
+        if (!mSSID.startsWith("\"") || !mSSID.endsWith("\"")) {
+            mSSID = String.format("\"%s\"", mSSID);
+        }
+        mSecurityType = arguments.getString("security_type", "");
+        mPassword = arguments.getString("password", "");
+        start();
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void onStart() {
+        // Handle connect action
+        if (ACTION_CONNECT.equals(mAction)) {
+            handleConnect();
+        } else if (ACTION_DISCONNECT.equals(mAction)) {
+            handleDisconnect();
+        } else {
+            handleError(String.format("Invalid action: %s", mAction));
+        }
+    }
+
+    /**
+     * Handles connecting wifi request.
+     */
+    private void handleConnect() {
+        if (!NetworkUtils.enableWifi(getContext())) {
+            handleError("Failed to enable wifi.");
+        }
+
+        // If the expected wifi is already connected, return.
+        if (NetworkUtils.isConnectedToSpecifiedWifi(getContext(), mSSID)) {
+            Log.i(TAG, String.format("Wifi already connected: %s (%s)", mSSID, mSecurityType));
+            handleSuccess();
+        } else {
+            WifiConnector wifiConnector = new WifiConnector(getContext(),
+                    mSSID,
+                    mSecurityType,
+                    mPassword);
+            if (wifiConnector.connect()) {
+                Log.i(TAG, String.format("Connected to wifi: %s (%s)", mSSID, mSecurityType));
+                handleSuccess();
+            } else {
+                handleError(String.
+                        format("Failed to connect to wifi: %s (%s)", mSSID, mSecurityType));
+            }
+        }
+    }
+
+    /**
+     * Handles wifi disconnecting request.
+     *
+     * Handled by removing all configured networks.
+     */
+    private void handleDisconnect() {
+        if (!NetworkUtils.disconnectFromWifi(getContext())) {
+            handleError("Failed to disconnect from Wifi");
+        } else {
+            Log.i(TAG, "Wifi disconnected.");
+            handleSuccess();
+        }
+    }
+
+    /**
+     * Handles error by exiting instrumentation.
+     *
+     * @param errorMsg error msg
+     */
+    private void handleError(String errorMsg) {
+        Log.i(TAG, errorMsg);
+        Bundle results = new Bundle();
+        results.putString("error", errorMsg);
+        finish(Activity.RESULT_CANCELED, results);
+    }
+
+    /**
+     * Handles success execution
+     */
+    private void handleSuccess() {
+        Bundle results = new Bundle();
+        results.putString("result", "SUCCESS");
+        finish(Activity.RESULT_OK, results);
+    }
+}
\ No newline at end of file
diff --git a/apps/Util/src/com/android/afwtest/util/WifiConfig.java b/apps/Util/src/com/android/afwtest/util/WifiConfig.java
new file mode 100644
index 0000000..f49d710
--- /dev/null
+++ b/apps/Util/src/com/android/afwtest/util/WifiConfig.java
@@ -0,0 +1,145 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.afwtest.util;
+
+import android.net.wifi.WifiConfiguration;
+import android.net.wifi.WifiManager;
+import android.text.TextUtils;
+import android.util.Log;
+
+import java.util.Locale;
+
+/**
+ * Utility class for configuring a new WiFi network.
+ */
+public final class WifiConfig {
+
+    private static final String TAG = "afwtest.WifiConfig";
+
+    /** Security type enum. */
+    enum SecurityType {
+        NONE,
+        WPA,
+        WEP
+    }
+
+    /** {@link WifiManager} instance. */
+    private final WifiManager mWifiManager;
+
+    /**
+     * Constructor.
+     *
+     * @param manager {@link WifiManager} object
+     */
+    public WifiConfig(WifiManager manager) {
+        mWifiManager = manager;
+    }
+
+    /**
+     * Adds a new WiFi network.
+     *
+     * @param ssid Wifi SSID
+     * @param type Wifi security type
+     * @param password Wifi password
+     * @return the ID of the newly created network description. Returns -1 on failure.
+     */
+    public int addNetwork(String ssid, String type, String password) {
+        WifiConfiguration wifiConf = new WifiConfiguration();
+        SecurityType securityType;
+        if (type == null || TextUtils.isEmpty(type)) {
+            securityType = SecurityType.NONE;
+        } else {
+            try {
+                securityType = Enum.valueOf(SecurityType.class, type.toUpperCase(Locale.US));
+            } catch (IllegalArgumentException e) {
+                Log.e(TAG, String.format("Invalid Wifi security type: %s", type));
+                return -1;
+            }
+        }
+        // If we have a password, and no security type, assume WPA.
+        if (securityType.equals(SecurityType.NONE) && !TextUtils.isEmpty(password)) {
+            securityType = SecurityType.WPA;
+        }
+
+        wifiConf.SSID = ssid;
+        wifiConf.status = WifiConfiguration.Status.ENABLED;
+        switch (securityType) {
+            case NONE:
+                wifiConf.allowedKeyManagement.set(WifiConfiguration.KeyMgmt.NONE);
+                wifiConf.allowedAuthAlgorithms.set(WifiConfiguration.AuthAlgorithm.OPEN);
+                break;
+            case WPA:
+                updateForWPAConfiguration(wifiConf, password);
+                break;
+            case WEP:
+                updateForWEPConfiguration(wifiConf, password);
+                break;
+        }
+
+        int netId = mWifiManager.addNetwork(wifiConf);
+
+        if (netId != -1) {
+            // Setting disableOthers to 'true' should trigger a connection attempt.
+            mWifiManager.enableNetwork(netId, true);
+            mWifiManager.saveConfiguration();
+        }
+
+        return netId;
+    }
+
+    /**
+     * Updates WAP Wifi configuration.
+     *
+     * @param wifiConf {@link WifiConfiguration} object to update
+     * @param password Wifi password
+     */
+    protected void updateForWPAConfiguration(WifiConfiguration wifiConf, String password) {
+        wifiConf.allowedKeyManagement.set(WifiConfiguration.KeyMgmt.WPA_PSK);
+        wifiConf.allowedAuthAlgorithms.set(WifiConfiguration.AuthAlgorithm.OPEN);
+        wifiConf.allowedProtocols.set(WifiConfiguration.Protocol.WPA); // For WPA
+        wifiConf.allowedProtocols.set(WifiConfiguration.Protocol.RSN); // For WPA2
+        wifiConf.allowedPairwiseCiphers.set(WifiConfiguration.PairwiseCipher.TKIP);
+        wifiConf.allowedPairwiseCiphers.set(WifiConfiguration.PairwiseCipher.CCMP);
+        wifiConf.allowedGroupCiphers.set(WifiConfiguration.GroupCipher.TKIP);
+        wifiConf.allowedGroupCiphers.set(WifiConfiguration.GroupCipher.CCMP);
+        if (!TextUtils.isEmpty(password)) {
+            wifiConf.preSharedKey = "\"" + password + "\"";
+        }
+    }
+
+    /**
+     * Updates WEP Wifi configuration.
+     *
+     * @param wifiConf {@link WifiConfiguration} object to update
+     * @param password Wifi password
+     */
+    protected void updateForWEPConfiguration(WifiConfiguration wifiConf, String password) {
+        wifiConf.allowedKeyManagement.set(WifiConfiguration.KeyMgmt.NONE);
+        wifiConf.allowedAuthAlgorithms.set(WifiConfiguration.AuthAlgorithm.OPEN);
+        wifiConf.allowedAuthAlgorithms.set(WifiConfiguration.AuthAlgorithm.SHARED);
+        wifiConf.allowedGroupCiphers.set(WifiConfiguration.GroupCipher.WEP40);
+        wifiConf.allowedGroupCiphers.set(WifiConfiguration.GroupCipher.WEP104);
+        wifiConf.allowedGroupCiphers.set(WifiConfiguration.GroupCipher.TKIP);
+        wifiConf.allowedGroupCiphers.set(WifiConfiguration.GroupCipher.CCMP);
+        int length = password.length();
+        if ((length == 10 || length == 26 || length == 58) && password.matches("[0-9A-Fa-f]*")) {
+            wifiConf.wepKeys[0] = password;
+        } else {
+            wifiConf.wepKeys[0] = '"' + password + '"';
+        }
+        wifiConf.wepTxKeyIndex = 0;
+    }
+}
\ No newline at end of file
diff --git a/apps/Util/src/com/android/afwtest/util/WifiConnector.java b/apps/Util/src/com/android/afwtest/util/WifiConnector.java
new file mode 100644
index 0000000..f10ab2d
--- /dev/null
+++ b/apps/Util/src/com/android/afwtest/util/WifiConnector.java
@@ -0,0 +1,145 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.afwtest.util;
+
+import android.content.Context;
+import android.net.wifi.WifiManager;
+import android.os.SystemClock;
+import android.util.Log;
+
+import com.android.afwtest.common.NetworkUtils;
+
+import java.util.concurrent.Semaphore;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Utility class responsible for connecting wifi.
+ */
+public final class WifiConnector implements NetworkMonitor.Callback {
+
+    private static final String TAG = "afwtest.WifiConnector";
+
+    /** Connection retry base duration. */
+    private static final int RETRY_SLEEP_DURATION_BASE_MS = 500;
+    /** Connection retry duration multiplier. */
+    private static final int RETRY_SLEEP_MULTIPLIER = 2;
+    /** Max connection attempts. */
+    private static final int MAX_ATTEMPS = 6;
+    /** Wifi reconnection timeout. */
+    private static final int RECONNECT_TIMEOUT_MS = (int)TimeUnit.MINUTES.toMillis(1);
+
+    private final Context mContext;
+    private final String mSSID, mSecurityType, mPassword;
+    private final WifiManager mWifiManager;
+    private final WifiConfig mWifiConfig;
+
+    /**
+     * {@link Semaphore}, indicating result is available if it's value > 0.
+     */
+    private Semaphore mLock = new Semaphore(0);
+
+    /**
+     * Constructor.
+     *
+     * @param context {@link Context} object
+     * @param ssid Wifi SSID
+     * @param securityType Wifi security type
+     * @param password Wifi password
+     */
+    public WifiConnector(Context context, String ssid, String securityType, String password) {
+        mContext = context;
+        mSSID = ssid;
+        mSecurityType = securityType;
+        mPassword = password;
+        mWifiManager  = (WifiManager) context.getSystemService(Context.WIFI_SERVICE);
+        mWifiConfig = new WifiConfig(mWifiManager);
+    }
+
+    /**
+     * Connects to WIFI.
+     *
+     * @return {@code true} if the given WIFI is connected; {@code false} othwerise
+     */
+    public boolean connect() {
+        if (!NetworkUtils.enableWifi(mContext)) {
+            Log.e(TAG, "Failed to enable WIFI.");
+            return false;
+        }
+
+        NetworkMonitor networkMonitor = new NetworkMonitor(mContext, this);
+
+        try {
+            int netId = -1;
+
+            int nextSleepTimeMs = RETRY_SLEEP_DURATION_BASE_MS;
+            int attempts = MAX_ATTEMPS;
+            while (attempts > 0) {
+                if (netId == -1) {
+                    netId = mWifiConfig.addNetwork(mSSID, mSecurityType, mPassword);
+                }
+
+                if (netId == -1) {
+                    Log.e(TAG, "Failed to save network.");
+                } else if (!mWifiManager.reconnect()) {
+                    Log.e(TAG, "Unable to connect to wifi");
+                } else {
+                    // Connected
+                    break;
+                }
+
+                --attempts;
+                // Sleep before next attempt
+                Log.e(TAG, String.format("Retrying in %s ms.", nextSleepTimeMs));
+                SystemClock.sleep(nextSleepTimeMs);
+                // Increase the next sleep time.
+                nextSleepTimeMs *= RETRY_SLEEP_MULTIPLIER;
+            }
+
+            // Failed to add network or reconnect.
+            if (netId == -1 || attempts <= 0) {
+                return false;
+            }
+
+            // Wait for connection event
+            mLock.tryAcquire(RECONNECT_TIMEOUT_MS, TimeUnit.MILLISECONDS);
+            return true;
+        } catch (InterruptedException e) {
+            Log.e(TAG, "Failed to connect to wifi", e);
+        } finally {
+            networkMonitor.close();
+        }
+
+        return false;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void onNetworkConnected() {
+        if (NetworkUtils.isConnectedToSpecifiedWifi(mContext, mSSID)) {
+            // Let's the waiter know it's connected.
+            mLock.release();
+        }
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void onNetworkDisconnected() {
+    }
+}
\ No newline at end of file
diff --git a/build/config.mk b/build/config.mk
new file mode 100644
index 0000000..3260950
--- /dev/null
+++ b/build/config.mk
@@ -0,0 +1,27 @@
+#
+# Copyright (C) 2016 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+AFW_TH_TESTCASES_OUT := $(HOST_OUT)/afw-th/android-cts/testcases
+
+# default module config filename
+AFW_TEST_MODULE_TEST_CONFIG := AndroidTest.xml
+
+# customized test package build rule
+BUILD_AFW_TEST_PACKAGE := test/AfwTestHarness/build/test_package.mk
+BUILD_AFW_TEST_SUPPORT_PACKAGE := test/AfwTestHarness/build/support_package.mk
+BUILD_AFW_TEST_MODULE_TEST_CONFIG := test/AfwTestHarness/build/module_test_config.mk
+BUILD_AFW_TEST_HOST_JAVA_LIBRARY := test/AfwTestHarness/build/test_host_java_library.mk
+
diff --git a/build/module_test_config.mk b/build/module_test_config.mk
new file mode 100644
index 0000000..1ef889f
--- /dev/null
+++ b/build/module_test_config.mk
@@ -0,0 +1,24 @@
+#
+# 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.
+#
+
+ifneq ($(LOCAL_AFW_TEST_MODULE_CONFIG),)
+afw_test_module_test_config := $(AFW_TH_TESTCASES_OUT)/$(LOCAL_MODULE).config
+$(afw_test_module_test_config): $(LOCAL_AFW_TEST_MODULE_CONFIG) | $(ACP)
+	$(call copy-file-to-target)
+endif
+# clear var
+LOCAL_AFW_TEST_MODULE_CONFIG :=
+
diff --git a/build/support_package.mk b/build/support_package.mk
new file mode 100644
index 0000000..666a1da
--- /dev/null
+++ b/build/support_package.mk
@@ -0,0 +1,35 @@
+#
+# 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.
+#
+
+#
+# Builds a package which is needed by a test package
+#
+# Replace "include $(BUILD_PACKAGE)" with "include $(BUILD_AFW_TEST_SUPPORT_PACKAGE)"
+#
+LOCAL_DEX_PREOPT := false
+LOCAL_PROGUARD_ENABLED := disabled
+
+include $(BUILD_PACKAGE)
+
+afw_test_support_apks :=
+$(foreach fp, $(ALL_MODULES.$(LOCAL_PACKAGE_NAME).BUILT_INSTALLED),\
+  $(eval pair := $(subst :,$(space),$(fp)))\
+  $(eval built := $(word 1,$(pair)))\
+  $(eval installed := $(AFW_TH_TESTCASES_OUT)/$(notdir $(word 2,$(pair))))\
+  $(eval $(call copy-one-file, $(built), $(installed)))\
+  $(eval afw_test_support_apks += $(installed)))
+
+$(my_register_name) : $(afw_test_support_apks)
diff --git a/build/test_host_java_library.mk b/build/test_host_java_library.mk
new file mode 100644
index 0000000..2964116
--- /dev/null
+++ b/build/test_host_java_library.mk
@@ -0,0 +1,30 @@
+# 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.
+
+#
+# Builds a host library and defines a rule to generate the associated test
+# package XML needed by CTS.
+#
+# Replace "include $(BUILD_HOST_JAVA_LIBRARY)" with "include $(BUILD_AFW_TEST_HOST_JAVA_LIBRARY)"
+#
+
+include $(BUILD_HOST_JAVA_LIBRARY)
+include $(BUILD_AFW_TEST_MODULE_TEST_CONFIG)
+
+afw_test_library_jar := $(AFW_TH_TESTCASES_OUT)/$(LOCAL_MODULE).jar
+$(afw_test_library_jar): $(LOCAL_BUILT_MODULE)
+	$(copy-file-to-target)
+
+# Have the module name depend on the cts files; so the cts files get generated when you run mm/mmm/mma/mmma.
+$(my_register_name) : $(afw_test_library_jar) $(afw_test_module_test_config)
diff --git a/build/test_package.mk b/build/test_package.mk
new file mode 100644
index 0000000..816f065
--- /dev/null
+++ b/build/test_package.mk
@@ -0,0 +1,29 @@
+#
+# 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.
+#
+#
+# Builds a package and defines a rule to generate the associated test
+# package XML needed by the tradefed.
+#
+# Replace "include $(BUILD_PACKAGE)" with "include $(BUILD_AFW_TEST_PACKAGE)"
+#
+
+LOCAL_DEX_PREOPT := false
+LOCAL_PROGUARD_ENABLED := disabled
+
+include $(BUILD_AFW_TEST_SUPPORT_PACKAGE)
+include $(BUILD_AFW_TEST_MODULE_TEST_CONFIG)
+
+$(my_register_name) : $(afw_test_module_test_config)
diff --git a/libs/Android.mk b/libs/Android.mk
new file mode 100644
index 0000000..6801acf
--- /dev/null
+++ b/libs/Android.mk
@@ -0,0 +1,18 @@
+#
+# 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.
+#
+
+include $(call all-subdir-makefiles)
+
diff --git a/libs/CommonLib/Android.mk b/libs/CommonLib/Android.mk
new file mode 100644
index 0000000..b6a2e3b
--- /dev/null
+++ b/libs/CommonLib/Android.mk
@@ -0,0 +1,29 @@
+#
+# 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.
+#
+
+LOCAL_PATH:= $(call my-dir)
+
+include $(CLEAR_VARS)
+
+LOCAL_SRC_FILES := $(call all-java-files-under, src)
+
+LOCAL_STATIC_JAVA_LIBRARIES := android-support-test
+
+LOCAL_MODULE_TAGS := optional
+
+LOCAL_MODULE := AfwThCommonLib
+
+include $(BUILD_STATIC_JAVA_LIBRARY)
diff --git a/libs/CommonLib/src/com/android/afwtest/common/AccountManagerUtils.java b/libs/CommonLib/src/com/android/afwtest/common/AccountManagerUtils.java
new file mode 100644
index 0000000..8384a6c
--- /dev/null
+++ b/libs/CommonLib/src/com/android/afwtest/common/AccountManagerUtils.java
@@ -0,0 +1,79 @@
+/*
+ * 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.common;
+
+import android.accounts.AccountManager;
+import android.accounts.AccountManagerFuture;
+import android.accounts.AuthenticatorException;
+import android.accounts.OperationCanceledException;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Bundle;
+
+import java.io.IOException;
+
+/**
+ * Help class to wrap AccountManager ralated functionalities.
+ */
+public final class AccountManagerUtils {
+
+    private static final String TAG = "afwtest.AccountManagerUtils";
+
+    private static final String GOOGLE_ACCOUNT_TYPE = "com.google";
+    private static final String EXTRA_SETUP_WIZARD = "setupWizard";
+
+    /**
+     * Private constructor to prevent instantiation.
+     */
+    private AccountManagerUtils() {
+    }
+
+    /**
+     * Starts Add Account activity by firing a proper intent.
+     *
+     * @param context {@link Context} object
+     * @param isSetupWizard {@code true} if simulating setup wizard,
+     *                      {@code false} if simulating Settings->Add Account
+     */
+    public static void startAddGoogleAccountActivity(Context context, boolean isSetupWizard)
+            throws IOException, AuthenticatorException, OperationCanceledException {
+        final AccountManager accountManager = AccountManager.get(context);
+
+        // Options for the Add Account activity.
+        Bundle options = new Bundle();
+        options.putBoolean(EXTRA_SETUP_WIZARD, isSetupWizard);
+        if (isSetupWizard) {
+            // Skip "Got another device?" page
+            options.putBoolean("suppress_device_to_device_setup", true);
+        }
+
+        AccountManagerFuture<Bundle> amf = accountManager.addAccount(
+                GOOGLE_ACCOUNT_TYPE,
+                null, /* authTokenType*/
+                null, /* requiredFeatures */
+                options,
+                null, /* Activity context, null to not start the intent automatically */
+                null, /* callback */
+                null /* handler */
+        );
+
+        // Fire the intent to start the UI.
+        Intent intent = (Intent) amf.getResult().get(AccountManager.KEY_INTENT);
+        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+        context.startActivity(intent);
+    }
+}
diff --git a/libs/CommonLib/src/com/android/afwtest/common/Constants.java b/libs/CommonLib/src/com/android/afwtest/common/Constants.java
new file mode 100644
index 0000000..580b703
--- /dev/null
+++ b/libs/CommonLib/src/com/android/afwtest/common/Constants.java
@@ -0,0 +1,214 @@
+/*
+ * 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.common;
+
+/***
+ * Common constants.
+ */
+public final class Constants {
+
+    /**
+     * Property key of device admin package name, used in the test configuration file.
+     */
+    public static final String KEY_DEVICE_ADMIN_PKG_NAME = "device_admin_pkg_name";
+
+    /**
+     * Property key of device admin receiver, used in the test configuration file.
+     */
+    public static final String KEY_DEVICE_ADMIN_RECEIVER = "device_admin_receiver";
+
+    /**
+     * Property key of device admin package location, used in the test configuration file.
+     */
+    public static final String KEY_DEVICE_ADMIN_PKG_LOCATION = "device_admin_pkg_location";
+
+    /**
+     * Property key of device admin package checksum, used in the test configuration file.
+     */
+    public static final String KEY_DEVICE_ADMIN_PKG_CHECKSUM = "device_admin_pkg_checksum";
+
+    /**
+     * Property key of device admin package signature hash, used in the test configuration file.
+     */
+    public static final String KEY_DEVICE_ADMIN_PKG_SIGNATURE_HASH
+            = "device_admin_pkg_signature_hash";
+
+    /**
+     * Property key of timeout size, used in the test configuration file.
+     */
+    public static final String KEY_TIMEOUT_SIZE = "timeout_size";
+
+    /**
+     * Property key of test timeout in minute, used in the test configuration file.
+     */
+    public static final String KEY_TEST_TIMEOUT_MIN = "test_timeout_min";
+
+    /**
+     * Property key of wifi ssid, used in the test configuration file.
+     */
+    public static final String KEY_WIFI_SSID = "wifi_ssid";
+
+    /**
+     * Property key of the wifi password, used in the test configuration file.
+     */
+    public static final String KEY_WIFI_PWD = "wifi_password";
+
+    /**
+     * Property key of the wifi security type, used in the test configuration file.
+     */
+    public static final String KEY_WIFI_SECURITY_TYPE = "wifi_security_type";
+
+    /**
+     * Property key of the username of the work account, used in the test configuration file.
+     */
+    public static final String KEY_WORK_ACCOUNT_USERNAME = "work_account_username";
+
+    /**
+     * Property key of the password of the work account, used in the test configuration file.
+     */
+    public static final String KEY_WORK_ACCOUNT_PASSWORD = "work_account_password";
+
+    /**
+     * Property key for the list of OEM customized widget Ids.
+     *
+     * <p>The properties of each OEM widget are specified by separate keys in the format of
+     * widget_id.property. "property" can be any of {@link #KEY_OEM_WIDGET_TEXT},
+     * {@link #KEY_OEM_WIDGET_DESCRIPTION}, {@link #KEY_OEM_WIDGET_RESOURCE_ID},
+     * {@link #KEY_OEM_WIDGET_PACKAGE}, {@link #KEY_OEM_WIDGET_CLASS} or
+     * {@link #KEY_OEM_WIDGET_ACTION}.</p>
+     *
+     * <p>For example, OEM can define a widget like:
+     * <ul>
+     * <li>oem_widgets=custom_widget</li>
+     * <li>custom_widget.text=Get started</li>
+     * <li>custom_widget.class=android.widget.Button</li>
+     * <li>custom_widget.action=click</li>
+     * </ul>
+     * This tells the test to find a widget with text "Get started" and with class name
+     * "android.widget.Button" to click.</p>
+     */
+    public static final String KEY_OEM_WIDGETS = "oem_widgets";
+
+    /**
+     * Property key for the text of an OEM widget.
+     */
+    public static final String KEY_OEM_WIDGET_TEXT = "text";
+
+    /**
+     * Property key for the content description of an OEM widget.
+     */
+    public static final String KEY_OEM_WIDGET_DESCRIPTION = "description";
+
+    /**
+     * Property key for the resource ID of an OEM widget.
+     */
+    public static final String KEY_OEM_WIDGET_RESOURCE_ID = "resource_id";
+
+    /**
+     * Property key for the package name of an OEM widget.
+     */
+    public static final String KEY_OEM_WIDGET_PACKAGE = "package";
+
+    /**
+     * Property key for the class name of an OEM widget.
+     */
+    public static final String KEY_OEM_WIDGET_CLASS = "class";
+
+    /**
+     * Property key for the action on an OEM widget. It can any of {@link #ACTION_CLICK},
+     * {@link #ACTION_CHECK} or {@link #ACTION_SCROLL}.
+     */
+    public static final String KEY_OEM_WIDGET_ACTION = "action";
+
+    /**
+     * Property key for the scrolling direction of an OEM widget. It can be any of {@code UP},
+     * {@code DOWN}, {@code LEFT} or {@code RIGHT}.
+     */
+    public static final String KEY_OEM_WIDGET_SCROLL_DIRECTION = "scroll_direction";
+
+    /**
+     * Property key for the app crash dialog auto close strategy: whether to auto close
+     * all non-fatal crashes.
+     */
+    public static final String KEY_MUTE_APP_CRASH_DIALOGS = "mute_app_crash_dialogs";
+
+    /**
+     * Property key for app names whose app crash dialog should be auto closed, separated by ",".
+     */
+    public static final String KEY_APP_CRASH_WHITELIST = "app_crash_whitelist";
+
+    /**
+     * Click action on a widget.
+     */
+    public static final String ACTION_CLICK = "click";
+
+    /**
+     * Scroll action on a widget.
+     */
+    public static final String ACTION_SCROLL = "scroll";
+
+    /**
+     * Check action on a widget, either a checkbox or radio button.
+     */
+    public static final String ACTION_CHECK = "check";
+
+    /**
+     * Constant string for the user to specify the path of the file
+     * that contains NFC provisioning configurations.
+     */
+    public static final String NFC_BUMP_FILE = "NFCBumpFile";
+
+    /**
+     * Package name of system util app.
+     */
+    public static final String SYSTEM_UTIL_PKG_NAME = "com.android.afwtest.systemutil";
+
+    /**
+     * Send NFC bump action.
+     */
+    public static final String ACTION_SEND_NFC_BUMP
+            = "com.android.afwtest.systemutil.action.SEND_NFC_BUMP";
+
+    /**
+     * Start QR code provisioning action.
+     */
+    public static final String ACTION_START_QR_CODE_PROVISIONING
+            = "com.android.afwtest.systemutil.action.START_QR_CODE_PROVISIONING";
+
+
+    /**
+     * Constant string for the user to specify the path of the file
+     * that contains QR code provisioning configurations.
+     */
+    public static final String QR_CODE_FILE = "QRCodeFile";
+
+    /**
+     * Property key for leaving system apps enabled during provisioning.
+     */
+    public static final String LEAVE_ALL_SYSTEM_APPS_ENABLED = "leave_all_system_apps_enabled";
+
+    /**
+     * Property key for the admin extras bundle used during provisioning.
+     */
+    public static final String ADMIN_EXTRAS_BUNDLE = "admin_extras_bundle";
+
+    /**
+     * Private constructor to prevent instantiation.
+     */
+    private Constants() {
+    }
+}
diff --git a/libs/CommonLib/src/com/android/afwtest/common/FileUtils.java b/libs/CommonLib/src/com/android/afwtest/common/FileUtils.java
new file mode 100644
index 0000000..dcf962f
--- /dev/null
+++ b/libs/CommonLib/src/com/android/afwtest/common/FileUtils.java
@@ -0,0 +1,86 @@
+/*
+ * 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.common;
+
+import android.util.Log;
+
+import java.io.Closeable;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.Properties;
+
+/**
+ * File Utils.
+ */
+public final class FileUtils {
+
+    private static final String TAG = "afwtest.FileUtils";
+
+    /**
+     * Reads {@link Properties} object from file.
+     *
+     * @param path File path
+     * @return Constructed {@link Properties} object
+     */
+    public static Properties readPropertiesFromFile(String path) throws IOException {
+        InputStream input = null;
+
+        try {
+            input = new FileInputStream(path);
+            Properties props = new Properties();
+            props.load(input);
+            return props;
+        } finally {
+            quietClose(input);
+        }
+    }
+
+    /**
+     * Writes {@link Properties} object into a file.
+     *
+     * @param props {@link Properties} object to write
+     * @param path file path
+     */
+    public static void writePropertiesToFile(Properties props, String path) throws IOException {
+        OutputStream output = null;
+
+        try {
+            output = new FileOutputStream(path);
+            props.store(output, null);
+        } finally {
+            quietClose(output);
+        }
+    }
+
+    /**
+     * Closes a closeable object quietly.
+     *
+     * @param handler The handler to close.
+     */
+    public static void quietClose(Closeable handler) {
+        if (handler != null) {
+            try {
+                handler.close();
+            } catch (IOException e) {
+                Log.e(TAG, "Closing stream failed", e);
+            }
+        }
+    }
+}
diff --git a/libs/CommonLib/src/com/android/afwtest/common/NetworkUtils.java b/libs/CommonLib/src/com/android/afwtest/common/NetworkUtils.java
new file mode 100644
index 0000000..ef7e00c
--- /dev/null
+++ b/libs/CommonLib/src/com/android/afwtest/common/NetworkUtils.java
@@ -0,0 +1,104 @@
+/*
+ * 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.common;
+
+import android.content.Context;
+import android.net.ConnectivityManager;
+import android.net.NetworkInfo;
+import android.net.wifi.WifiManager;
+import android.util.Log;
+
+/**
+ * Network utilities.
+ */
+public class NetworkUtils {
+    private static final String TAG = "afwtest.NetworkUtils";
+
+    /**
+     * Enables Wifi.
+     *
+     * @param context {@link Context} object
+     * @return {@code true} if Wifi is enabled successfully; {@code false} otherwise
+     */
+    public static boolean enableWifi(Context context) {
+        WifiManager wifiManager = (WifiManager) context.getSystemService(Context.WIFI_SERVICE);
+        return wifiManager != null
+                && (wifiManager.isWifiEnabled() || wifiManager.setWifiEnabled(true));
+    }
+
+    /**
+     * Returns whether the device is currently connected to a wifi.
+     *
+     * @param context {@link Context} object
+     * @return {@code true} if connected to Wifi; {@code false} otherwise
+     */
+    public static boolean isConnectedToWifi(Context context) {
+        NetworkInfo info = getActiveNetworkInfo(context);
+        return info != null
+                && info.isConnected()
+                && info.getType() == ConnectivityManager.TYPE_WIFI;
+    }
+
+    /**
+     * Checks if connected with expected wifi.
+     *
+     * @param context {@link Context} object
+     * @param ssid Wifi SSID
+     * @return {@code true} if the expected wifi
+     */
+    public static boolean isConnectedToSpecifiedWifi(Context context, String ssid) {
+        WifiManager wifiManager = (WifiManager) context.getSystemService(Context.WIFI_SERVICE);
+        return ssid != null
+                && wifiManager != null
+                && isConnectedToWifi(context)
+                && wifiManager.getConnectionInfo() != null
+                && ssid.equals(wifiManager.getConnectionInfo().getSSID());
+    }
+
+    /**
+     * Gets the active network.
+     *
+     * @param context {@link Context} object
+     * @return active {@link NetworkInfo}
+     */
+    private static NetworkInfo getActiveNetworkInfo(Context context) {
+        ConnectivityManager cm =
+                (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
+        if (cm != null) {
+            return cm.getActiveNetworkInfo();
+        }
+        return null;
+    }
+
+    /**
+     * Disconnects a device from the connected Wi-Fi network by removing the network configuration.
+     *
+     * @param context {@link Context} object
+     */
+    public static boolean disconnectFromWifi(Context context) {
+        WifiManager wifiManager = (WifiManager) context.getSystemService(Context.WIFI_SERVICE);
+        if (wifiManager.isWifiEnabled() && isConnectedToWifi(context)) {
+            String wifiSsid = wifiManager.getConnectionInfo().getSSID();
+            if (!wifiManager.removeNetwork(wifiManager.getConnectionInfo().getNetworkId())) {
+                Log.e(TAG, String.format("Failed to remove Wifi %s", wifiSsid));
+                return false;
+            }
+        }
+
+        return true;
+    }
+}
diff --git a/libs/CommonLib/src/com/android/afwtest/common/PkgMgrUtils.java b/libs/CommonLib/src/com/android/afwtest/common/PkgMgrUtils.java
new file mode 100644
index 0000000..fac3624
--- /dev/null
+++ b/libs/CommonLib/src/com/android/afwtest/common/PkgMgrUtils.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.common;
+
+import android.content.Context;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.util.Log;
+
+/**
+ * Utils for {@link PackageManager}.
+ */
+public final class PkgMgrUtils {
+    private static final String TAG = "afwtest.PkgMgrUtil";
+
+    /**
+     * Checks if a package is installed.
+     *
+     * @param context {@link Context} object
+     * @param pkgName name of the package to check
+     * @return {@code true} if target package is installed, {@code false} otherwise
+     */
+    public static boolean isPkgInstalled(Context context, String pkgName) {
+        try {
+            return context.getPackageManager().getPackageInfo(pkgName, 0) != null;
+        } catch (NameNotFoundException e) {
+            Log.w(TAG, "Pkg not found", e);
+        }
+
+        // Not installed
+        return false;
+    }
+}
diff --git a/libs/CommonLib/src/com/android/afwtest/common/Preconditions.java b/libs/CommonLib/src/com/android/afwtest/common/Preconditions.java
new file mode 100644
index 0000000..bcaf2fc
--- /dev/null
+++ b/libs/CommonLib/src/com/android/afwtest/common/Preconditions.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.common;
+
+import android.text.TextUtils;
+
+/**
+ * Precondition check utility.
+ */
+public final class Preconditions {
+
+    /**
+     * Private constructor to prevent instantiation.
+     */
+    private Preconditions() {
+    }
+
+    /**
+     * Ensures that a string passed is not null or empty.
+     *
+     * @param string a string
+     * @return the non-null, non-empty string that was validated
+     * @throws IllegalArgumentException if {@code String} is null or empty
+     */
+    public static String checkNotEmpty(String string) {
+        if (TextUtils.isEmpty(string)) {
+            throw new IllegalArgumentException();
+        }
+        return string;
+    }
+}
diff --git a/libs/CommonLib/src/com/android/afwtest/common/Timer.java b/libs/CommonLib/src/com/android/afwtest/common/Timer.java
new file mode 100644
index 0000000..b33fcf1
--- /dev/null
+++ b/libs/CommonLib/src/com/android/afwtest/common/Timer.java
@@ -0,0 +1,95 @@
+/*
+ * 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.common;
+
+import android.util.Log;
+
+import java.util.concurrent.TimeUnit;
+
+/**
+ * A count-down timer.
+ */
+public final class Timer {
+
+    private static final String TAG = "afwtest.Timer";
+
+    // A year in milliseconds
+    private static final long ONE_YEAR_IN_MS = TimeUnit.DAYS.toMillis(365);
+
+    // Timeout of this timer in milliseconds
+    private final long mTimeoutMs;
+
+    // Start time in milliseconds since Jan 1, 1970
+    private long mStartTime;
+
+    // Elapsed time so far, in milliseconds
+    private long mElapsedTime;
+
+    /**
+     * Constructor.
+     *
+     * @param timeout timeout in milliseconds
+     */
+    public Timer(long timeout) {
+        mTimeoutMs = timeout;
+    }
+
+    /**
+     * Starts this timer.
+     */
+    public void start() {
+        mStartTime = System.currentTimeMillis();
+        mElapsedTime = 0;
+    }
+
+    /**
+     * Checks if time is up by checking if elapsed time is greater than timeout.
+     *
+     * @return {@code true} if time is up, {@code false} otherwise
+     */
+    public boolean isTimeUp() {
+        elapse();
+        return mElapsedTime > mTimeoutMs;
+    }
+
+    /**
+     * Gets elapsed time since start().
+     *
+     * @return long, elapsed time in milliseconds.
+     */
+    public long elapsedTime(){
+        elapse();
+        return mElapsedTime;
+    }
+
+    /**
+     * Re-calculate elapsed time.
+     */
+    private void elapse() {
+        long currentTime = System.currentTimeMillis();
+        long elapsedTime = currentTime - mStartTime;
+        // The system time might change suddenly from 1970 to 21 century, e.g. after factory reset.
+        // Reset mStartTime without updating mElapsedTime.
+        if (elapsedTime > ONE_YEAR_IN_MS) {
+            // Reset time
+            mStartTime = currentTime - mElapsedTime;
+            Log.w(TAG, String.format("System time changed: delta=%d", elapsedTime));
+        } else {
+            mElapsedTime = elapsedTime;
+        }
+    }
+}
diff --git a/libs/CommonLib/src/com/android/afwtest/common/nfcprovisioning/NfcBumpSimulator.java b/libs/CommonLib/src/com/android/afwtest/common/nfcprovisioning/NfcBumpSimulator.java
new file mode 100644
index 0000000..d38a997
--- /dev/null
+++ b/libs/CommonLib/src/com/android/afwtest/common/nfcprovisioning/NfcBumpSimulator.java
@@ -0,0 +1,128 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.afwtest.common.nfcprovisioning;
+
+import static android.app.admin.DevicePolicyManager.EXTRA_PROVISIONING_DEVICE_ADMIN_PACKAGE_CHECKSUM;
+import static android.app.admin.DevicePolicyManager.EXTRA_PROVISIONING_DEVICE_ADMIN_PACKAGE_DOWNLOAD_LOCATION;
+import static android.app.admin.DevicePolicyManager.EXTRA_PROVISIONING_DEVICE_ADMIN_PACKAGE_NAME;
+import static android.app.admin.DevicePolicyManager.EXTRA_PROVISIONING_DEVICE_ADMIN_SIGNATURE_CHECKSUM;
+import static android.app.admin.DevicePolicyManager.EXTRA_PROVISIONING_SKIP_ENCRYPTION;
+import static android.app.admin.DevicePolicyManager.EXTRA_PROVISIONING_WIFI_PASSWORD;
+import static android.app.admin.DevicePolicyManager.EXTRA_PROVISIONING_WIFI_SECURITY_TYPE;
+import static android.app.admin.DevicePolicyManager.EXTRA_PROVISIONING_WIFI_SSID;
+import static android.app.admin.DevicePolicyManager.MIME_TYPE_PROVISIONING_NFC;
+import static android.nfc.NfcAdapter.ACTION_NDEF_DISCOVERED;
+import static com.android.afwtest.common.Constants.KEY_DEVICE_ADMIN_PKG_CHECKSUM;
+import static com.android.afwtest.common.Constants.KEY_DEVICE_ADMIN_PKG_SIGNATURE_HASH;
+import static com.android.afwtest.common.Preconditions.checkNotEmpty;
+
+import android.content.Context;
+import android.content.Intent;
+import android.nfc.NdefMessage;
+import android.nfc.NdefRecord;
+import android.nfc.NfcAdapter;
+
+import com.android.afwtest.common.test.TestConfig;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.util.Properties;
+
+/**
+ * Simulates Nfc Bump to start device owner provisioning.
+ */
+public final class NfcBumpSimulator {
+
+    private static final String TAG = "afwtest.NfcBumpSimulator";
+
+    /**
+     * Sends NFC bump to initiate device owner provisioning.
+     *
+     * @param context {@link Context} object
+     * @param propsConfigFile {@link Properties} configuration file containing NFC bump properties.
+     * @return Device admin package name after provisioning completes successfully
+     */
+    public static String sendNfcBump(Context context, String propsConfigFile) throws IOException {
+
+        TestConfig testConfig = TestConfig.get(propsConfigFile);
+
+        Properties bumpProps = new Properties();
+        String deviceAdminPkgName = checkNotEmpty(testConfig.getDeviceAdminPkgName());
+        bumpProps.put(EXTRA_PROVISIONING_DEVICE_ADMIN_PACKAGE_NAME, deviceAdminPkgName);
+        bumpProps.put(EXTRA_PROVISIONING_DEVICE_ADMIN_PACKAGE_DOWNLOAD_LOCATION,
+                checkNotEmpty(testConfig.getDeviceAdminPkgLocation()));
+
+        String signatureHash = testConfig.getDeviceAdminPkgSignatureHash("");
+        String checksum = testConfig.getDeviceAdminPkgChecksum("");
+        if (signatureHash.isEmpty() && checksum.isEmpty()) {
+            throw new RuntimeException(String.format("Neither %s nor %s is specified in file %s",
+                    KEY_DEVICE_ADMIN_PKG_SIGNATURE_HASH,
+                    KEY_DEVICE_ADMIN_PKG_CHECKSUM,
+                    propsConfigFile));
+        }
+
+        if (!signatureHash.isEmpty()) {
+            bumpProps.put(EXTRA_PROVISIONING_DEVICE_ADMIN_SIGNATURE_CHECKSUM, signatureHash);
+        }
+
+        // Add checksum if it's not empty because OEM L devices use SHA1 checksum only.
+        if (!checksum.isEmpty()) {
+            bumpProps.put(EXTRA_PROVISIONING_DEVICE_ADMIN_PACKAGE_CHECKSUM, checksum);
+        }
+
+        String wifiSsid = checkNotEmpty(testConfig.getWifiSsid());
+        // Make sure to surround SSID with double quotes.
+        if (!wifiSsid.startsWith("\"") || !wifiSsid.endsWith("\"")) {
+            wifiSsid = "\"" + wifiSsid + "\"";
+        }
+        bumpProps.put(EXTRA_PROVISIONING_WIFI_SSID, wifiSsid);
+
+        bumpProps.put(EXTRA_PROVISIONING_WIFI_SECURITY_TYPE, testConfig.getWifiSecurityType(""));
+        bumpProps.put(EXTRA_PROVISIONING_WIFI_PASSWORD, testConfig.getWifiPassword(""));
+
+        // Skip encryption.
+        bumpProps.put(EXTRA_PROVISIONING_SKIP_ENCRYPTION, "true");
+
+        sendNfcBump(context, bumpProps);
+
+        // Return expected device admin package name
+        return deviceAdminPkgName;
+    }
+
+    /**
+     * Sends NFC bump to initiate device owner provisioning.
+     *
+     * @param context {@link Context} object.
+     * @param props {@link Properties} object contains NFC bump properties.
+     */
+    public static void sendNfcBump(Context context, Properties props) throws IOException {
+        ByteArrayOutputStream stream = new ByteArrayOutputStream();
+
+        props.store(stream, "AFW NFC provisioning");
+        NdefRecord record = NdefRecord
+                .createMime(MIME_TYPE_PROVISIONING_NFC, stream.toByteArray());
+        NdefMessage ndfMsg = new NdefMessage(new NdefRecord[]{record});
+
+        Intent intent = new Intent(ACTION_NDEF_DISCOVERED);
+        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+        intent.addCategory(Intent.CATEGORY_DEFAULT);
+        intent.setType(MIME_TYPE_PROVISIONING_NFC);
+        intent.putExtra(NfcAdapter.EXTRA_NDEF_MESSAGES, new NdefMessage[]{ndfMsg});
+
+        context.startActivity(intent);
+    }
+}
diff --git a/libs/CommonLib/src/com/android/afwtest/common/nfcprovisioning/Utils.java b/libs/CommonLib/src/com/android/afwtest/common/nfcprovisioning/Utils.java
new file mode 100644
index 0000000..810846a
--- /dev/null
+++ b/libs/CommonLib/src/com/android/afwtest/common/nfcprovisioning/Utils.java
@@ -0,0 +1,72 @@
+/*
+ * 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.common.nfcprovisioning;
+
+import static com.android.afwtest.common.Constants.ACTION_SEND_NFC_BUMP;
+import static com.android.afwtest.common.Constants.KEY_DEVICE_ADMIN_PKG_NAME;
+import static com.android.afwtest.common.Constants.NFC_BUMP_FILE;
+import static com.android.afwtest.common.Constants.SYSTEM_UTIL_PKG_NAME;
+
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+
+import com.android.afwtest.common.FileUtils;
+import com.android.afwtest.common.PkgMgrUtils;
+
+import java.util.Properties;
+
+/**
+ * NFC provisioning utils.
+ */
+public final class Utils {
+
+    /**
+     * Start provisioning by sending an intent to AfwThSystemUtil app.
+     *
+     * @param context {@link Context} object
+     * @param nfcBumpFilePath path of the file containing the parameters to be sent in the Nfc bump
+     * @return expected device admin package name
+     */
+    public static String startProvisioning(Context context, String nfcBumpFilePath)
+            throws Exception {
+        // Find Device Admin Package name
+        Properties props = FileUtils.readPropertiesFromFile(nfcBumpFilePath);
+        if (!props.containsKey(KEY_DEVICE_ADMIN_PKG_NAME)) {
+            throw new RuntimeException(
+                    KEY_DEVICE_ADMIN_PKG_NAME + " not specified in " + nfcBumpFilePath);
+        }
+
+        // Check if system util app is installed.
+        String deviceAdminPkgName = props.getProperty(KEY_DEVICE_ADMIN_PKG_NAME);
+        PackageManager pkgManager = context.getPackageManager();
+        if (!PkgMgrUtils.isPkgInstalled(context, SYSTEM_UTIL_PKG_NAME)) {
+            throw new RuntimeException(String.format("%s is not installed", SYSTEM_UTIL_PKG_NAME));
+        }
+
+        // Fire an intent to start nfc provisioning
+        Intent intent = new Intent(ACTION_SEND_NFC_BUMP);
+        intent.addCategory(Intent.CATEGORY_DEFAULT);
+        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+        intent.putExtra(NFC_BUMP_FILE, nfcBumpFilePath);
+
+        context.startActivity(intent);
+
+        // Return the expected device admin package name
+        return deviceAdminPkgName;
+    }
+}
diff --git a/libs/CommonLib/src/com/android/afwtest/common/qrcodeprovisioning/QRCodeSimulator.java b/libs/CommonLib/src/com/android/afwtest/common/qrcodeprovisioning/QRCodeSimulator.java
new file mode 100644
index 0000000..2c76716
--- /dev/null
+++ b/libs/CommonLib/src/com/android/afwtest/common/qrcodeprovisioning/QRCodeSimulator.java
@@ -0,0 +1,84 @@
+/*
+ * 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.common.qrcodeprovisioning;
+
+import static android.app.admin.DevicePolicyManager.ACTION_PROVISION_MANAGED_DEVICE_FROM_TRUSTED_SOURCE;
+import static android.app.admin.DevicePolicyManager.EXTRA_PROVISIONING_ADMIN_EXTRAS_BUNDLE;
+import static android.app.admin.DevicePolicyManager.EXTRA_PROVISIONING_DEVICE_ADMIN_COMPONENT_NAME;
+import static android.app.admin.DevicePolicyManager.EXTRA_PROVISIONING_DEVICE_ADMIN_PACKAGE_DOWNLOAD_LOCATION;
+import static android.app.admin.DevicePolicyManager.EXTRA_PROVISIONING_DEVICE_ADMIN_SIGNATURE_CHECKSUM;
+import static android.app.admin.DevicePolicyManager.EXTRA_PROVISIONING_LEAVE_ALL_SYSTEM_APPS_ENABLED;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.os.PersistableBundle;
+import android.text.TextUtils;
+
+import com.android.afwtest.common.test.TestConfig;
+import java.io.IOException;
+import java.util.Properties;
+
+/**
+ * Simulates QR code to start device owner provisioning.
+ */
+public final class QRCodeSimulator {
+
+    /**
+     * Sends QR code to initiate device owner provisioning.
+     *
+     * @param context {@link Context} object
+     * @param propsConfigFile {@link Properties} configuration file containing QR code properties.
+     */
+    public static void sendQRCode(Context context, String propsConfigFile)
+            throws IOException {
+        TestConfig testConfig = TestConfig.get(propsConfigFile);
+        // Fire an intent to start qrcode provisioning
+        Intent provisioningIntent =
+                new Intent(ACTION_PROVISION_MANAGED_DEVICE_FROM_TRUSTED_SOURCE);
+
+        String deviceAdminPkgName = testConfig.getDeviceAdminPkgName();
+        String deviceAdminReceiver = testConfig.getDeviceAdminReceiver();
+        String deviceAdminPkgLocation = testConfig.getDeviceAdminPkgLocation();
+        String deviceAdminPkgSignatureHash = testConfig.getDeviceAdminPkgSignatureHash();
+        PersistableBundle adminExtras = testConfig.getAdminExtrasBundle();
+
+        if (!TextUtils.isEmpty(deviceAdminPkgName) && !TextUtils.isEmpty(deviceAdminReceiver)) {
+            provisioningIntent
+                    .putExtra(EXTRA_PROVISIONING_DEVICE_ADMIN_COMPONENT_NAME,
+                            new ComponentName(deviceAdminPkgName, deviceAdminReceiver));
+        }
+        if (!TextUtils.isEmpty(deviceAdminPkgLocation)) {
+            provisioningIntent
+                    .putExtra(EXTRA_PROVISIONING_DEVICE_ADMIN_PACKAGE_DOWNLOAD_LOCATION,
+                    deviceAdminPkgLocation);
+        }
+        if (!TextUtils.isEmpty(deviceAdminPkgSignatureHash)) {
+            provisioningIntent
+                    .putExtra(EXTRA_PROVISIONING_DEVICE_ADMIN_SIGNATURE_CHECKSUM,
+                            deviceAdminPkgSignatureHash);
+        }
+        provisioningIntent
+                .putExtra(EXTRA_PROVISIONING_LEAVE_ALL_SYSTEM_APPS_ENABLED,
+                        testConfig.getLeaveAllSystemAppsEnabled());
+        if (adminExtras != null) {
+            provisioningIntent.putExtra(EXTRA_PROVISIONING_ADMIN_EXTRAS_BUNDLE, adminExtras);
+        }
+
+        context.startActivity(provisioningIntent);
+    }
+}
diff --git a/libs/CommonLib/src/com/android/afwtest/common/qrcodeprovisioning/Utils.java b/libs/CommonLib/src/com/android/afwtest/common/qrcodeprovisioning/Utils.java
new file mode 100644
index 0000000..b6b9e88
--- /dev/null
+++ b/libs/CommonLib/src/com/android/afwtest/common/qrcodeprovisioning/Utils.java
@@ -0,0 +1,70 @@
+/*
+ * 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.common.qrcodeprovisioning;
+
+import static com.android.afwtest.common.Constants.ACTION_START_QR_CODE_PROVISIONING;
+import static com.android.afwtest.common.Constants.KEY_DEVICE_ADMIN_PKG_NAME;
+import static com.android.afwtest.common.Constants.QR_CODE_FILE;
+import static com.android.afwtest.common.Constants.SYSTEM_UTIL_PKG_NAME;
+
+import android.content.Context;
+import android.content.Intent;
+
+import com.android.afwtest.common.FileUtils;
+import com.android.afwtest.common.PkgMgrUtils;
+
+import java.util.Properties;
+
+/**
+ * QR Code provisioning utils.
+ */
+public final class Utils {
+
+    /**
+     * Start provisioning by sending an intent to AfwThSystemUtil app.
+     *
+     * @param context {@link Context} object
+     * @param qrcodeFilePath path of the file containing the parameters to be sent in the QR Code
+     * @return expected device admin package name
+     */
+    public static String startProvisioning(Context context, String qrcodeFilePath)
+            throws Exception {
+        // Find Device Admin Package name
+        Properties props = FileUtils.readPropertiesFromFile(qrcodeFilePath);
+        if (!props.containsKey(KEY_DEVICE_ADMIN_PKG_NAME)) {
+            throw new RuntimeException(String.format("%s not specified in %s.",
+                    KEY_DEVICE_ADMIN_PKG_NAME, qrcodeFilePath));
+        }
+
+        // Check if system util app is installed.
+        String deviceAdminPkgName = props.getProperty(KEY_DEVICE_ADMIN_PKG_NAME);
+        if (!PkgMgrUtils.isPkgInstalled(context, SYSTEM_UTIL_PKG_NAME)) {
+            throw new RuntimeException(String.format("%s is not installed", SYSTEM_UTIL_PKG_NAME));
+        }
+
+        // Fire an intent to start qrcode provisioning
+        Intent intent = new Intent(ACTION_START_QR_CODE_PROVISIONING);
+        intent.addCategory(Intent.CATEGORY_DEFAULT);
+        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+        intent.putExtra(QR_CODE_FILE, qrcodeFilePath);
+
+        context.startActivity(intent);
+
+        // Return the expected device admin package name
+        return deviceAdminPkgName;
+    }
+}
diff --git a/libs/CommonLib/src/com/android/afwtest/common/test/OemWidget.java b/libs/CommonLib/src/com/android/afwtest/common/test/OemWidget.java
new file mode 100644
index 0000000..3f82d10
--- /dev/null
+++ b/libs/CommonLib/src/com/android/afwtest/common/test/OemWidget.java
@@ -0,0 +1,177 @@
+/*
+ * 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.common.test;
+
+import static com.android.afwtest.common.Constants.ACTION_CLICK;
+import static com.android.afwtest.common.Constants.KEY_OEM_WIDGET_ACTION;
+import static com.android.afwtest.common.Constants.KEY_OEM_WIDGET_CLASS;
+import static com.android.afwtest.common.Constants.KEY_OEM_WIDGET_DESCRIPTION;
+import static com.android.afwtest.common.Constants.KEY_OEM_WIDGET_PACKAGE;
+import static com.android.afwtest.common.Constants.KEY_OEM_WIDGET_RESOURCE_ID;
+import static com.android.afwtest.common.Constants.KEY_OEM_WIDGET_SCROLL_DIRECTION;
+import static com.android.afwtest.common.Constants.KEY_OEM_WIDGET_TEXT;
+
+import android.util.Log;
+
+import com.android.afwtest.common.Preconditions;
+
+/**
+ * A data class to store properties of an OEM widget.
+ */
+public final class OemWidget {
+
+    private static final String TAG = "afwtest.OemWidget";
+
+    // Id of the OEM widget.
+    private final String mId;
+
+    // Text property of this widget.
+    private final String mText;
+
+    // Description property of this widget.
+    private final String mDescription;
+
+    // Resource id of this widget.
+    private final String mResourceId;
+
+    // Package name of this widget.
+    private final String mPackage;
+
+    // Class name of this widget.
+    private final String mClass;
+
+    // Action of this widget, any of {@link com.android.afwtest.common.Constants#ACTION_CLICK},
+    // {@link com.android.afwtest.common.Constants#ACTION_CHECK} or
+    // {@link com.android.afwtest.common.Constants#ACTION_SCROLL}.
+    private final String mAction;
+
+    // If {@link #mAction} equals {@link com.android.afwtest.common.Constants#ACTION_SCROLL}
+    // this field specifies direction of scrolling.
+    private final String mScrollDirection;
+
+    /**
+     * Constructs a {@link OemWidget} from configuration file.
+     *
+     * @param testConfig Path of the test configuration file
+     * @param widgetId id of the widget to construct.
+     */
+    public OemWidget(TestConfig testConfig, String widgetId) {
+        // Check Id.
+        Preconditions.checkNotEmpty(widgetId);
+
+        Log.d(TAG, "Creating OemWidget " + widgetId);
+
+        mId = widgetId;
+
+        mText = testConfig.getProperty(widgetId + "." + KEY_OEM_WIDGET_TEXT, "");
+        mDescription = testConfig.getProperty(widgetId + "." + KEY_OEM_WIDGET_DESCRIPTION, "");
+        mResourceId = testConfig.getProperty(widgetId + "." + KEY_OEM_WIDGET_RESOURCE_ID, "");
+        mPackage = testConfig.getProperty(widgetId + "." + KEY_OEM_WIDGET_PACKAGE, "");
+        mClass = testConfig.getProperty(widgetId + "." + KEY_OEM_WIDGET_CLASS, "");
+        mAction = testConfig.getProperty(widgetId + "." + KEY_OEM_WIDGET_ACTION, ACTION_CLICK);
+        mScrollDirection =
+                testConfig.getProperty(widgetId + "." + KEY_OEM_WIDGET_SCROLL_DIRECTION, "UP");
+
+        // At least one property is specified.
+        Preconditions.checkNotEmpty(mText + mDescription + mResourceId + mPackage + mClass);
+
+        Log.d(TAG, "OemWidget: " + toString());
+    }
+
+    /**
+     * Gets id of this widget.
+     *
+     * @return id of this widget
+     */
+    public String getId() {
+        return mId;
+    }
+
+    /**
+     * Gets text property of this widget.
+     *
+     * @return text property of this widget
+     */
+    public String getText() {
+        return mText;
+    }
+
+    /**
+     * Gets description property of this widget.
+     *
+     * @return description property of this widget
+     */
+    public String getDescription() {
+        return mDescription;
+    }
+
+    /**
+     * Gets resource id of this widget.
+     *
+     * @return resournce id of this widget
+     */
+    public String getResourceId() {
+        return mResourceId;
+    }
+
+    /**
+     * Gets package name of this widget.
+     *
+     * @return package name of this widget
+     */
+    public String getPackage() {
+        return mPackage;
+    }
+
+    /**
+     * Gets class name of this widget.
+     *
+     * @return class name of this widget
+     */
+    public String getClassName() {
+        return mClass;
+    }
+
+    /**
+     * Gets action of this widget.
+     *
+     * @return action of this widget
+     */
+    public String getAction() {
+        return mAction;
+    }
+
+    /**
+     * Gets scrolling direction of this widget.
+     *
+     * @return scrolling direction of this widget.
+     */
+    public String getScrollDirection() {
+        return mScrollDirection;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public String toString() {
+        return String.format(
+                "OemWidget[Id=%s,text=[%s],desc=[%s],res=[%s],pkg=[%s],class=[%s],action=[%s],"
+                        + "scroll_direction=[%s]]",
+                mId, mText, mDescription, mResourceId, mPackage, mClass, mAction, mScrollDirection);
+    }
+}
diff --git a/libs/CommonLib/src/com/android/afwtest/common/test/StatsLogger.java b/libs/CommonLib/src/com/android/afwtest/common/test/StatsLogger.java
new file mode 100644
index 0000000..6131f9d
--- /dev/null
+++ b/libs/CommonLib/src/com/android/afwtest/common/test/StatsLogger.java
@@ -0,0 +1,153 @@
+/*
+ * 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.common.test;
+
+import static android.os.Environment.getExternalStorageDirectory;
+
+import android.text.TextUtils;
+import android.util.Log;
+import android.util.Range;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Singleton class to measure and save time stats in a local file in External Storage Directory.
+ *
+ * <p>writeStatsToFile() should be called at the end of the test.</p>
+ */
+public final class StatsLogger {
+
+    private static final String TAG = "afwtest.StatsLogger";
+
+    // Map stats file with corresponding {@link StatsLogger} object.
+    private static Map<String, StatsLogger> sStasLogger = new HashMap<String, StatsLogger>();
+
+    // File to save stats.
+    private File mAfwStatsFile;
+
+    // Time stats being logged by this StatsLogger.
+    // The key of the map is the name of a metric.
+    private Map<String, Range<Long>> mTimeStats = new HashMap<String, Range<Long>>();
+
+    /**
+     * Constructor.
+     *
+     * @param fileName Path of the file to save stats
+     */
+    private StatsLogger(String fileName) throws IOException {
+        mAfwStatsFile = new File(getExternalStorageDirectory().getAbsolutePath(), fileName);
+
+        if (!mAfwStatsFile.exists()){
+            Log.d(TAG, String.format("%s does not exist, creating... ", fileName));
+            mAfwStatsFile.createNewFile();
+        }
+    }
+
+    /**
+     * Gets {@link StatsLogger} object for given file, or create a new one if does not exist.
+     *
+     * @param fileName file relative to external storage.
+     */
+    public static StatsLogger getInstance(String fileName) throws IOException {
+        if (sStasLogger.containsKey(fileName)){
+            return sStasLogger.get(fileName);
+        }
+
+        StatsLogger statsLogger = new StatsLogger(fileName);
+        sStasLogger.put(fileName, statsLogger);
+        return statsLogger;
+    }
+
+    /**
+     * Saves the stats to file.
+     */
+    public void writeStatsToFile() throws IOException {
+        FileOutputStream outputStream = new FileOutputStream(mAfwStatsFile);
+        String stats = toCsvFormatString(mTimeStats);
+
+        try {
+            outputStream.write(stats.getBytes());
+        } catch (Exception e) {
+            Log.e(TAG, "Failed to write stats to file", e);
+        } finally {
+            outputStream.close();
+        }
+    }
+
+    /**
+     * Converts set of time stats to cvs formatted string.
+     *
+     * @param timeStats time stats to convert, the key of the map is time stats name
+     * @return String format:
+     *             Key1,Key2,...\n
+     *             Time1,Time2,...\n
+     */
+    private static String toCsvFormatString(Map<String, Range<Long>> timeStats) {
+        List<String> keyList = new ArrayList();
+        List<String> valueList = new ArrayList();
+        for (Map.Entry<String, Range<Long>> entry : timeStats.entrySet()) {
+            keyList.add(entry.getKey());
+            long interval = entry.getValue().getUpper() - entry.getValue().getLower();
+            valueList.add(String.format("%d", interval));
+        }
+        String keys = TextUtils.join(",", keyList);
+        String values = TextUtils.join(",", valueList);
+
+        return String.format("%s\n%s", keys, values);
+    }
+
+    /**
+     * Start counting time associated with the passed metric.
+     *
+     * @param keyName name of the metric to measure.
+     */
+    public void startTime(String keyName){
+        long currentTime = System.currentTimeMillis();
+        Range<Long> range = Range.create(currentTime, currentTime);
+        Log.d(TAG, String.format("%s start-time: %d", keyName, currentTime));
+
+        mTimeStats.put(keyName, range);
+        Log.d(TAG, String.format("HashMap for %s:%s", keyName, range.toString()));
+    }
+
+    /**
+     * Stops counting time associated with the given metric and return the period.
+     *
+     * @param keyName name of the metric to measure
+     * @return time between startTime and stopTime in milliseconds
+     */
+    public long stopTime(String keyName) throws Exception {
+        // Find the key
+        if (!mTimeStats.containsKey(keyName)) {
+            throw new RuntimeException(String.format("%s was never started", keyName));
+        }
+
+        // Set end time
+        Range<Long> range = mTimeStats.get(keyName).extend(System.currentTimeMillis());
+        mTimeStats.put(keyName, range);
+        Log.d(TAG, String.format("%s: %s", keyName, range.toString()));
+
+        return range.getUpper() - range.getLower();
+    }
+}
+
diff --git a/libs/CommonLib/src/com/android/afwtest/common/test/TestConfig.java b/libs/CommonLib/src/com/android/afwtest/common/test/TestConfig.java
new file mode 100644
index 0000000..5b88a05
--- /dev/null
+++ b/libs/CommonLib/src/com/android/afwtest/common/test/TestConfig.java
@@ -0,0 +1,472 @@
+/*
+ * 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.common.test;
+
+import static com.android.afwtest.common.Constants.ADMIN_EXTRAS_BUNDLE;
+import static com.android.afwtest.common.Constants.KEY_APP_CRASH_WHITELIST;
+import static com.android.afwtest.common.Constants.KEY_DEVICE_ADMIN_PKG_CHECKSUM;
+import static com.android.afwtest.common.Constants.KEY_DEVICE_ADMIN_PKG_LOCATION;
+import static com.android.afwtest.common.Constants.KEY_DEVICE_ADMIN_PKG_NAME;
+import static com.android.afwtest.common.Constants.KEY_DEVICE_ADMIN_RECEIVER;
+import static com.android.afwtest.common.Constants.KEY_DEVICE_ADMIN_PKG_SIGNATURE_HASH;
+import static com.android.afwtest.common.Constants.KEY_MUTE_APP_CRASH_DIALOGS;
+import static com.android.afwtest.common.Constants.KEY_OEM_WIDGETS;
+import static com.android.afwtest.common.Constants.KEY_TEST_TIMEOUT_MIN;
+import static com.android.afwtest.common.Constants.KEY_TIMEOUT_SIZE;
+import static com.android.afwtest.common.Constants.KEY_WIFI_PWD;
+import static com.android.afwtest.common.Constants.KEY_WIFI_SECURITY_TYPE;
+import static com.android.afwtest.common.Constants.KEY_WIFI_SSID;
+import static com.android.afwtest.common.Constants.KEY_WORK_ACCOUNT_PASSWORD;
+import static com.android.afwtest.common.Constants.KEY_WORK_ACCOUNT_USERNAME;
+import static com.android.afwtest.common.Constants.LEAVE_ALL_SYSTEM_APPS_ENABLED;
+
+import android.os.PersistableBundle;
+import android.support.annotation.Nullable;
+import android.util.Log;
+
+import com.android.afwtest.common.FileUtils;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+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 final class TestConfig {
+
+    private static final String TAG = "afwtest.TestConfig";
+
+    /**
+     * Mapping from timeout size to its integer value.
+     */
+    private static final Map<String, Integer> TIMEOUT_SIZE_MAPPING =
+            new HashMap<String, Integer>() {{
+                put("S", 2);
+                put("M", 3);
+                put("L", 5);
+                put("XL", 8);
+                put("XXL", 13);
+            }};
+
+    /**
+     * File path of the default test config file.
+     */
+    public static final String DEFAULT_TEST_CONFIG_FILE = "/data/local/tmp/afw-test.props";
+
+    // Mapping from test configuration file path and its loaded {@link TestConfig} object.
+    private static Map<String, TestConfig> sTestConfigs = new HashMap<String, TestConfig>();
+
+    // Store configurations loaded from a file.
+    private final Properties mProps;
+
+    /**
+     * Creates a new object by loading configurations from file.
+     *
+     * @param configFilePath path of the test configuration file
+     */
+    private TestConfig(String configFilePath) throws IOException {
+        mProps = FileUtils.readPropertiesFromFile(configFilePath);
+    }
+
+    /**
+     * Gets {@link TestConfig} object for given file path.
+     *
+     * @param configFilePath file path of the test configuration
+     * @return {@link TestConfig} object for given file path
+     */
+    public static TestConfig get(String configFilePath) throws IOException {
+        if (sTestConfigs.containsKey(configFilePath)) {
+            return sTestConfigs.get(configFilePath);
+        }
+
+        // Create new object.
+        TestConfig testConfig = new TestConfig(configFilePath);
+        sTestConfigs.put(configFilePath, testConfig);
+        return testConfig;
+    }
+
+    /**
+     * Gets {@link TestConfig} object for {@link #DEFAULT_TEST_CONFIG_FILE}.
+     *
+     * @return {@link TestConfig} object for {@link #DEFAULT_TEST_CONFIG_FILE}
+     */
+    public static TestConfig getDefault() throws IOException {
+        return get(DEFAULT_TEST_CONFIG_FILE);
+    }
+
+    /**
+     * Gets device admin package name.
+     *
+     * @return device admin package name
+     */
+    public String getDeviceAdminPkgName() {
+        return mProps.getProperty(KEY_DEVICE_ADMIN_PKG_NAME);
+    }
+
+    /**
+     * Gets device admin package name.
+     *
+     * @param defaultValue default value if device admin pkg name is not found
+     * @return device admin package name
+     */
+    public String getDeviceAdminPkgName(String defaultValue) {
+        return mProps.getProperty(KEY_DEVICE_ADMIN_PKG_NAME, defaultValue);
+    }
+
+    /**
+     * Gets device admin receiver.
+     *
+     * @return device admin receiver
+     */
+    public String getDeviceAdminReceiver() {
+        return mProps.getProperty(KEY_DEVICE_ADMIN_RECEIVER);
+    }
+
+    /**
+     * Gets device admin receiver.
+     *
+     * @param defaultValue default value if device admin receiver is not found
+     * @return device admin receiver
+     */
+    public String getDeviceAdminReceiver(String defaultValue) {
+        return mProps.getProperty(KEY_DEVICE_ADMIN_RECEIVER, defaultValue);
+    }
+
+    /**
+     * Gets device admin package location.
+     *
+     * @return device admin package location
+     */
+    public String getDeviceAdminPkgLocation() {
+        return mProps.getProperty(KEY_DEVICE_ADMIN_PKG_LOCATION);
+    }
+
+    /**
+     * Gets device admin package location.
+     *
+     * @param defaultValue default value if device admin package location is not found
+     * @return device admin package location
+     */
+    public String getDeviceAdminPkgLocation(String defaultValue) {
+        return mProps.getProperty(KEY_DEVICE_ADMIN_PKG_LOCATION, defaultValue);
+    }
+
+    /**
+     * Gets device admin package checksum.
+     *
+     * @return device admin package checksum
+     */
+    public String getDeviceAdminPkgChecksum() {
+        return mProps.getProperty(KEY_DEVICE_ADMIN_PKG_CHECKSUM);
+    }
+
+    /**
+     * Gets device admin package checksum.
+     *
+     * @param defaultValue default valud if device admin package checksum is not found
+     * @return device admin package checksum
+     */
+    public String getDeviceAdminPkgChecksum(String defaultValue) {
+        return mProps.getProperty(KEY_DEVICE_ADMIN_PKG_CHECKSUM, defaultValue);
+    }
+
+    /**
+     * Gets device admin package signature hash.
+     *
+     * @return device admin package signature hash
+     */
+    public String getDeviceAdminPkgSignatureHash() {
+        return mProps.getProperty(KEY_DEVICE_ADMIN_PKG_SIGNATURE_HASH);
+    }
+
+    /**
+     * Gets device admin package signature hash.
+     *
+     * @param defaultValue default value if device admin package signature hash is not found
+     * @return device admin package signature hash
+     */
+    public String getDeviceAdminPkgSignatureHash(String defaultValue) {
+        return mProps.getProperty(KEY_DEVICE_ADMIN_PKG_SIGNATURE_HASH, defaultValue);
+    }
+
+    /**
+     * Gets timeout size value from props file.
+     *
+     * <p>
+     * Possible size values are strings "S", "M", "L", "XL" or "XXL".
+     * </p>
+     *
+     * @return An integer corresponding to the timeout size configured in the config file.
+     */
+    public int getTimeoutSize() {
+        // Default to M
+        String timeoutSize = getProperty(KEY_TIMEOUT_SIZE, "M");
+        if (!TIMEOUT_SIZE_MAPPING.containsKey(timeoutSize)) {
+            Log.w(TAG, "Invalid timeout size, defaulting to M");
+            timeoutSize = "M";
+        }
+
+        return TIMEOUT_SIZE_MAPPING.get(timeoutSize);
+    }
+
+    /**
+     * Gets test timeout in minutes.
+     *
+     * @return test timeout in minutes or -1 if either timeout is not specified or invalid
+     */
+    public int getTestTimeoutMin() {
+        return getTestTimeoutMin(-1);
+    }
+
+    /**
+     * Gets test timeout in minutes.
+     *
+     * @param defaultValue default value if test timeout not specified
+     * @return test timeout in minutes
+     */
+    public int getTestTimeoutMin(int defaultValue) {
+        String value = mProps.getProperty(KEY_TEST_TIMEOUT_MIN);
+        if (value == null || value.isEmpty()) {
+            return defaultValue;
+        }
+
+        return Integer.valueOf(value);
+    }
+
+    /**
+     * Gets Wifi SSID.
+     *
+     * @return Wifi SSID
+     */
+    public String getWifiSsid() {
+        return mProps.getProperty(KEY_WIFI_SSID);
+    }
+
+    /**
+     * Gets Wifi SSID.
+     *
+     * @param defaultValue default value if Wifi SSID is not found
+     * @return Wifi SSID
+     */
+    public String getWifiSsid(String defaultValue) {
+        return mProps.getProperty(KEY_WIFI_SSID, defaultValue);
+    }
+
+    /**
+     * Gets Wifi password.
+     *
+     * @return Wifi password
+     */
+    public String getWifiPassword() {
+        return mProps.getProperty(KEY_WIFI_PWD);
+    }
+
+    /**
+     * Gets Wifi password.
+     *
+     * @param defaultValue default value if Wifi password is not found
+     * @return Wifi password
+     */
+    public String getWifiPassword(String defaultValue) {
+        return mProps.getProperty(KEY_WIFI_PWD, defaultValue);
+    }
+
+    /**
+     * Gets Wifi security type.
+     *
+     * @return Wifi security type
+     */
+    public String getWifiSecurityType() {
+        return mProps.getProperty(KEY_WIFI_SECURITY_TYPE);
+    }
+
+    /**
+     * Gets Wifi security type.
+     *
+     * @param defaultValue default value if Wifi security type is not found
+     * @return Wifi security type
+     */
+    public String getWifiSecurityType(String defaultValue) {
+        return mProps.getProperty(KEY_WIFI_SECURITY_TYPE, defaultValue);
+    }
+
+    /**
+     * Gets work account user name.
+     *
+     * @return work account user name
+     */
+    public String getWorkAccountUsername() {
+        return mProps.getProperty(KEY_WORK_ACCOUNT_USERNAME);
+    }
+
+    /**
+     * Gets work account user name.
+     *
+     * @param defaultValue default value if work account user name is not found
+     * @return work account user name
+     */
+    public String getWorkAccountUsername(String defaultValue) {
+        return mProps.getProperty(KEY_WORK_ACCOUNT_USERNAME, defaultValue);
+    }
+
+    /**
+     * Gets work account password.
+     *
+     * @return work account password
+     */
+    public String getWorkAccountPassword() {
+        return mProps.getProperty(KEY_WORK_ACCOUNT_PASSWORD);
+    }
+
+    /**
+     * Gets work account password.
+     *
+     * @param defaultValue default value if work account password is not found
+     * @return work account password
+     */
+    public String getWorkAccountPassword(String defaultValue) {
+        return mProps.getProperty(KEY_WORK_ACCOUNT_PASSWORD, defaultValue);
+    }
+
+    /**
+     * Gets the property of a specific key.
+     *
+     * @param key property key
+     * @return property 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 the OEM widgets as a list.
+     *
+     * @return {@link List} of {@link OemWidget}
+     */
+    public List<OemWidget> getOemWidgets() {
+        List<OemWidget> oemWidgets = new LinkedList<OemWidget>();
+
+        String idList = mProps.getProperty(KEY_OEM_WIDGETS, "");
+        if (!idList.isEmpty()) {
+            String[] ids = idList.split(",");
+            for (String id : ids) {
+                OemWidget oemWidget = new OemWidget(this, id);
+                oemWidgets.add(oemWidget);
+            }
+        }
+
+        return oemWidgets;
+    }
+
+    /**
+     * Whether to mute app crash dialogs.
+     *
+     * @return {@code true} if yes, {@code false} otherwise
+     */
+    public boolean muteAppCrashDialogs() {
+        return Boolean.parseBoolean(mProps.getProperty(KEY_MUTE_APP_CRASH_DIALOGS, "true"));
+    }
+
+    /**
+     * Gets list of app names that if they crash, just auto close the dialog.
+     *
+     * @return set of app names that in the app crash dialog message, all in lower case
+     */
+    public List<String> getAppCrashWhitelist() {
+        String[] apps = getProperty(KEY_APP_CRASH_WHITELIST, "").split(",");
+        List<String> results = new LinkedList(Arrays.asList(apps));
+        results.removeAll(Arrays.asList("", null));
+        return results;
+    }
+
+    /**
+     * Gets whether all system apps should be left enabled while provisioning
+     */
+    public boolean getLeaveAllSystemAppsEnabled() {
+        return Boolean.parseBoolean(mProps.getProperty(LEAVE_ALL_SYSTEM_APPS_ENABLED, "false"));
+    }
+
+    /**
+     * Gets the admin extras to be passed along during provisioning
+     */
+    @Nullable
+    public PersistableBundle getAdminExtrasBundle() {
+        try {
+            String rawExtrasString = getProperty(ADMIN_EXTRAS_BUNDLE, null);
+            if (rawExtrasString == null)
+                return null;
+
+            JSONObject adminExtras = new JSONObject(rawExtrasString);
+            PersistableBundle adminExtrasBundle = new PersistableBundle();
+            Iterator<String> keys = adminExtras.keys();
+
+            while (keys.hasNext()) {
+                String key = keys.next();
+                Object val = adminExtras.get(key);
+
+                if (val == null) {
+                    // Assume any null value from the provisioning data json is of type string.
+                    adminExtrasBundle.putString(key, null);
+                } else if (val instanceof Boolean) {
+                    adminExtrasBundle.putBoolean(key, (Boolean) val);
+                } else if (val instanceof Double) {
+                    adminExtrasBundle.putDouble(key, (Double) val);
+                } else if (val instanceof Integer) {
+                    adminExtrasBundle.putInt(key, (Integer) val);
+                } else if (val instanceof Long) {
+                    adminExtrasBundle.putLong(key, (Long) val);
+                } else if (val instanceof String) {
+                    adminExtrasBundle.putString(key, (String) val);
+                } else {
+                    throw new JSONException("Unsupported value for key: " + key);
+                }
+            }
+
+            return adminExtrasBundle;
+
+        } catch (JSONException e) {
+            Log.w(TAG, "Invalid JSON format for admin extras bundle, returning null");
+            return null;
+        }
+    }
+
+
+}
diff --git a/libs/UiAutomatorLib/Android.mk b/libs/UiAutomatorLib/Android.mk
new file mode 100644
index 0000000..5aade26
--- /dev/null
+++ b/libs/UiAutomatorLib/Android.mk
@@ -0,0 +1,29 @@
+#
+# 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.
+#
+
+LOCAL_PATH:= $(call my-dir)
+
+include $(CLEAR_VARS)
+
+LOCAL_SRC_FILES := $(call all-java-files-under, src)
+
+LOCAL_MODULE_TAGS := optional
+
+LOCAL_STATIC_JAVA_LIBRARIES := ub-uiautomator AfwThCommonLib android-support-test
+
+LOCAL_MODULE := AfwThUiAutomatorLib
+
+include $(BUILD_STATIC_JAVA_LIBRARY)
diff --git a/libs/UiAutomatorLib/src/com/android/afwtest/uiautomator/Constants.java b/libs/UiAutomatorLib/src/com/android/afwtest/uiautomator/Constants.java
new file mode 100644
index 0000000..bb9e3df
--- /dev/null
+++ b/libs/UiAutomatorLib/src/com/android/afwtest/uiautomator/Constants.java
@@ -0,0 +1,282 @@
+/*
+ * 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.uiautomator;
+
+import static java.util.regex.Pattern.CASE_INSENSITIVE;
+
+import android.support.test.uiautomator.By;
+import android.support.test.uiautomator.BySelector;
+import android.webkit.WebView;
+import android.widget.Button;
+import android.widget.EditText;
+import android.widget.RadioButton;
+import android.widget.ScrollView;
+import android.widget.Switch;
+
+import java.util.regex.Pattern;
+
+/**
+ * Constants for this lib and test packages.
+ */
+public final class Constants {
+
+    /**
+     * Android package name.
+     */
+    public static final String ANDROID_PKG_NAME = "android";
+
+    /**
+     * System UI package name.
+     */
+    public static final String SYSTEM_UI_PKG_NAME = "com.android.systemui";
+
+    /**
+     * GMSCore package name.
+     */
+    public static final String GMS_PKG_NAME = "com.google.android.gms";
+
+    /**
+     * GMSCore package name regular expression string.
+     */
+    public static final String GMS_PKG_NAME_REGEX =
+            Pattern.quote(GMS_PKG_NAME) + "(\\.[^:]+)?";
+
+    /**
+     * Managed provisioning package name.
+     */
+    public static final String MANAGED_PROVISIONING_PKG_NAME = "com.android.managedprovisioning";
+
+    /**
+     * Android package installer package name.
+     */
+    public static final String PACKAGE_INSTALLER_PKG_NAME = "com.android.packageinstaller";
+
+    /**
+     * TestDpc package name.
+     */
+    public static final String TESTDPC_PKG_NAME = "com.afwsamples.testdpc";
+
+    /**
+     * Default file name for provisioning performance stats.
+     */
+    public static final String PROVISIONING_STATS_FILE = "Provisioning-Stats.csv";
+
+    /**
+     * Representation name of TestDpc work profile creation time.
+     */
+    public static final String STAT_TESTDPC_WORK_PROFILE_CREATION_TIME =
+            "testDPC Work Profile creation";
+
+    /**
+     * Representation name of Managed Provisioning work profile creation name.
+     */
+    public static final String STAT_MANAGED_PROVISIONING_WORK_PROFILE_CREATION_TIME =
+            "MP Work Profile creation";
+
+    /**
+     * GMS button with text ok.
+     */
+    public static final BySelector GMS_BTN_WITH_TEXT_OK =
+        By.pkg(GMS_PKG_NAME).text(Pattern.compile("ok", CASE_INSENSITIVE));
+
+    /**
+     * GMS button with content description ok.
+     */
+    public static final BySelector GMS_BTN_WITH_DESC_OK =
+        By.pkg(GMS_PKG_NAME).desc(Pattern.compile("ok", CASE_INSENSITIVE));
+
+    /**
+     * Regular expression string to match resource id of GMSCore NEXT button.
+     */
+    public static final String GMS_NEXT_BTN_RESOURCE_ID_REGEX =
+            GMS_PKG_NAME_REGEX
+                    + ":id/(auth_setup_wizard_navbar_next"
+                    + "|suw_navbar_next"
+                    + "|google_services_next_button_item"
+                    + "|auth_device_management_download_next_button"
+                    + "|next_button)";
+    /**
+     * {@link BySelector} for {@link Button} in GMS core with description "ACCEPT".
+     */
+    public static final BySelector GMS_ACCEPT_BUTTON_SELECTOR =
+        By.pkg(GMS_PKG_NAME)
+            .desc(Pattern.compile("ACCEPT", CASE_INSENSITIVE));
+
+    /**
+     * {@link BySelector} for {@link Button} in GMS core with text "NEXT".
+     */
+    public static final BySelector GMS_BUTTON_WITH_TEXT_NEXT =
+            By.pkg(GMS_PKG_NAME)
+                    .text(Pattern.compile("NEXT", CASE_INSENSITIVE));
+
+
+    /**
+     * {@link BySelector} for {@link Button} in GMS core with description "NEXT".
+     */
+    public static final BySelector GMS_NEXT_BUTTON_SELECTOR =
+        By.pkg(GMS_PKG_NAME)
+            .desc(Pattern.compile("NEXT", CASE_INSENSITIVE));
+
+    /**
+     * Resource Id {@link BySelector} for GmsCore NEXT button.
+     */
+    public static final BySelector GMS_NEXT_BUTTON_RES_SELECTOR =
+            By.res(Pattern.compile(GMS_NEXT_BTN_RESOURCE_ID_REGEX))
+                    .enabled(true)
+                    .clickable(true);
+
+    /**
+     * {@link BySelector} for {@link EditText} in GMS core.
+     */
+    public static final BySelector GMS_TEXT_FIELD_SELECTOR = By.pkg(GMS_PKG_NAME).clazz(
+            EditText.class.getName());
+
+    /**
+     * {@link BySelector} for {@link CheckBox} in GMS core with resource-id "agree_backup".
+     */
+    public static final BySelector GMS_AGREE_BACKUP_SELECTOR =
+            By.res(Pattern.compile(GMS_PKG_NAME_REGEX + ":id/agree_backup"));
+
+    /**
+     * {@link BySelector} for {@link Switch} in GMS core with resource-id
+     * "suw_items_switch".
+     */
+    public static final BySelector GMS_SWITCH_SELECTOR =
+            By.res(Pattern.compile(GMS_PKG_NAME_REGEX + ":id/suw_items_switch"));
+
+    /**
+     * {@link BySelector} for {@link Button} in GMS core with text "CLOSE".
+     */
+    public static final BySelector GMS_CLOSE_BACKUP_MESSAGE_SELECTOR =
+            By.pkg(GMS_PKG_NAME).text("CLOSE");
+
+    /**
+     * @{@link BySelector} for downloading MDMs in GmsCore.
+     */
+    public static final BySelector GMS_DOWNLOADING_DIALOG_SELECTOR =
+            By.text(Pattern.compile("Downloading.*"))
+                    .res(ANDROID_PKG_NAME, "message")
+                    .pkg(GMS_PKG_NAME);
+
+    /**
+     * {@link BySelector} for GMSCore web view UI.
+     */
+    public static final BySelector GMS_WEBVIEW_SELECTOR =
+            By.pkg(Constants.GMS_PKG_NAME).clazz(WebView.class.getName());
+
+    /**
+     * @{@link BySelector} unique to the add account page of GmsCore.
+     */
+    public static final BySelector GMS_ADD_ACCOUNT_PAGE_SELECTOR =
+            By.pkg(GMS_PKG_NAME)
+                .desc(Pattern.compile("Add your account|Sign in", CASE_INSENSITIVE));
+
+    /**
+     * {@link BySelector} for {@link EditText} with resource-name "password" in GMSCore.
+     */
+    public static final BySelector GMS_PASSWORD_FIELD_SELECTOR =
+            By.pkg(GMS_PKG_NAME).res("password");
+
+    /**
+     * {@link BySelector} for {@link Button} in ManagedProvisioning to set up the profile/device.
+     */
+    public static final BySelector MANAGED_PROVISIONING_SETUP_BUTTON_SELECTOR = By.res(
+            Pattern.compile(MANAGED_PROVISIONING_PKG_NAME + ":id/next_button"));
+
+    /**
+     * {@link BySelector} in ManagedProvisioning.
+     */
+    public static final BySelector MANAGED_PROVISIONING_PKG_SELECTOR = By.pkg(
+            MANAGED_PROVISIONING_PKG_NAME);
+
+    /**
+     * {@link BySelector} for {@link Button} in ManagedProvisioning with resource-id
+     * positive_button.
+     */
+    public static final BySelector MANAGED_PROVISIONING_OK_BUTTON_SELECTOR = By.res(
+            MANAGED_PROVISIONING_PKG_NAME, "positive_button");
+
+    public static final BySelector MANAGED_PROVISIONING_SCROLL_VIEW_SELECTOR =
+            By.pkg(MANAGED_PROVISIONING_PKG_NAME).clazz(ScrollView.class.getName());
+
+    /**
+     * {@link BySelector} for {@link Button} in Package Installer with resource-id ok_button.
+     */
+    public static final BySelector PACKAGE_INSTALLER_INSTALL_BUTTON_SELECTOR = By.res(
+            PACKAGE_INSTALLER_PKG_NAME, "ok_button");
+
+    public static final BySelector TESTDPC_SETUP_MANAGEMENT_PAGE_TITLE_SELECTOR =
+            By.pkg(TESTDPC_PKG_NAME).text(Pattern.compile("Set.*management.*", CASE_INSENSITIVE));
+
+    public static final BySelector TESTDPC_SCROLL_VIEW_SELECTOR =
+            By.pkg(TESTDPC_PKG_NAME).clazz(ScrollView.class.getName());
+
+    /**
+     * {@link BySelector} for {@link RadioButton} with resource-id setup_device_owner on
+     * TestDpc Setup Management page.
+     */
+    public static final BySelector TESTDPC_SETUP_DEVICE_OWNER_RADIO_BUTTON_SELECTOR =
+            By.res(TESTDPC_PKG_NAME, "setup_device_owner")
+                    .clazz(RadioButton.class.getName())
+                    .clickable(true)
+                    .checkable(true);
+
+    /**
+     * {@link BySelector} for {@link RadioButton} with resource-id setup_managed_profile on
+     * TestDpc Setup Management page.
+     */
+    public static final BySelector TESTDPC_SETUP_MANAGED_PROFILE_RADIO_BUTTON_SELECTOR =
+            By.res(TESTDPC_PKG_NAME, "setup_managed_profile")
+                    .clazz(RadioButton.class.getName())
+                    .clickable(true)
+                    .checkable(true);
+
+    /**
+     * {@link BySelector} for the 'NEXT' navigate button on TestDpc pages.
+     */
+    public static final BySelector TESTDPC_NEXT_BUTTON_SELECTOR =
+            By.res(TESTDPC_PKG_NAME, "suw_navbar_next")
+                    .clazz(Button.class.getName())
+                    .clickable(true);
+
+    /**
+     * {@link BySelector} for Managed Provisioning error message.
+     */
+    public static final BySelector MANAGED_PROVISIONING_ERROR_MSG_SELECTOR =
+            By.pkg(MANAGED_PROVISIONING_PKG_NAME).res(ANDROID_PKG_NAME, "message");
+
+    /**
+     * {@link BySelector} for Android platform alert message.
+     */
+    public static final BySelector ANDROID_ALERT_MSG_SELECTOR = By.res(ANDROID_PKG_NAME, "message");
+
+    /**
+     * {@link BySelector} for Android platform button.
+     */
+    public static final BySelector ANDROID_DIALOG_BTN_SELECTOR = By.clazz(Button.class.getName());
+
+    /**
+     * {@link BySelector} for {@link Button} in TestDpc with resource-id finish_setup.
+     */
+    public static final BySelector TESTDPC_FINISH_SKIP_BUTTON_SELECTOR =
+            By.res(TESTDPC_PKG_NAME, "btn_add_account_skip");
+
+    /**
+     * Private constructor to prevent instantiation.
+     */
+    private Constants() {
+    }
+}
diff --git a/libs/UiAutomatorLib/src/com/android/afwtest/uiautomator/pages/LandingPage.java b/libs/UiAutomatorLib/src/com/android/afwtest/uiautomator/pages/LandingPage.java
new file mode 100644
index 0000000..4b99dc6
--- /dev/null
+++ b/libs/UiAutomatorLib/src/com/android/afwtest/uiautomator/pages/LandingPage.java
@@ -0,0 +1,65 @@
+/*
+ * 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.uiautomator.pages;
+
+import android.support.test.uiautomator.BySelector;
+import android.support.test.uiautomator.UiDevice;
+
+import com.android.afwtest.common.test.TestConfig;
+
+/**
+ * A {@link UiPage} that won't navigate any further.
+ *
+ * <p>Such page is usually used as the last page of an automation flow.</p>
+ */
+public final class LandingPage extends UiPage {
+
+    /**
+     * Unique element that this page should wait for.
+     */
+    private final BySelector mUniqueElementSelector;
+
+    /**
+     * Constructor.
+     *
+     * @param uiDevice {@link UiDevice} object
+     * @param config {@link TestConfig} object holding test configurations
+     * @param uniqueElementSelector {@link BySelector} of the ui element to wait for
+     */
+    public LandingPage(UiDevice uiDevice,
+            TestConfig config,
+            BySelector uniqueElementSelector) {
+        super(uiDevice, config);
+        mUniqueElementSelector =  uniqueElementSelector;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public BySelector uniqueElement() {
+        return mUniqueElementSelector;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void navigate() throws Exception {
+        // Do nothing.
+    }
+}
diff --git a/libs/UiAutomatorLib/src/com/android/afwtest/uiautomator/pages/PageSkipper.java b/libs/UiAutomatorLib/src/com/android/afwtest/uiautomator/pages/PageSkipper.java
new file mode 100644
index 0000000..cc3c8c8
--- /dev/null
+++ b/libs/UiAutomatorLib/src/com/android/afwtest/uiautomator/pages/PageSkipper.java
@@ -0,0 +1,233 @@
+/*
+ * 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.uiautomator.pages;
+
+import static com.android.afwtest.common.Constants.ACTION_CHECK;
+import static com.android.afwtest.common.Constants.ACTION_CLICK;
+import static com.android.afwtest.common.Constants.ACTION_SCROLL;
+import static com.android.afwtest.uiautomator.utils.WidgetUtils.safeClick;
+import static com.android.afwtest.uiautomator.utils.WidgetUtils.safeFling;
+
+import android.os.SystemClock;
+import android.support.test.uiautomator.By;
+import android.support.test.uiautomator.BySelector;
+import android.support.test.uiautomator.Direction;
+import android.support.test.uiautomator.UiDevice;
+import android.support.test.uiautomator.UiObject2;
+import android.text.TextUtils;
+import android.util.Log;
+
+import com.android.afwtest.common.Timer;
+import com.android.afwtest.common.test.OemWidget;
+import com.android.afwtest.common.test.TestConfig;
+import com.android.afwtest.uiautomator.utils.BySelectorHelper;
+import com.android.afwtest.uiautomator.utils.WidgetUtils;
+
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.TimeUnit;
+import java.util.regex.Pattern;
+
+/**
+ * A help class to skip pages until the expected page appears.
+ */
+public final class PageSkipper extends UiPage {
+
+    private static final String TAG = "afwtest.PageSkipper";
+
+    /**
+     * Thread sleep time.
+     */
+    private static final long THREAD_SLEEP_TIME_MS = TimeUnit.SECONDS.toMillis(3);
+
+    /**
+     * Words to match when finding available "NEXT" navigation buttons.
+     *
+     * The order of the list is preferring skip over continue or next.
+     */
+    private static final String[] NEXT_WORDS = {
+            "[sS]kip", "SKIP",
+            "[fF]inish", "FINISH",
+            "[dD]one", "DONE",
+            "[nN]ext", "NEXT",
+            "[cC]ontinue", "CONTINUE",
+            "[pP]roceed", "PROCEED",
+            "[yY][eE][sS]", "[oO][kK]"};
+
+    /**
+     * {@link Pattern} to match for NEXT buttons.
+     */
+    private static final Pattern NEXT_BTN_PATTERN =
+            Pattern.compile(TextUtils.join("|", NEXT_WORDS));
+
+    /**
+     * Buttons with text matching {@link #NEXT_BTN_PATTERN}.
+     */
+    private static final BySelector NAVIGATION_BTN_TEXT_SELECTOR =
+            By.enabled(true)
+                    .checkable(false)
+                    .clickable(true)
+                    .text(NEXT_BTN_PATTERN);
+
+    /**
+     * Buttons with content description matching {@link #NEXT_BTN_PATTERN}.
+     */
+    private static final BySelector NAVIGATION_BTN_DESC_SELECTOR =
+            By.enabled(true)
+                    .checkable(false)
+                    .clickable(true)
+                    .desc(NEXT_BTN_PATTERN);
+
+    /**
+     * List of package names whose buttons should not be clicked.
+     */
+    private Set<String> mPackageNameBlacklist = null;
+
+    /**
+     * Stop skipping when any object with this {@link BySelector} is found.
+     */
+    private final BySelector mSkipEndSelector;
+
+    /**
+     * Creates a new page skipper.
+     *
+     * @param uiDevice {@link UiDevice} object to access UIAutomator API
+     * @param skipEndSelector stop skipping pages if this any object matching this
+     *                        {@link BySelector} is found
+     * @param testConfig {@link testConfig} to get test configurations
+     */
+    public PageSkipper(UiDevice uiDevice, BySelector skipEndSelector, TestConfig testConfig) {
+        super(uiDevice, testConfig);
+
+        mSkipEndSelector = skipEndSelector;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public boolean waitForLoading() throws Exception {
+        // Do nothing.
+        return true;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public BySelector uniqueElement() {
+        // No unique element.
+        return null;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void navigate() throws Exception {
+        long timeout =  TimeUnit.MINUTES.toMillis(1) * getTestConfig().getTimeoutSize();
+        Timer timer = new Timer(timeout);
+        List<OemWidget> oemWidgets = getTestConfig().getOemWidgets();
+        timer.start();
+        while (!timer.isTimeUp() && !getUiDevice().hasObject(mSkipEndSelector)) {
+
+            if (!clickAvailableNextButton()) {
+                iterateOemWidgets(oemWidgets);
+            }
+
+            // Assert on fatal app crash
+            assertOnFatalAppCrash();
+
+            SystemClock.sleep(THREAD_SLEEP_TIME_MS);
+        }
+
+        if (timer.isTimeUp()) {
+            Log.e(TAG, "Timeout");
+        }
+    }
+
+    /**
+     * Finds buttons with text or content description that match pattern {@link NEXT_BTN_PATTERN}
+     * and click on them.
+     *
+     * @return {@code true} if any button found, {@code false} otherwise
+     */
+    private boolean clickAvailableNextButton() {
+        return clickAnyButton(getUiDevice().findObjects(NAVIGATION_BTN_TEXT_SELECTOR))
+                || clickAnyButton(getUiDevice().findObjects(NAVIGATION_BTN_DESC_SELECTOR));
+    }
+
+    /**
+     * Clicks any button from given list.
+     *
+     * @param buttons list of buttons to click
+     * @return {@code true} if there is any button clicked successfully, {@code false} otherwise
+     */
+    private boolean clickAnyButton(List<UiObject2> navigationBtns) {
+        for (UiObject2 obj : navigationBtns) {
+            // Skip widget with package name in mPackageNameBlacklist.
+            String pkgName = WidgetUtils.getPackageName(obj);
+            if (mPackageNameBlacklist != null && mPackageNameBlacklist.contains(pkgName)) {
+                continue;
+            }
+
+            if (safeClick(obj)) {
+                // Returns immediately after the first 'Next' button is found and clicked; because
+                // after clicking the current view hierarchy will change; the found navigation
+                // buttons will be stale.
+                return true;
+            }
+        }
+
+        return false;
+    }
+
+    /**
+     * Iterates OEM widgets and act on found ones if necessary.
+     *
+     * @param oemWidgets {@link List} of {@link OemWidget} to operate
+     */
+    private void iterateOemWidgets(List<OemWidget> oemWidgets) {
+
+        for (OemWidget widget : oemWidgets) {
+            BySelector selector = BySelectorHelper.getSelector(widget);
+
+            if (getUiDevice().hasObject(selector)) {
+                Log.d(TAG, "Found OEM widget: " + widget.toString());
+
+                if (widget.getAction().equals(ACTION_CHECK)) {
+                    safeClick(getUiDevice().findObject(selector));
+                } else if (widget.getAction().equals(ACTION_SCROLL)){
+                    safeFling(
+                            getUiDevice().findObject(selector),
+                            Direction.valueOf(widget.getScrollDirection()));
+                } else if (widget.getAction().equals(ACTION_CLICK)) {
+                    safeClick(getUiDevice().findObject(selector));
+                }
+            }
+        }
+    }
+
+    /**
+     * Sets list of package names which should not be skipped.
+     *
+     * @param blacklist list of package names
+     */
+    public void setPackageNameBlacklist(Set<String> blacklist) {
+        mPackageNameBlacklist = blacklist;
+    }
+}
diff --git a/libs/UiAutomatorLib/src/com/android/afwtest/uiautomator/pages/UiPage.java b/libs/UiAutomatorLib/src/com/android/afwtest/uiautomator/pages/UiPage.java
new file mode 100644
index 0000000..f6b2532
--- /dev/null
+++ b/libs/UiAutomatorLib/src/com/android/afwtest/uiautomator/pages/UiPage.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.uiautomator.pages;
+
+import static com.android.afwtest.uiautomator.Constants.PROVISIONING_STATS_FILE;
+
+import android.support.test.uiautomator.BySelector;
+import android.support.test.uiautomator.UiDevice;
+import android.util.Log;
+
+import com.android.afwtest.common.Timer;
+import com.android.afwtest.common.test.StatsLogger;
+import com.android.afwtest.common.test.TestConfig;
+import com.android.afwtest.uiautomator.test.AfwTestUiWatcher;
+import com.android.afwtest.uiautomator.utils.WidgetUtils;
+
+import java.io.IOException;
+import java.util.concurrent.TimeUnit;
+
+
+/**
+ * Abstract class that represents the screen view of the device.
+ */
+public abstract class UiPage {
+
+    private static final String TAG = "afwtest.UiPage";
+
+    /**
+     * Default waiting for a {@link UiPage}.
+     */
+    private static final long DEFAULT_LOAD_TIMEOUT_MS = TimeUnit.MINUTES.toMillis(1);
+
+    /**
+     * Reference to {@link UiDevice} object.
+     */
+    private final UiDevice mUiDevice;
+
+    /**
+     * Test configuration.
+     */
+    private final TestConfig mTestConfig;
+
+    /**
+     * Constructor.
+     *
+     * @param uiDevice {@link UiDevice} object
+     * @param config {@link TestConfig} object holding test configurations
+     */
+    public UiPage(UiDevice uiDevice, TestConfig config) {
+        mUiDevice = uiDevice;
+        mTestConfig = config;
+    }
+
+    /**
+     * Gets {@link UiDevice}.
+     *
+     * @return {@link UiDevice} object
+     */
+    protected UiDevice getUiDevice() {
+        return mUiDevice;
+    }
+
+    /**
+     * Gets test configuration.
+     *
+     * @return {@link TestConfig} object
+     */
+    protected TestConfig getTestConfig() {
+        return mTestConfig;
+    }
+
+    /**
+     * Gets the page loading timeout.
+     *
+     * @return page loading timeout in millisecond
+     */
+    protected long getLoadingTimeoutInMs() throws IOException {
+        return DEFAULT_LOAD_TIMEOUT_MS * mTestConfig.getTimeoutSize();
+    }
+
+    /**
+     * Gets if this page optional, e.g. might not appear in some flow.
+     *
+     * @return {@code true} if this page can be optional, {@code} false otherwise
+     */
+    public boolean isOptional() {
+        return false;
+    }
+
+    /**
+     * Waits for this page to load.
+     *
+     * @return {@code true} if this page is loaded successful, {@code false} otherwise
+     */
+    public boolean waitForLoading() throws Exception {
+        Timer timer = new Timer(getLoadingTimeoutInMs());
+        timer.start();
+        do {
+            if (WidgetUtils.safeWait(getUiDevice(), uniqueElement()) != null) {
+                return true;
+            }
+            assertOnFatalAppCrash();
+        } while (!timer.isTimeUp());
+
+        Log.e(TAG, String.format("UiPage.waitForLoading timeout(%sms).", getLoadingTimeoutInMs()));
+
+        return false;
+    }
+
+    /**
+     * Asserts if fatal app crash is found.
+     */
+    protected void assertOnFatalAppCrash() {
+        String appCrashMsg = AfwTestUiWatcher.getFatalAppCrashMsg();
+        if (appCrashMsg != null) {
+            throw new RuntimeException(appCrashMsg);
+        }
+    }
+
+    /**
+     * Gets provisioning stats logger.
+     *
+     * @return {@link StatsLogger} object
+     */
+    protected StatsLogger getProvisioningStatsLogger() throws IOException {
+        return StatsLogger.getInstance(PROVISIONING_STATS_FILE);
+    }
+
+    /**
+     * Gets unique element of this page described by {@link BySelector}.
+     *
+     * <p>The returned element is used to identify this page, such as to determine if this page
+     * is visible. For example, {@link waitForLoading} function is using the returned element
+     * of this function to determine if this page has successfully loaded.
+     * </p>
+     *
+     * @return unique element of this page described by {@link BySelector}
+     */
+    public abstract BySelector uniqueElement();
+
+    /**
+     * Navigates this page properly to next page.
+     *
+     * @throws Exception if navigation failed
+     */
+    public abstract void navigate() throws Exception;
+}
diff --git a/libs/UiAutomatorLib/src/com/android/afwtest/uiautomator/pages/gms/AddAccountPage.java b/libs/UiAutomatorLib/src/com/android/afwtest/uiautomator/pages/gms/AddAccountPage.java
new file mode 100644
index 0000000..427d435
--- /dev/null
+++ b/libs/UiAutomatorLib/src/com/android/afwtest/uiautomator/pages/gms/AddAccountPage.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.uiautomator.pages.gms;
+
+import static com.android.afwtest.uiautomator.Constants.GMS_NEXT_BUTTON_SELECTOR;
+import static com.android.afwtest.uiautomator.Constants.GMS_PKG_NAME;
+import static com.android.afwtest.uiautomator.Constants.GMS_TEXT_FIELD_SELECTOR;
+import static java.util.regex.Pattern.CASE_INSENSITIVE;
+
+import android.support.test.uiautomator.By;
+import android.support.test.uiautomator.BySelector;
+import android.support.test.uiautomator.UiDevice;
+import com.android.afwtest.common.test.TestConfig;
+import com.android.afwtest.uiautomator.pages.UiPage;
+import com.android.afwtest.uiautomator.utils.TextField;
+import com.android.afwtest.uiautomator.utils.WidgetUtils;
+import java.util.concurrent.TimeUnit;
+import java.util.regex.Pattern;
+
+/**
+ * GMS Core add account page.
+ */
+public final class AddAccountPage extends UiPage {
+
+    /**
+     * @{@link BySelector} unique to the add or verify account page.
+     */
+    public static final BySelector GMS_ADD_OR_VERIFY_ACCOUNT_PAGE_SELECTOR =
+        By.pkg(GMS_PKG_NAME)
+            .desc(Pattern.compile("(Add|Verify) your account|Sign in", CASE_INSENSITIVE));
+
+    /**
+     * Default waiting time for widget, in milliseconds.
+     */
+    private static final long DEFAULT_TIMEOUT_MS = TimeUnit.SECONDS.toMillis(20);
+
+    /**
+     * Username of the account to add.
+     */
+    private final String mUsername;
+
+    /**
+     * Constructor.
+     *
+     * @param uiDevice {@link UiDevice} object
+     * @param config {@link TestConfig} object holding test configurations
+     */
+    public AddAccountPage(UiDevice uiDevice, TestConfig config) {
+        super(uiDevice, config);
+        mUsername = config.getWorkAccountUsername();
+    }
+
+    /**
+     * Constructor.
+     *
+     * @param uiDevice {@link UiDevice} object
+     * @param config {@link TestConfig} object holding test configurations
+     * @param customAccountKey property key of the account in {@link TestConfig}
+     */
+    public AddAccountPage(UiDevice uiDevice, TestConfig config, String customAccountKey) {
+        super(uiDevice, config);
+        mUsername = config.getProperty(customAccountKey);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    protected long getLoadingTimeoutInMs() {
+        // "Checking..." and "Loading" may take longer time than normal pages
+        return TimeUnit.MINUTES.toMillis(5);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public BySelector uniqueElement() {
+        return GMS_ADD_OR_VERIFY_ACCOUNT_PAGE_SELECTOR;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void navigate() throws Exception {
+        TextField.enterTextAndActivateNavigationBtn(
+                getUiDevice(),
+                GMS_TEXT_FIELD_SELECTOR,
+                mUsername,
+                GMS_NEXT_BUTTON_SELECTOR);
+
+        WidgetUtils.safeWaitAndClick(getUiDevice(), GMS_NEXT_BUTTON_SELECTOR, DEFAULT_TIMEOUT_MS);
+        WidgetUtils.waitToBeGone(getUiDevice(), GMS_ADD_OR_VERIFY_ACCOUNT_PAGE_SELECTOR);
+    }
+}
diff --git a/libs/UiAutomatorLib/src/com/android/afwtest/uiautomator/pages/gms/BackUpToDrivePage.java b/libs/UiAutomatorLib/src/com/android/afwtest/uiautomator/pages/gms/BackUpToDrivePage.java
new file mode 100644
index 0000000..0ae2025
--- /dev/null
+++ b/libs/UiAutomatorLib/src/com/android/afwtest/uiautomator/pages/gms/BackUpToDrivePage.java
@@ -0,0 +1,72 @@
+package com.android.afwtest.uiautomator.pages.gms;
+
+import static com.android.afwtest.uiautomator.Constants.GMS_BTN_WITH_TEXT_OK;
+import static com.android.afwtest.uiautomator.Constants.GMS_PKG_NAME_REGEX;
+
+import android.support.test.uiautomator.By;
+import android.support.test.uiautomator.BySelector;
+import android.support.test.uiautomator.UiDevice;
+import com.android.afwtest.common.test.TestConfig;
+import com.android.afwtest.uiautomator.pages.UiPage;
+import com.android.afwtest.uiautomator.utils.WidgetUtils;
+import java.util.concurrent.TimeUnit;
+import java.util.regex.Pattern;
+
+
+/**
+ * Back up to Google Drive Page, optional.
+ */
+public final class BackUpToDrivePage extends UiPage {
+
+    /**
+     * Turn off back up to Google drive button.
+     */
+    private static final BySelector TURN_OFF_BACK_UP_BTN =
+            By.res(Pattern.compile(GMS_PKG_NAME_REGEX + ":id/backup_opt_in_disable_backup",
+                    Pattern.CASE_INSENSITIVE));
+
+    /**
+     * Constructor.
+     *
+     * @param uiDevice {@link UiDevice} object
+     * @param config {@link TestConfig} object holding test configurations
+     */
+    public BackUpToDrivePage(UiDevice uiDevice, TestConfig config) {
+        super(uiDevice, config);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public BySelector uniqueElement() {
+        return TURN_OFF_BACK_UP_BTN;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    protected long getLoadingTimeoutInMs() {
+        return TimeUnit.MINUTES.toMillis(1);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public boolean isOptional() {
+        // This page is optional.
+        return true;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void navigate() throws Exception {
+        WidgetUtils.waitAndClick(getUiDevice(), TURN_OFF_BACK_UP_BTN);
+        WidgetUtils.waitToBeGone(getUiDevice(), TURN_OFF_BACK_UP_BTN);
+        WidgetUtils.waitAndClick(getUiDevice(), GMS_BTN_WITH_TEXT_OK);
+    }
+}
diff --git a/libs/UiAutomatorLib/src/com/android/afwtest/uiautomator/pages/gms/EnterPasswordPage.java b/libs/UiAutomatorLib/src/com/android/afwtest/uiautomator/pages/gms/EnterPasswordPage.java
new file mode 100644
index 0000000..96e7dcd
--- /dev/null
+++ b/libs/UiAutomatorLib/src/com/android/afwtest/uiautomator/pages/gms/EnterPasswordPage.java
@@ -0,0 +1,116 @@
+/*
+ * 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.uiautomator.pages.gms;
+
+import static com.android.afwtest.uiautomator.Constants.GMS_NEXT_BUTTON_SELECTOR;
+import static com.android.afwtest.uiautomator.Constants.GMS_PKG_NAME;
+import static com.android.afwtest.uiautomator.Constants.GMS_TEXT_FIELD_SELECTOR;
+
+import android.os.SystemClock;
+import android.support.test.uiautomator.By;
+import android.support.test.uiautomator.BySelector;
+import android.support.test.uiautomator.UiDevice;
+import android.view.KeyCharacterMap;
+import android.view.KeyEvent;
+import com.android.afwtest.common.test.TestConfig;
+import com.android.afwtest.uiautomator.pages.UiPage;
+import com.android.afwtest.uiautomator.utils.WidgetUtils;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * GMS Core auth enter password page.
+ */
+public final class EnterPasswordPage extends UiPage {
+
+    /**
+     * {@link BySelector} unique to this page.
+     */
+    private static final BySelector ENTER_PASSWORD_PAGE_SELECTOR =
+        By.pkg(GMS_PKG_NAME).desc("Forgot password?");
+
+    /**
+     * Password of the account to add.
+     */
+    private final String mPassword;
+
+    /**
+     * Constructor.
+     *
+     * @param uiDevice {@link UiDevice} object
+     * @param config {@link TestConfig} object holding test configurations
+     */
+    public EnterPasswordPage(UiDevice uiDevice, TestConfig config) {
+        super(uiDevice, config);
+        mPassword = config.getWorkAccountPassword();
+    }
+
+    /**
+     * Constructor.
+     *
+     * @param uiDevice {@link UiDevice} object
+     * @param config {@link TestConfig} object holding test configurations
+     * @param customPasswordKey property key of the password in {@link TestConfig}
+     */
+    public EnterPasswordPage(UiDevice uiDevice, TestConfig config, String customPasswordKey) {
+        super(uiDevice, config);
+        mPassword = config.getProperty(customPasswordKey);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    protected long getLoadingTimeoutInMs() {
+        // Accessing Google server takes long time
+        return TimeUnit.MINUTES.toMillis(5);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public BySelector uniqueElement() {
+        return ENTER_PASSWORD_PAGE_SELECTOR;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void navigate() throws Exception {
+        WidgetUtils.waitAndClick(getUiDevice(),
+                GMS_TEXT_FIELD_SELECTOR,
+                TimeUnit.SECONDS.toMillis(5));
+        // Clicking the text field will bring up the keyboard and change the view hierachy of
+        // current screen; textField may become stale. Try to find it again before simulating
+        // the keyboard events.
+        SystemClock.sleep(TimeUnit.SECONDS.toMillis(5));
+
+        //Enter password by key event
+        KeyCharacterMap map = KeyCharacterMap.load(KeyCharacterMap.VIRTUAL_KEYBOARD);
+        KeyEvent[] events = map.getEvents(mPassword.toCharArray());
+        for (KeyEvent event : events) {
+            if (event.getAction() == KeyEvent.ACTION_DOWN) {
+                getUiDevice().pressKeyCode(event.getKeyCode(), event.getMetaState());
+            }
+        }
+
+        WidgetUtils.waitAndClick(getUiDevice(), GMS_NEXT_BUTTON_SELECTOR);
+        // Wait until the next button is gone.
+        WidgetUtils.waitToBeGone(getUiDevice(), GMS_NEXT_BUTTON_SELECTOR);
+    }
+}
diff --git a/libs/UiAutomatorLib/src/com/android/afwtest/uiautomator/pages/gms/GoogleAppsDevicePolicyPage.java b/libs/UiAutomatorLib/src/com/android/afwtest/uiautomator/pages/gms/GoogleAppsDevicePolicyPage.java
new file mode 100644
index 0000000..b5f572c
--- /dev/null
+++ b/libs/UiAutomatorLib/src/com/android/afwtest/uiautomator/pages/gms/GoogleAppsDevicePolicyPage.java
@@ -0,0 +1,122 @@
+/*
+ * 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.uiautomator.pages.gms;
+
+import static com.android.afwtest.uiautomator.Constants.GMS_DOWNLOADING_DIALOG_SELECTOR;
+import static com.android.afwtest.uiautomator.Constants.GMS_NEXT_BUTTON_RES_SELECTOR;
+import static com.android.afwtest.uiautomator.Constants.GMS_PKG_NAME_REGEX;
+
+import android.os.SystemClock;
+import android.support.test.uiautomator.By;
+import android.support.test.uiautomator.BySelector;
+import android.support.test.uiautomator.UiDevice;
+import com.android.afwtest.common.Timer;
+import com.android.afwtest.common.test.TestConfig;
+import com.android.afwtest.uiautomator.pages.UiPage;
+import com.android.afwtest.uiautomator.utils.WidgetUtils;
+import java.util.concurrent.TimeUnit;
+import java.util.regex.Pattern;
+
+/**
+ * GMS Google Apps device policy page.
+ */
+public final class GoogleAppsDevicePolicyPage extends UiPage {
+
+    /**
+     * Timeout for starting downloading mdm.
+     */
+    private static final long START_DOWNLOAD_MDM_TIMEOUT_MS = TimeUnit.SECONDS.toMillis(5);
+
+    /**
+     * Timeout for downloading the dmd.
+     */
+    private static final long DOWNLOAD_MDM_TIMEOUT_MS = TimeUnit.MINUTES.toMillis(1);
+
+    /**
+     * Number of attempts to start downloading MDM.
+     *
+     * <p>
+     * It's found on some devices it takes some time for the MDM app icon to load;
+     * and before the icon is loaded, clicking the Next button doesn't work.
+     * Add retry logic to keep clicking until the downloading starts.
+     * </p>
+     */
+    private static final int CLICK_NEXT_BTN_ATTEMPTS = 5;
+
+    /**
+     * {@link BySelector} for the MDM app icon.
+     */
+    private static final BySelector MDM_ICON_SELECTOR =
+            By.res(Pattern.compile(GMS_PKG_NAME_REGEX +
+                    ":id/auth_device_management_download_app_icon"));
+
+    /**
+     * Constructor.
+     *
+     * @param uiDevice {@link UiDevice} object
+     * @param config {@link TestConfig} object holding test configurations
+     */
+    public GoogleAppsDevicePolicyPage(UiDevice uiDevice, TestConfig config) {
+        super(uiDevice, config);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public BySelector uniqueElement() {
+        return MDM_ICON_SELECTOR;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    protected long getLoadingTimeoutInMs() {
+        // Accessing Google server takes long time
+        return TimeUnit.MINUTES.toMillis(5);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void navigate() throws Exception {
+        // Clicks the Next button until downloading starts.
+        int attempts = CLICK_NEXT_BTN_ATTEMPTS;
+        while (getUiDevice().hasObject(GMS_NEXT_BUTTON_RES_SELECTOR) && attempts > 0) {
+
+            --attempts;
+
+            // Clicks the next button to start downloading mdm
+            WidgetUtils.safeClick(getUiDevice().findObject(GMS_NEXT_BUTTON_RES_SELECTOR));
+
+            // Wait for downloading mdm to appear
+            if (WidgetUtils.safeWait(getUiDevice(),
+                    GMS_DOWNLOADING_DIALOG_SELECTOR, START_DOWNLOAD_MDM_TIMEOUT_MS) != null) {
+                break;
+            }
+        }
+
+        // Waits until the download finishes
+        Timer timer = new Timer(DOWNLOAD_MDM_TIMEOUT_MS);
+        timer.start();
+        while (!timer.isTimeUp() && getUiDevice().hasObject(GMS_DOWNLOADING_DIALOG_SELECTOR)) {
+            SystemClock.sleep(TimeUnit.SECONDS.toMillis(2));
+        }
+    }
+}
diff --git a/libs/UiAutomatorLib/src/com/android/afwtest/uiautomator/pages/gms/GoogleServicesPage.java b/libs/UiAutomatorLib/src/com/android/afwtest/uiautomator/pages/gms/GoogleServicesPage.java
new file mode 100644
index 0000000..f3cb453
--- /dev/null
+++ b/libs/UiAutomatorLib/src/com/android/afwtest/uiautomator/pages/gms/GoogleServicesPage.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.uiautomator.pages.gms;
+
+import static com.android.afwtest.uiautomator.Constants.GMS_PKG_NAME_REGEX;
+import static java.util.regex.Pattern.CASE_INSENSITIVE;
+
+import android.support.test.uiautomator.By;
+import android.support.test.uiautomator.BySelector;
+import android.support.test.uiautomator.UiDevice;
+import com.android.afwtest.common.test.TestConfig;
+import com.android.afwtest.uiautomator.pages.UiPage;
+import com.android.afwtest.uiautomator.test.AfwTestUiWatcher;
+import com.android.afwtest.uiautomator.utils.Device;
+import com.android.afwtest.uiautomator.utils.WidgetUtils;
+import java.util.concurrent.TimeUnit;
+import java.util.regex.Pattern;
+
+/**
+ * GMS Core Google Services page.
+ */
+public final class GoogleServicesPage extends UiPage {
+
+    /**
+     * {@link BySelector} unique to this page.
+     */
+    private static final BySelector GOOGLE_SERVICES_PAGE_SELECTOR =
+            By.pkg(Pattern.compile(GMS_PKG_NAME_REGEX)).text("Google services");
+
+    private static final BySelector GMS_BTN_WITH_TEXT_AGREE =
+        By.pkg(Pattern.compile(GMS_PKG_NAME_REGEX))
+            .text(Pattern.compile("agree", CASE_INSENSITIVE));
+
+    /**
+     * Constructor.
+     *
+     * @param uiDevice {@link UiDevice} object
+     * @param config {@link TestConfig} object holding test configurations
+     */
+    public GoogleServicesPage(UiDevice uiDevice, TestConfig config) {
+        super(uiDevice, config);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    protected long getLoadingTimeoutInMs() {
+        // Accessing Google server takes long time
+        return TimeUnit.MINUTES.toMillis(5);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public boolean isOptional() {
+        // This page is optional.
+        return true;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public BySelector uniqueElement() {
+        return GOOGLE_SERVICES_PAGE_SELECTOR;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void navigate() throws Exception {
+        // Otherwise "Agree" will be clicked automatically.
+        AfwTestUiWatcher.disallowPageSkipping();
+
+        // Scroll up until "more" button becomes "agree".
+        for (int i = 0; i < 5; ++i) {
+            Device.swipeUp(getUiDevice());
+            if (WidgetUtils.safeWait(getUiDevice(), GMS_BTN_WITH_TEXT_AGREE,
+                    TimeUnit.SECONDS.toMillis(3)) != null) {
+                break;
+            }
+        }
+
+        if (!WidgetUtils.safeWaitAndClick(getUiDevice(), GMS_BTN_WITH_TEXT_AGREE)) {
+            throw new Exception("Failed to click Agree button");
+        }
+
+        AfwTestUiWatcher.allowPageSkipping();
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public boolean waitForLoading() throws Exception {
+        AfwTestUiWatcher.disallowPageSkipping();
+        boolean result = super.waitForLoading();
+        AfwTestUiWatcher.allowPageSkipping();
+        return result;
+    }
+}
diff --git a/libs/UiAutomatorLib/src/com/android/afwtest/uiautomator/pages/managedprovisioning/BasePage.java b/libs/UiAutomatorLib/src/com/android/afwtest/uiautomator/pages/managedprovisioning/BasePage.java
new file mode 100644
index 0000000..1e249ce
--- /dev/null
+++ b/libs/UiAutomatorLib/src/com/android/afwtest/uiautomator/pages/managedprovisioning/BasePage.java
@@ -0,0 +1,122 @@
+/*
+ * 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.uiautomator.pages.managedprovisioning;
+
+import static com.android.afwtest.uiautomator.Constants.MANAGED_PROVISIONING_ERROR_MSG_SELECTOR;
+import static com.android.afwtest.uiautomator.Constants.MANAGED_PROVISIONING_OK_BUTTON_SELECTOR;
+import static com.android.afwtest.uiautomator.Constants.MANAGED_PROVISIONING_PKG_NAME;
+import static com.android.afwtest.uiautomator.Constants.MANAGED_PROVISIONING_PKG_SELECTOR;
+
+import android.os.SystemClock;
+import android.support.test.uiautomator.By;
+import android.support.test.uiautomator.BySelector;
+import android.support.test.uiautomator.UiDevice;
+
+import com.android.afwtest.common.Timer;
+import com.android.afwtest.common.test.TestConfig;
+import com.android.afwtest.uiautomator.pages.UiPage;
+import com.android.afwtest.uiautomator.utils.WidgetUtils;
+
+import java.io.IOException;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Base class for all Managed Provisioning pages to keep common functionality in one place.
+ */
+public abstract class BasePage extends UiPage {
+
+    /**
+     * Thread sleep time, in milliseconds.
+     */
+    protected static final long THREAD_SLEEP_TIME_MS = TimeUnit.SECONDS.toMillis(1);
+
+    private static final BySelector LEARN_MORE_TEXT_1 =
+            By.res(MANAGED_PROVISIONING_PKG_NAME, "learn_more_text1");
+
+    /**
+     * Default provisioning timeout in milliseconds.
+     */
+    private static final long DEFAULT_PROVISIONING_TIMEOUT_MS = TimeUnit.MINUTES.toMillis(1);
+
+    /**
+     * Constructor.
+     *
+     * @param uiDevice {@link UiDevice} object
+     * @param config {@link TestConfig} object holding test configurations
+     */
+    public BasePage(UiDevice uiDevice, TestConfig config) {
+        super(uiDevice, config);
+    }
+
+    /**
+     * Gets provisioning error message.
+     *
+     * @return Error message if provisioning is showing error dialog, {@code null} otherwise
+     */
+    protected String getProvisioningError() {
+        // Get any managed provisioning error dialog
+        if (getUiDevice().hasObject(MANAGED_PROVISIONING_ERROR_MSG_SELECTOR)) {
+            return getUiDevice().findObject(MANAGED_PROVISIONING_ERROR_MSG_SELECTOR).getText();
+        }
+
+        return null;
+    }
+
+    /**
+     * Gets the provisioning timeout in millisecond.
+     *
+     * @return provisioning timeout in millisecond
+     */
+    protected long getProvisioningTimeoutInMs() throws IOException {
+        return DEFAULT_PROVISIONING_TIMEOUT_MS * getTestConfig().getTimeoutSize();
+    }
+
+    /**
+     * Waits for the provisioning to finish.
+     *
+     * @param timeout Waiting timeout in milliseconds
+     * @return {@code true} if the provisioning is finished, {@code false} otherwise
+     */
+    protected boolean waitForProvisioningToFinish() throws Exception {
+        Timer timer = new Timer(getProvisioningTimeoutInMs());
+        timer.start();
+        while (!timer.isTimeUp()) {
+            if (WidgetUtils.safeWait(getUiDevice(), MANAGED_PROVISIONING_PKG_SELECTOR) == null) {
+                assertOnFatalAppCrash();
+                return true;
+            }
+
+            // Dismiss the dialog with: "Your administrator has the ability to monitor...".
+            if (getUiDevice().hasObject(LEARN_MORE_TEXT_1)) {
+                getUiDevice().findObject(MANAGED_PROVISIONING_OK_BUTTON_SELECTOR).click();
+                SystemClock.sleep(THREAD_SLEEP_TIME_MS);
+                continue;
+            }
+
+            String errorMsg = getProvisioningError();
+            if (errorMsg != null) {
+                throw new RuntimeException("Provisioning error: " + errorMsg);
+            }
+
+            assertOnFatalAppCrash();
+
+            SystemClock.sleep(THREAD_SLEEP_TIME_MS);
+        }
+
+        return false;
+    }
+}
diff --git a/libs/UiAutomatorLib/src/com/android/afwtest/uiautomator/pages/managedprovisioning/ErrorPage.java b/libs/UiAutomatorLib/src/com/android/afwtest/uiautomator/pages/managedprovisioning/ErrorPage.java
new file mode 100644
index 0000000..7fc55f7
--- /dev/null
+++ b/libs/UiAutomatorLib/src/com/android/afwtest/uiautomator/pages/managedprovisioning/ErrorPage.java
@@ -0,0 +1,74 @@
+/*
+ * 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.uiautomator.pages.managedprovisioning;
+
+import static com.android.afwtest.uiautomator.Constants.MANAGED_PROVISIONING_PKG_NAME;
+
+import android.support.test.uiautomator.By;
+import android.support.test.uiautomator.BySelector;
+import android.support.test.uiautomator.UiDevice;
+
+import com.android.afwtest.common.test.TestConfig;
+import com.android.afwtest.uiautomator.utils.WidgetUtils;
+
+import java.util.concurrent.TimeUnit;
+import java.util.regex.Pattern;
+
+/**
+ * A {@link UiPage} representing error dialog popped up from Managed Provisioning.
+ */
+public class ErrorPage extends BasePage {
+
+    /**
+     * Default UI waiting time, in milliseconds.
+     */
+    private static final long DEFAULT_TIMEOUT_MS = TimeUnit.SECONDS.toMillis(10);
+
+    private static final BySelector ERROR_PAGE_SELECTOR =
+            By.pkg(MANAGED_PROVISIONING_PKG_NAME).text("Can't set up device");
+
+    private static final BySelector OK_BUTTON_SELECTOR =
+            By.pkg(MANAGED_PROVISIONING_PKG_NAME)
+                   .text(Pattern.compile("[Oo][Kk]"))
+                   .clickable(true);
+
+    /**
+     * Constructor.
+     *
+     * @param uiDevice {@link UiDevice} object
+     * @param config {@link TestConfig} object holding test configurations
+     */
+    public ErrorPage(UiDevice uiDevice, TestConfig config) {
+        super(uiDevice, config);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public BySelector uniqueElement() {
+        return ERROR_PAGE_SELECTOR;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void navigate() throws Exception {
+        WidgetUtils.waitAndClick(getUiDevice(), OK_BUTTON_SELECTOR, DEFAULT_TIMEOUT_MS);
+    }
+}
diff --git a/libs/UiAutomatorLib/src/com/android/afwtest/uiautomator/pages/managedprovisioning/SetupYourDevicePage.java b/libs/UiAutomatorLib/src/com/android/afwtest/uiautomator/pages/managedprovisioning/SetupYourDevicePage.java
new file mode 100644
index 0000000..d10e281
--- /dev/null
+++ b/libs/UiAutomatorLib/src/com/android/afwtest/uiautomator/pages/managedprovisioning/SetupYourDevicePage.java
@@ -0,0 +1,113 @@
+/*
+ * 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.uiautomator.pages.managedprovisioning;
+
+import static com.android.afwtest.uiautomator.Constants.MANAGED_PROVISIONING_PKG_NAME;
+import static com.android.afwtest.uiautomator.Constants.MANAGED_PROVISIONING_SCROLL_VIEW_SELECTOR;
+import static com.android.afwtest.uiautomator.Constants.MANAGED_PROVISIONING_SETUP_BUTTON_SELECTOR;
+import static com.android.afwtest.uiautomator.Constants.STAT_TESTDPC_WORK_PROFILE_CREATION_TIME;
+import static java.util.regex.Pattern.CASE_INSENSITIVE;
+
+import android.support.test.uiautomator.By;
+import android.support.test.uiautomator.BySelector;
+import android.support.test.uiautomator.UiDevice;
+import android.widget.CheckBox;
+
+import com.android.afwtest.common.test.TestConfig;
+import com.android.afwtest.uiautomator.utils.WidgetUtils;
+import java.util.concurrent.TimeUnit;
+import java.util.regex.Pattern;
+
+/**
+ * Managed Provisioning setup your device page.
+ */
+public class SetupYourDevicePage extends BasePage {
+
+    /**
+     * Default UI waiting time, in milliseconds.
+     */
+    private static final long DEFAULT_TIMEOUT_MS = TimeUnit.SECONDS.toMillis(20);
+
+    /**
+     * {@link BySelector} unique to this page.
+     */
+    private static final BySelector SET_UP_YOUR_DEVICE_PAGE_SELECTOR =
+            By.pkg(MANAGED_PROVISIONING_PKG_NAME)
+                    .text(Pattern.compile("Set up .* device", CASE_INSENSITIVE));
+
+    private static final BySelector LEARN_MORE_TEXT_1 =
+            By.res(MANAGED_PROVISIONING_PKG_NAME, "learn_more_text1");
+
+    /**
+     * {@link BySelector} for {@link CheckBox} with resource-id user_consent_checkbox on
+     * base ManagedProvisioning dialog.
+     */
+    private static final BySelector MANAGED_PROVISIONING_CONSENT_CHECKBOX_SELECTOR =
+            By.res(MANAGED_PROVISIONING_PKG_NAME, "user_consent_checkbox")
+                    .clazz(CheckBox.class.getName())
+                    .checkable(true);
+
+    /**
+     * Constructor.
+     *
+     * @param uiDevice {@link UiDevice} object
+     * @param config {@link TestConfig} object holding test configurations
+     */
+    public SetupYourDevicePage(UiDevice uiDevice, TestConfig config) {
+        super(uiDevice, config);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public BySelector uniqueElement() {
+        return SET_UP_YOUR_DEVICE_PAGE_SELECTOR;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void navigate() throws Exception {
+        WidgetUtils.waitAndClick(getUiDevice(), MANAGED_PROVISIONING_SETUP_BUTTON_SELECTOR,
+                DEFAULT_TIMEOUT_MS);
+        if (null != WidgetUtils.safeWait(getUiDevice(), LEARN_MORE_TEXT_1)) {
+            WidgetUtils.scrollToItem(getUiDevice(),
+                    MANAGED_PROVISIONING_SCROLL_VIEW_SELECTOR,
+                    MANAGED_PROVISIONING_CONSENT_CHECKBOX_SELECTOR);
+            WidgetUtils.waitAndClick(getUiDevice(),
+                    MANAGED_PROVISIONING_CONSENT_CHECKBOX_SELECTOR,
+                    DEFAULT_TIMEOUT_MS);
+        }
+
+        onProvisioningStarted();
+
+        // Wait for the provisioning to finish.
+        if (!waitForProvisioningToFinish()) {
+            throw new RuntimeException("DO Provisioning timeout");
+        }
+    }
+
+    /**
+     * Handles provisioning started event.
+     */
+    protected void onProvisioningStarted() throws Exception {
+        // Default to be TestDpc provisioning
+        getProvisioningStatsLogger().startTime(STAT_TESTDPC_WORK_PROFILE_CREATION_TIME);
+    }
+}
diff --git a/libs/UiAutomatorLib/src/com/android/afwtest/uiautomator/pages/managedprovisioning/TermsPage.java b/libs/UiAutomatorLib/src/com/android/afwtest/uiautomator/pages/managedprovisioning/TermsPage.java
new file mode 100644
index 0000000..d595499
--- /dev/null
+++ b/libs/UiAutomatorLib/src/com/android/afwtest/uiautomator/pages/managedprovisioning/TermsPage.java
@@ -0,0 +1,81 @@
+/*
+ * 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.uiautomator.pages.managedprovisioning;
+
+import static com.android.afwtest.uiautomator.Constants.MANAGED_PROVISIONING_PKG_NAME;
+import static com.android.afwtest.uiautomator.Constants.MANAGED_PROVISIONING_SETUP_BUTTON_SELECTOR;
+import static com.android.afwtest.uiautomator.Constants.STAT_TESTDPC_WORK_PROFILE_CREATION_TIME;
+
+import android.support.test.uiautomator.By;
+import android.support.test.uiautomator.BySelector;
+import android.support.test.uiautomator.UiDevice;
+
+import com.android.afwtest.common.test.TestConfig;
+import com.android.afwtest.uiautomator.utils.WidgetUtils;
+
+import java.util.concurrent.TimeUnit;
+import java.util.regex.Pattern;
+
+/**
+ * Managed Provisioning terms and conditions page.
+ */
+public class TermsPage extends BasePage {
+
+    private static final long DEFAULT_TIMEOUT_MS = TimeUnit.SECONDS.toMillis(20);
+
+    /**
+     * Constructor.
+     *
+     * @param uiDevice {@link UiDevice} object
+     * @param config {@link TestConfig} object holding test configurations
+     */
+    public TermsPage(UiDevice uiDevice, TestConfig config) {
+        super(uiDevice, config);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public BySelector uniqueElement() {
+        return MANAGED_PROVISIONING_SETUP_BUTTON_SELECTOR;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void navigate() throws Exception {
+        WidgetUtils.waitAndClick(getUiDevice(), MANAGED_PROVISIONING_SETUP_BUTTON_SELECTOR,
+                DEFAULT_TIMEOUT_MS);
+
+        onProvisioningStarted();
+
+        // Wait for the provisioning to finish.
+        if (!waitForProvisioningToFinish()) {
+            throw new RuntimeException("PO Provisioning timeout");
+        }
+    }
+
+    /**
+     * Handles provisioning started event.
+     */
+    protected void onProvisioningStarted() throws Exception {
+        // Default to be TestDpc provisioning
+        getProvisioningStatsLogger().startTime(STAT_TESTDPC_WORK_PROFILE_CREATION_TIME);
+    }
+}
diff --git a/libs/UiAutomatorLib/src/com/android/afwtest/uiautomator/pages/packageinstaller/DeviceAccessPage.java b/libs/UiAutomatorLib/src/com/android/afwtest/uiautomator/pages/packageinstaller/DeviceAccessPage.java
new file mode 100644
index 0000000..3c1f930
--- /dev/null
+++ b/libs/UiAutomatorLib/src/com/android/afwtest/uiautomator/pages/packageinstaller/DeviceAccessPage.java
@@ -0,0 +1,59 @@
+/*
+ * 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.uiautomator.pages.packageinstaller;
+
+import static com.android.afwtest.uiautomator.Constants.PACKAGE_INSTALLER_INSTALL_BUTTON_SELECTOR;
+
+import android.support.test.uiautomator.BySelector;
+import android.support.test.uiautomator.UiDevice;
+
+import com.android.afwtest.common.test.TestConfig;
+import com.android.afwtest.uiautomator.pages.UiPage;
+import com.android.afwtest.uiautomator.utils.WidgetUtils;
+
+/**
+ * Package Installer device access page.
+ */
+public final class DeviceAccessPage extends UiPage {
+
+    /**
+     * Constructor.
+     *
+     * @param uiDevice {@link UiDevice} object
+     * @param config {@link TestConfig} object holding test configurations
+     */
+    public DeviceAccessPage(UiDevice uiDevice, TestConfig config) {
+        super(uiDevice, config);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public BySelector uniqueElement() {
+        return PACKAGE_INSTALLER_INSTALL_BUTTON_SELECTOR;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void navigate() throws Exception {
+        WidgetUtils.waitAndClick(getUiDevice(), PACKAGE_INSTALLER_INSTALL_BUTTON_SELECTOR);
+    }
+}
+
diff --git a/libs/UiAutomatorLib/src/com/android/afwtest/uiautomator/pages/testdpc/FinishSetupPage.java b/libs/UiAutomatorLib/src/com/android/afwtest/uiautomator/pages/testdpc/FinishSetupPage.java
new file mode 100644
index 0000000..4b266a1
--- /dev/null
+++ b/libs/UiAutomatorLib/src/com/android/afwtest/uiautomator/pages/testdpc/FinishSetupPage.java
@@ -0,0 +1,133 @@
+/*
+ * 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.uiautomator.pages.testdpc;
+
+import static com.android.afwtest.uiautomator.Constants.STAT_TESTDPC_WORK_PROFILE_CREATION_TIME;
+import static com.android.afwtest.uiautomator.Constants.TESTDPC_NEXT_BUTTON_SELECTOR;
+import static com.android.afwtest.uiautomator.Constants.TESTDPC_PKG_NAME;
+
+import android.support.test.uiautomator.By;
+import android.support.test.uiautomator.BySelector;
+import android.support.test.uiautomator.UiDevice;
+import android.support.test.uiautomator.UiObject2;
+import android.view.View;
+import android.widget.Button;
+
+import com.android.afwtest.common.test.TestConfig;
+import com.android.afwtest.uiautomator.pages.UiPage;
+import com.android.afwtest.uiautomator.utils.WidgetUtils;
+
+import java.util.regex.Pattern;
+
+/**
+ * Finish Setup page, which should be launched after DO or PO provisioning is successful.
+ */
+public final class FinishSetupPage extends UiPage {
+
+    /**
+     * Flag indicating if account was migrated.
+     */
+    private final boolean mIsAccountMigrated;
+
+    /**
+     * {@link BySelector} for TestDpc SuW layout title "Finish setup".
+     */
+    private static final BySelector TESTDPC_FINISH_SETUP_PAGE_FINISH_BUTTON_SELECTOR =
+            By.pkg(TESTDPC_PKG_NAME)
+                .clazz(Pattern.compile(Button.class.getName() + "|" + View.class.getName()))
+                .text("FINISH")
+                .enabled(true);
+
+    /**
+     * Constructor.
+     *
+     * @param uiDevice {@link UiDevice} object
+     * @param config {@link TestConfig} object holding test configurations
+     * @param isAccountMigrated whether account was migrated
+     */
+    public FinishSetupPage(UiDevice uiDevice, TestConfig config, boolean isAccountMigrated) {
+        super(uiDevice, config);
+        mIsAccountMigrated = isAccountMigrated;
+    }
+
+    /**
+     * Constructor.
+     *
+     * @param uiDevice {@link UiDevice} object
+     * @param config {@link TestConfig} object holding test configurations
+     */
+    public FinishSetupPage(UiDevice uiDevice, TestConfig config) {
+        this(uiDevice, config, true);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public BySelector uniqueElement() {
+        return TESTDPC_FINISH_SETUP_PAGE_FINISH_BUTTON_SELECTOR;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void navigate() throws Exception {
+        if (mIsAccountMigrated) {
+            // Find message that account migration was successful.
+            BySelector succeedResult =
+                    By.pkg(TESTDPC_PKG_NAME).text("Added account that is now managed:");
+
+            // Find managed work account name to verify required account is migrated.
+            BySelector managedAccount =
+                    By.res(TESTDPC_PKG_NAME, "managed_account_name")
+                            .text(getTestConfig().getWorkAccountUsername().toLowerCase());
+
+            if (WidgetUtils.safeWait(getUiDevice(), succeedResult) == null ||
+                    WidgetUtils.safeWait(getUiDevice(), managedAccount) == null) {
+                throw new RuntimeException(String.format(
+                        "Provisioning failed: %s is not setup to be managed by TestDpc!",
+                         getTestConfig().getWorkAccountUsername()));
+            }
+
+            // Find page title to verify its text displays successful setup
+            BySelector titleSelector = By.res(TESTDPC_PKG_NAME, "suw_layout_title");
+            UiObject2 titleWidget = WidgetUtils.safeWait(getUiDevice(), titleSelector);
+            if (titleWidget == null) {
+                throw new RuntimeException("Provisioning failed: Could not find page title!");
+            }
+            if (!titleWidget.getText().equals("Finish setup")) {
+                throw new RuntimeException(
+                    String.format(
+                        "Provisioning failed: Unexpected page title: %s",
+                        titleWidget.getText()
+                    ));
+            }
+
+            // Save Time metric
+            getProvisioningStatsLogger().stopTime(STAT_TESTDPC_WORK_PROFILE_CREATION_TIME);
+            getProvisioningStatsLogger().writeStatsToFile();
+
+        } else {
+            BySelector succeedResult = By.pkg(TESTDPC_PKG_NAME)
+                    .text("To manage the new managed profile, visit the badged version of this");
+            WidgetUtils.safeWait(getUiDevice(), succeedResult);
+        }
+
+        WidgetUtils.waitAndClick(getUiDevice(), TESTDPC_NEXT_BUTTON_SELECTOR);
+    }
+}
diff --git a/libs/UiAutomatorLib/src/com/android/afwtest/uiautomator/pages/testdpc/SetupFinishedPage.java b/libs/UiAutomatorLib/src/com/android/afwtest/uiautomator/pages/testdpc/SetupFinishedPage.java
new file mode 100644
index 0000000..e347dfa
--- /dev/null
+++ b/libs/UiAutomatorLib/src/com/android/afwtest/uiautomator/pages/testdpc/SetupFinishedPage.java
@@ -0,0 +1,122 @@
+/*
+ * 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.uiautomator.pages.testdpc;
+
+import static com.android.afwtest.uiautomator.Constants.STAT_TESTDPC_WORK_PROFILE_CREATION_TIME;
+import static com.android.afwtest.uiautomator.Constants.TESTDPC_NEXT_BUTTON_SELECTOR;
+import static com.android.afwtest.uiautomator.Constants.TESTDPC_PKG_NAME;
+
+import android.support.test.uiautomator.By;
+import android.support.test.uiautomator.BySelector;
+import android.support.test.uiautomator.UiDevice;
+import android.widget.RadioButton;
+
+import com.android.afwtest.common.test.TestConfig;
+import com.android.afwtest.uiautomator.pages.UiPage;
+import com.android.afwtest.uiautomator.utils.WidgetUtils;
+
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Setup finished page, with 3 options: add account, add account with name, and Skip.
+ */
+public final class SetupFinishedPage extends UiPage {
+    /** Options for next step.*/
+    public enum OPTION {
+        DO_NOTHING,
+        ADD_ACCOUNT,
+        ADD_ACCOUNT_WITH_NAME,
+        SKIP,
+    };
+
+    /**
+     * {@link BySelector} unique to this page.
+     */
+    private static final BySelector TESTDPC_SETUP_FINISHED_PAGE_SELECTOR =
+            By.res(TESTDPC_PKG_NAME, "suw_layout_title")
+                    .text("Setup finished");
+
+    /**
+     * {@link BySelector} for {@link RadioButton} with resource-id add_account on
+     * TestDpc Setup finished page.
+     */
+    private static final BySelector ADD_ACCOUNT_RADIO_BUTTON =
+            By.res(TESTDPC_PKG_NAME, "add_account")
+                    .clazz(RadioButton.class)
+                    .clickable(true)
+                    .checkable(true);
+
+    /**
+     * {@link BySelector} for {@link RadioButton} with resource-id add_account_with_name on
+     * TestDpc Setup finished page.
+     */
+    private static final BySelector ADD_ACCOUNT_WITH_NAME_RADIO_BUTTON =
+            By.res(TESTDPC_PKG_NAME, "add_account_with_name")
+                .clazz(RadioButton.class)
+                .clickable(true)
+                .checkable(true);
+
+    /**
+     * {@link BySelector} for {@link RadioButton} with resource-id add_account_with_name on
+     * TestDpc Setup finished page.
+     */
+    private static final BySelector ADD_ACCOUNT_SKIP_RADIO_BUTTON =
+            By.res(TESTDPC_PKG_NAME, "add_account_skip")
+                .clazz(RadioButton.class)
+                .clickable(true)
+                .checkable(true);
+
+    /** Option for next step. */
+    private final OPTION mNextStep;
+
+    /**
+     * Constructor.
+     *
+     * @param uiDevice {@link UiDevice} object
+     * @param config {@link TestConfig} object holding test configurations
+     */
+    public SetupFinishedPage(UiDevice uiDevice, TestConfig config, OPTION nextStep) {
+        super(uiDevice, config);
+        mNextStep = nextStep;
+    }
+
+    @Override
+    public BySelector uniqueElement() {
+        return TESTDPC_SETUP_FINISHED_PAGE_SELECTOR;
+    }
+
+    @Override
+    public void navigate() throws Exception {
+        getProvisioningStatsLogger().stopTime(STAT_TESTDPC_WORK_PROFILE_CREATION_TIME);
+        getProvisioningStatsLogger().writeStatsToFile();
+
+        switch(mNextStep) {
+            case ADD_ACCOUNT:
+                WidgetUtils.waitAndClick(getUiDevice(), ADD_ACCOUNT_RADIO_BUTTON);
+                break;
+            case ADD_ACCOUNT_WITH_NAME:
+                WidgetUtils.waitAndClick(getUiDevice(), ADD_ACCOUNT_WITH_NAME_RADIO_BUTTON);
+                break;
+            case SKIP:
+                WidgetUtils.waitAndClick(getUiDevice(), ADD_ACCOUNT_SKIP_RADIO_BUTTON);
+                break;
+        }
+        if (!mNextStep.equals(OPTION.DO_NOTHING)) {
+            WidgetUtils.waitAndClick(getUiDevice(), TESTDPC_NEXT_BUTTON_SELECTOR);
+        }
+    }
+}
diff --git a/libs/UiAutomatorLib/src/com/android/afwtest/uiautomator/pages/testdpc/SetupManagementDoPage.java b/libs/UiAutomatorLib/src/com/android/afwtest/uiautomator/pages/testdpc/SetupManagementDoPage.java
new file mode 100644
index 0000000..6aeb2e1
--- /dev/null
+++ b/libs/UiAutomatorLib/src/com/android/afwtest/uiautomator/pages/testdpc/SetupManagementDoPage.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.uiautomator.pages.testdpc;
+
+import static com.android.afwtest.uiautomator.Constants.TESTDPC_NEXT_BUTTON_SELECTOR;
+import static com.android.afwtest.uiautomator.Constants.TESTDPC_SCROLL_VIEW_SELECTOR;
+import static com.android.afwtest.uiautomator.Constants.TESTDPC_SETUP_DEVICE_OWNER_RADIO_BUTTON_SELECTOR;
+import static com.android.afwtest.uiautomator.Constants.TESTDPC_SETUP_MANAGEMENT_PAGE_TITLE_SELECTOR;
+
+import android.support.test.uiautomator.BySelector;
+import android.support.test.uiautomator.UiDevice;
+
+import com.android.afwtest.common.test.TestConfig;
+import com.android.afwtest.uiautomator.pages.UiPage;
+import com.android.afwtest.uiautomator.utils.WidgetUtils;
+
+/**
+ * TestDpc Setup Management page during DO provisioning.
+ */
+public final class SetupManagementDoPage extends UiPage {
+
+    /**
+     * Constructor.
+     *
+     * @param uiDevice {@link UiDevice} object
+     * @param config {@link TestConfig} object holding test configurations
+     */
+    public SetupManagementDoPage(UiDevice uiDevice, TestConfig config) {
+        super(uiDevice, config);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public BySelector uniqueElement() {
+        return TESTDPC_SETUP_MANAGEMENT_PAGE_TITLE_SELECTOR;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void navigate() throws Exception {
+        WidgetUtils.scrollToItem(getUiDevice(),
+                TESTDPC_SCROLL_VIEW_SELECTOR,
+                TESTDPC_SETUP_DEVICE_OWNER_RADIO_BUTTON_SELECTOR);
+        WidgetUtils.waitAndClick(getUiDevice(),
+                TESTDPC_SETUP_DEVICE_OWNER_RADIO_BUTTON_SELECTOR);
+        WidgetUtils.waitAndClick(getUiDevice(), TESTDPC_NEXT_BUTTON_SELECTOR);
+    }
+}
\ No newline at end of file
diff --git a/libs/UiAutomatorLib/src/com/android/afwtest/uiautomator/pages/testdpc/SetupManagementPoPage.java b/libs/UiAutomatorLib/src/com/android/afwtest/uiautomator/pages/testdpc/SetupManagementPoPage.java
new file mode 100644
index 0000000..468016f
--- /dev/null
+++ b/libs/UiAutomatorLib/src/com/android/afwtest/uiautomator/pages/testdpc/SetupManagementPoPage.java
@@ -0,0 +1,67 @@
+/*
+ * 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.uiautomator.pages.testdpc;
+
+import static com.android.afwtest.uiautomator.Constants.TESTDPC_NEXT_BUTTON_SELECTOR;
+import static com.android.afwtest.uiautomator.Constants.TESTDPC_SCROLL_VIEW_SELECTOR;
+import static com.android.afwtest.uiautomator.Constants.TESTDPC_SETUP_DEVICE_OWNER_RADIO_BUTTON_SELECTOR;
+import static com.android.afwtest.uiautomator.Constants.TESTDPC_SETUP_MANAGED_PROFILE_RADIO_BUTTON_SELECTOR;
+import static com.android.afwtest.uiautomator.Constants.TESTDPC_SETUP_MANAGEMENT_PAGE_TITLE_SELECTOR;
+
+import android.support.test.uiautomator.BySelector;
+import android.support.test.uiautomator.UiDevice;
+
+import com.android.afwtest.common.test.TestConfig;
+import com.android.afwtest.uiautomator.pages.UiPage;
+import com.android.afwtest.uiautomator.utils.WidgetUtils;
+
+/**
+ * TestDpc Setup Management page during PO provisioning.
+ */
+public final class SetupManagementPoPage extends UiPage {
+
+    /**
+     * Constructor.
+     *
+     * @param uiDevice {@link UiDevice} object
+     * @param config {@link TestConfig} object holding test configurations
+     */
+    public SetupManagementPoPage(UiDevice uiDevice, TestConfig config) {
+        super(uiDevice, config);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public BySelector uniqueElement() {
+        return TESTDPC_SETUP_MANAGEMENT_PAGE_TITLE_SELECTOR;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void navigate() throws Exception {
+        WidgetUtils.scrollToItem(getUiDevice(),
+            TESTDPC_SCROLL_VIEW_SELECTOR,
+            TESTDPC_SETUP_DEVICE_OWNER_RADIO_BUTTON_SELECTOR);
+        WidgetUtils.waitAndClick(getUiDevice(),
+                TESTDPC_SETUP_MANAGED_PROFILE_RADIO_BUTTON_SELECTOR);
+        WidgetUtils.waitAndClick(getUiDevice(), TESTDPC_NEXT_BUTTON_SELECTOR);
+    }
+}
\ No newline at end of file
diff --git a/libs/UiAutomatorLib/src/com/android/afwtest/uiautomator/provisioning/AutomationDriver.java b/libs/UiAutomatorLib/src/com/android/afwtest/uiautomator/provisioning/AutomationDriver.java
new file mode 100644
index 0000000..629c262
--- /dev/null
+++ b/libs/UiAutomatorLib/src/com/android/afwtest/uiautomator/provisioning/AutomationDriver.java
@@ -0,0 +1,318 @@
+/*
+ * 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.uiautomator.provisioning;
+
+import static com.android.afwtest.uiautomator.Constants.MANAGED_PROVISIONING_PKG_NAME;
+import static com.android.afwtest.uiautomator.Constants.TESTDPC_PKG_NAME;
+import static com.android.afwtest.uiautomator.pages.gms.AddAccountPage.GMS_ADD_OR_VERIFY_ACCOUNT_PAGE_SELECTOR;
+
+import android.support.test.uiautomator.By;
+import android.support.test.uiautomator.UiDevice;
+import android.util.Log;
+import com.android.afwtest.common.test.TestConfig;
+import com.android.afwtest.uiautomator.pages.LandingPage;
+import com.android.afwtest.uiautomator.pages.PageSkipper;
+import com.android.afwtest.uiautomator.pages.UiPage;
+import com.android.afwtest.uiautomator.pages.gms.AddAccountPage;
+import com.android.afwtest.uiautomator.pages.gms.EnterPasswordPage;
+import com.android.afwtest.uiautomator.pages.gms.GoogleAppsDevicePolicyPage;
+import com.android.afwtest.uiautomator.pages.gms.GoogleServicesPage;
+import com.android.afwtest.uiautomator.pages.managedprovisioning.ErrorPage;
+import com.android.afwtest.uiautomator.pages.managedprovisioning.SetupYourDevicePage;
+import com.android.afwtest.uiautomator.pages.managedprovisioning.TermsPage;
+import com.android.afwtest.uiautomator.pages.packageinstaller.DeviceAccessPage;
+import com.android.afwtest.uiautomator.pages.testdpc.FinishSetupPage;
+import com.android.afwtest.uiautomator.pages.testdpc.SetupManagementDoPage;
+import com.android.afwtest.uiautomator.pages.testdpc.SetupManagementPoPage;
+import java.util.HashSet;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * A help class that automates provisioning flows.
+ */
+public class AutomationDriver {
+
+    private static final String TAG = "afwtest.AutomationDriver";
+
+    /**
+     * {@link UiDevice} object.
+     */
+    private final UiDevice mUiDevice;
+
+    /**
+     * Constructor.
+     *
+     * @param uiDevice {@link UiDevice} object
+     */
+    public AutomationDriver(UiDevice uiDevice) {
+        mUiDevice = uiDevice;
+    }
+
+    /**
+     * Gets {@link UiDevice} instance.
+     *
+     * @return {@link UiDevice} instances
+     */
+    protected UiDevice getUiDevice() {
+        return mUiDevice;
+    }
+
+    /**
+     * Runs the NFC provisioning flow.
+     *
+     * @param testConfig test configurations required for provisioning automation
+     * @return {@code true} if provisioning completes, {@code false} otherwise
+     */
+    public boolean runNfcProvisioning(TestConfig testConfig) throws Exception {
+        return navigate(getNfcProvisioningPages(testConfig));
+    }
+
+    /**
+     * Runs the QR Code provisioning flow.
+     *
+     * @param testConfig test configurations required for provisioning automation
+     * @return {@code true} if provisioning completes, {@code false} otherwise
+     */
+    public boolean runQRCodeProvisioning(TestConfig testConfig) throws Exception {
+        return navigate(getQRCodeProvisioningPages(testConfig));
+    }
+
+    /**
+     * Runs the setup wizard device owner provisioning flow.
+     *
+     * @param testConfig test configurations required for provisioning automation
+     * @return {@code true} if provisioning completes, {@code false} otherwise
+     */
+    public boolean runSuwDoProvisioning(TestConfig testConfig) throws Exception {
+        return navigate(getSuwDoProvisioningPages(testConfig));
+    }
+
+    /**
+     * Runs the setup wizard profile owner provisioning flow.
+     *
+     * @param testConfig test configurations required for provisioning automation
+     * @return {@code true} if provisioning completes, {@code false} otherwise
+     */
+    public boolean runSuwPoProvisioning(TestConfig testConfig) throws Exception {
+        return navigate(getSuwPoProvisioningPages(testConfig));
+    }
+
+    /**
+     * Runs the non setup wizard profile owner provisioning flow.
+     *
+     * @param testConfig test configurations required for provisioning automation
+     * @return {@code true} if provisioning completes, {@code false} otherwise
+     */
+    public boolean runNonSuwPoProvisioning(TestConfig testConfig) throws Exception {
+        return navigate(getNonSuwPoProvisioningPages(testConfig));
+    }
+
+    /**
+     * Runs the non setup wizard profile owner provisioning flow when it is not allowed.
+     *
+     * @param testConfig test configurations required for provisioning automation
+     * @return {@code true} if provisioning completes, {@code false} otherwise
+     */
+    public boolean runNonSuwPoProvisioningDisallowed(TestConfig testConfig) throws Exception {
+        return navigate(getNonSuwPoProvisioningDisallowedPages(testConfig));
+    }
+
+    /**
+     * Navigates a list of {@link UiPage} one by one. Optional pages will be skipped if not found.
+     *
+     * @param pages list of {@link UiPage} to navigate
+     *
+     * @return {@code true} if all pages are navigated successfully, {@code false} otherwise
+     */
+    protected boolean navigate(List<UiPage> pages) throws Exception {
+        for (UiPage page : pages) {
+            if (page.waitForLoading()) {
+                Log.i(TAG, String.format("Navigating: %s", page.getClass().getName()));
+                page.navigate();
+                Log.i(TAG, String.format("Navigating: %s done", page.getClass().getName()));
+            } else if (page.isOptional()) {
+                Log.i(TAG, String.format("Failed to load page: %s; but it's optional, skipping",
+                    page.getClass().getName()));
+            } else {
+                throw new Exception(
+                    String.format("Failed to load page: %s", page.getClass().getName()));
+            }
+        }
+
+        return true;
+    }
+
+    /**
+     * Gets the list of {@link UiPage} for NFC provisioning flow.
+     *
+     * @param testConfig test configurations required for provisioning automation
+     * @return list of {@link UiPage} representing the setup wizard DO provisioning flow
+     */
+    private List<UiPage> getNfcProvisioningPages(TestConfig testConfig) {
+        List<UiPage> pages = new LinkedList<>();
+        pages.add(new SetupYourDevicePage(mUiDevice, testConfig));
+
+        // Land on the mdm page.
+        UiPage mdmPage =
+                new LandingPage(mUiDevice, testConfig, By.pkg(testConfig.getDeviceAdminPkgName()));
+        PageSkipper pageSkipper = new PageSkipper(mUiDevice, mdmPage.uniqueElement(), testConfig);
+
+        pages.add(pageSkipper);
+        pages.add(mdmPage);
+
+        // Setup page skipper.
+        Set<String> packageNameBlacklist = new HashSet<String>();
+        packageNameBlacklist.add(MANAGED_PROVISIONING_PKG_NAME);
+        packageNameBlacklist.add(testConfig.getDeviceAdminPkgName());
+        pageSkipper.setPackageNameBlacklist(packageNameBlacklist);
+
+        return pages;
+    }
+
+    /**
+     * Gets the list of {@link UiPage} for QR code provisioning flow.
+     *
+     * @param testConfig test configurations required for provisioning automation
+     * @return list of {@link UiPage} representing the setup wizard DO provisioning flow
+     */
+    private List<UiPage> getQRCodeProvisioningPages(TestConfig testConfig) {
+        List<UiPage> pages = new LinkedList<>();
+        pages.add(new SetupYourDevicePage(mUiDevice, testConfig));
+
+        // Land on the mdm page.
+        UiPage mdmPage =
+                new LandingPage(mUiDevice, testConfig, By.pkg(testConfig.getDeviceAdminPkgName()));
+        PageSkipper pageSkipper = new PageSkipper(mUiDevice, mdmPage.uniqueElement(), testConfig);
+
+        pages.add(pageSkipper);
+        pages.add(mdmPage);
+
+        // Setup page skipper.
+        Set<String> packageNameBlacklist = new HashSet<String>();
+        packageNameBlacklist.add(MANAGED_PROVISIONING_PKG_NAME);
+        packageNameBlacklist.add(testConfig.getDeviceAdminPkgName());
+        pageSkipper.setPackageNameBlacklist(packageNameBlacklist);
+
+        return pages;
+    }
+
+    /**
+     * Gets the list of {@link UiPage} for setup wizard DO provisioning flow.
+     *
+     * @param testConfig test configurations required for provisioning automation
+     * @return list of {@link UiPage} representing the setup wizard DO provisioning flow
+     */
+    private List<UiPage> getSuwDoProvisioningPages(TestConfig testConfig) {
+        List<UiPage> pages = new LinkedList<>();
+        pages.add(new AddAccountPage(mUiDevice, testConfig));
+        pages.add(new EnterPasswordPage(mUiDevice, testConfig));
+        pages.add(new GoogleServicesPage(mUiDevice, testConfig));
+        pages.add(new GoogleAppsDevicePolicyPage(mUiDevice, testConfig));
+        pages.add(new DeviceAccessPage(mUiDevice, testConfig));
+        pages.add(new SetupManagementDoPage(mUiDevice, testConfig));
+        pages.add(new SetupYourDevicePage(mUiDevice, testConfig));
+
+        UiPage landingPage = new FinishSetupPage(mUiDevice, testConfig);
+        PageSkipper pageSkipper =
+                new PageSkipper(mUiDevice, landingPage.uniqueElement(), testConfig);
+        pages.add(pageSkipper);
+        pages.add(landingPage);
+
+        // Setup page skipper.
+        Set<String> packageNameBlacklist = new HashSet<String>();
+        packageNameBlacklist.add(MANAGED_PROVISIONING_PKG_NAME);
+        packageNameBlacklist.add(TESTDPC_PKG_NAME);
+        pageSkipper.setPackageNameBlacklist(packageNameBlacklist);
+
+        return pages;
+    }
+
+    /**
+     * Gets the list of {@link UiPage} for setup wizard PO provisioning flow.
+     *
+     * @param testConfig test configurations required for provisioning automation
+     * @return list of {@link UiPage} representing the setup wizard PO provisioning flow
+     */
+    private List<UiPage> getSuwPoProvisioningPages(TestConfig testConfig) {
+        List<UiPage> pages = new LinkedList<>();
+        pages.add(getToAddAccountSkipper(testConfig));
+        pages.add(new AddAccountPage(mUiDevice, testConfig));
+        pages.add(new EnterPasswordPage(mUiDevice, testConfig));
+        pages.add(new GoogleServicesPage(mUiDevice, testConfig));
+        pages.add(new GoogleAppsDevicePolicyPage(mUiDevice, testConfig));
+        pages.add(new DeviceAccessPage(mUiDevice, testConfig));
+        pages.add(new SetupManagementPoPage(mUiDevice, testConfig));
+        pages.add(new TermsPage(mUiDevice, testConfig));
+
+        UiPage landingPage = new FinishSetupPage(mUiDevice, testConfig);
+        PageSkipper pageSkipper =
+                new PageSkipper(mUiDevice, landingPage.uniqueElement(), testConfig);
+        pages.add(pageSkipper);
+        pages.add(landingPage);
+
+        // Setup page skipper.
+        Set<String> packageNameBlacklist = new HashSet<String>();
+        packageNameBlacklist.add(MANAGED_PROVISIONING_PKG_NAME);
+        packageNameBlacklist.add(TESTDPC_PKG_NAME);
+        pageSkipper.setPackageNameBlacklist(packageNameBlacklist);
+
+        return pages;
+    }
+
+    /**
+     * Gets {@link PageSkipper} to go through Setup Wizard to reach {@link AddAccountPage}.
+     *
+     * @param testConfig test configurations required for provisioning automation.
+     * @return {@link PageSkipper} to go through Setup Wizard to reach {@link AddAccountPage}.
+     */
+    private UiPage getToAddAccountSkipper(TestConfig testConfig) {
+        return new PageSkipper(mUiDevice, GMS_ADD_OR_VERIFY_ACCOUNT_PAGE_SELECTOR, testConfig);
+    }
+
+    /**
+     * Gets the list of {@link UiPage} for non setup wizard PO provisioning flow.
+     *
+     * @param testConfig test configurations required for provisioning automation
+     * @return list of {@link UiPage} representing the non setup wizard PO provisioning flow
+     */
+    private List<UiPage> getNonSuwPoProvisioningPages(TestConfig testConfig) {
+        List<UiPage> pages = new LinkedList<>();
+        pages.add(new AddAccountPage(mUiDevice, testConfig));
+        pages.add(new EnterPasswordPage(mUiDevice, testConfig));
+        pages.add(new GoogleServicesPage(mUiDevice, testConfig));
+        pages.add(new GoogleAppsDevicePolicyPage(mUiDevice, testConfig));
+        pages.add(new DeviceAccessPage(mUiDevice, testConfig));
+        pages.add(new SetupManagementPoPage(mUiDevice, testConfig));
+        pages.add(new TermsPage(mUiDevice, testConfig));
+        pages.add(new FinishSetupPage(mUiDevice, testConfig));
+        return pages;
+    }
+
+    /**
+     * Gets the list of {@link UiPage} for testing disallowed provisioning test.
+     *
+     * @param testConfig test configurations required for provisioning automation
+     * @return list of {@link UiPage} for testing disallowed provisioning test.
+     */
+    private List<UiPage> getNonSuwPoProvisioningDisallowedPages(TestConfig testConfig) {
+        List<UiPage> pages = new LinkedList<>();
+        pages.add(new ErrorPage(mUiDevice, testConfig));
+        return pages;
+    }
+}
diff --git a/libs/UiAutomatorLib/src/com/android/afwtest/uiautomator/test/AbstractTestCase.java b/libs/UiAutomatorLib/src/com/android/afwtest/uiautomator/test/AbstractTestCase.java
new file mode 100644
index 0000000..2b4a58f
--- /dev/null
+++ b/libs/UiAutomatorLib/src/com/android/afwtest/uiautomator/test/AbstractTestCase.java
@@ -0,0 +1,57 @@
+/*
+ * 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.uiautomator.test;
+
+import android.app.Instrumentation;
+import android.content.Context;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.uiautomator.UiDevice;
+
+/**
+ * Abstract class for test cases using android-support-test.
+ *
+ * Provides common methods that are likely to be needed in test cases.
+ */
+public abstract class AbstractTestCase {
+
+    /**
+     * Gets application context.
+     *
+     * @return application context
+     */
+    protected Context getContext() {
+        return InstrumentationRegistry.getContext();
+    }
+
+    /**
+     * Get current instance of {@link UiDevice}.
+     *
+     * @return current {@link UiDevice}
+     */
+    protected UiDevice getUiDevice() {
+        return UiDevice.getInstance(getInstrumentation());
+    }
+
+    /**
+     * Gets current instance of {@link Instrumentation}.
+     *
+     * @return current {@link Instrumentation}
+     */
+    public Instrumentation getInstrumentation() {
+        return InstrumentationRegistry.getInstrumentation();
+    }
+}
diff --git a/libs/UiAutomatorLib/src/com/android/afwtest/uiautomator/test/AfwTestUiWatcher.java b/libs/UiAutomatorLib/src/com/android/afwtest/uiautomator/test/AfwTestUiWatcher.java
new file mode 100644
index 0000000..4e3cbe3
--- /dev/null
+++ b/libs/UiAutomatorLib/src/com/android/afwtest/uiautomator/test/AfwTestUiWatcher.java
@@ -0,0 +1,266 @@
+/*
+ * 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.uiautomator.test;
+
+import static com.android.afwtest.common.Constants.KEY_MUTE_APP_CRASH_DIALOGS;
+import static com.android.afwtest.uiautomator.Constants.ANDROID_PKG_NAME;
+import static com.android.afwtest.uiautomator.Constants.MANAGED_PROVISIONING_PKG_NAME;
+import static com.android.afwtest.uiautomator.utils.WidgetUtils.safeClick;
+import static com.android.afwtest.uiautomator.utils.WidgetUtils.safeClickAny;
+import static java.util.regex.Pattern.CASE_INSENSITIVE;
+
+import android.support.test.uiautomator.By;
+import android.support.test.uiautomator.BySelector;
+import android.support.test.uiautomator.UiDevice;
+import android.support.test.uiautomator.UiObject2;
+import android.support.test.uiautomator.UiWatcher;
+import android.text.TextUtils;
+import android.util.Log;
+
+import com.android.afwtest.common.test.TestConfig;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.regex.Pattern;
+
+/**
+ * {@link UiWatcher} to handle common unexpected scenarios.
+ *
+ * <p>
+ * This is a singleton class. There can be only one
+ * </p>
+ */
+public final class AfwTestUiWatcher implements UiWatcher {
+
+    private static final String TAG = "afwtest.AfwTestUiWatcher";
+
+    /**
+     * Unique name of this ui watcher.
+     */
+    private static final String UI_WATCHER_NAME = "afw-test-uiwatcher";
+
+    /**
+     * Regex string for app crashed message.
+     */
+    private static final String APP_STOPPED_MSG_REGEX = ".*has stopped.*";
+
+    /**
+     * {@link BySelector} for app stopped dialog title.
+     */
+    private static final BySelector APP_STOPPED_MSG_SELECTOR =
+            By.res(ANDROID_PKG_NAME, "alertTitle");
+
+    /**
+     * {@link BySelector} for mute option on app stop dialog.
+     */
+    private static final BySelector APP_STOPPED_DIALOG_MUTE_SELECTOR =
+            By.res(Pattern.compile(ANDROID_PKG_NAME + ":id/(aerr_mute|aerr_close)"));
+
+    /**
+     * Words to match for "Accept" button's text or description.
+     */
+    private static final String[] ACCEPT_WORDS = {
+            "accept", "agree", "allow", "I agree", "yes"};
+
+    /**
+     * {@link Pattern} to match any "Accept" word in {@link #ACCEPT_WORDS}.
+     */
+    private static final Pattern ACCEPT_BTN_PATTERN =
+            Pattern.compile(TextUtils.join("|", ACCEPT_WORDS), CASE_INSENSITIVE);
+
+    /**
+     * Buttons with text matching {@link #ACCEPT_BTN_PATTERN}.
+     */
+    private static final BySelector ACCEPT_BTN_TEXT_SELECTOR =
+            By.enabled(true)
+                    .checkable(false)
+                    .clickable(true)
+                    .text(ACCEPT_BTN_PATTERN);
+
+    /**
+     * Buttons with content description matching {@link #ACCEPT_BTN_PATTERN}.
+     */
+    private static final BySelector ACCEPT_BTN_DESC_SELECTOR =
+            By.enabled(true)
+                    .checkable(false)
+                    .clickable(true)
+                    .desc(ACCEPT_BTN_PATTERN);
+
+    /**
+     * Non-null string indicates fatal app crashed.
+     */
+    private static String sFatalAppCrashMsg;
+
+    /**
+     * Assert if any of these app crashes.
+     */
+    private static final List<String> FATAL_APP_CRASHES =
+            Arrays.asList(MANAGED_PROVISIONING_PKG_NAME,
+                    "Setup Wizard");
+
+
+    /**
+     * Whether it is allowed to use {@link #ACCEPT_WORDS} to skip pages.
+     */
+    private static boolean sAllowPageSkipping = true;
+
+    /**
+     * List of packages whose crash should be ignored.
+     */
+    private final List<String> mAppCrashWhitelist;
+
+    /**
+     * {@link UiDevice} object.
+     */
+    private final UiDevice mUiDevice;
+
+    /**
+     * Constructor.
+     *
+     * @param uiDevice {@link UiDevice} object
+     */
+    private AfwTestUiWatcher(UiDevice uiDevice) throws Exception {
+        mUiDevice = uiDevice;
+        mAppCrashWhitelist = new ArrayList<String>();
+        mAppCrashWhitelist.addAll(TestConfig.getDefault().getAppCrashWhitelist());
+    }
+
+    /**
+     * Registers this ui watcher.
+     *
+     * @param uiDevice {@link UiDevice} object
+     */
+    public static void register(UiDevice uiDevice) throws Exception {
+        uiDevice.registerWatcher(UI_WATCHER_NAME, new AfwTestUiWatcher(uiDevice));
+    }
+
+    /**
+     * Unregisters this ui watcher.
+     *
+     * @param uiDevice {@link UiDevice} object
+     */
+    public static void unregister(UiDevice uiDevice) {
+        uiDevice.removeWatcher(UI_WATCHER_NAME);
+    }
+
+    public static String getFatalAppCrashMsg() {
+        return sFatalAppCrashMsg;
+    }
+
+    /**
+     * Allows skipping pages using {@link #ACCEPT_WORDS}.
+     */
+    public static void allowPageSkipping() {
+        sAllowPageSkipping = true;
+    }
+
+    /**
+     * Disallows skipping pages using {@link #ACCEPT_WORDS}.
+     */
+    public static void disallowPageSkipping() {
+        sAllowPageSkipping = false;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public boolean checkForCondition() {
+
+        try {
+            // Use temp variables to avoid short-circuit boolean evaluation.
+            boolean isAppsStopped = clearAppStoppedDialog();
+            boolean isPagesSkipped = (sAllowPageSkipping && checkAcceptButtons());
+            return isAppsStopped || isPagesSkipped;
+        } catch (Exception e) {
+            // Don't throw exception as it will crashes the test runner
+            Log.e(TAG, UI_WATCHER_NAME, e);
+            return false;
+        }
+    }
+
+    /**
+     * Clears dialogs: "Unfortunately, {app name} has stopped."
+     *
+     * @return {@code true} if any dialog is dismissed, {@code false} otherwise
+     */
+    private boolean clearAppStoppedDialog() throws IOException {
+
+        if (mUiDevice.hasObject(APP_STOPPED_MSG_SELECTOR)
+                && mUiDevice.hasObject(APP_STOPPED_DIALOG_MUTE_SELECTOR)) {
+
+            UiObject2 msgWidget = mUiDevice.findObject(APP_STOPPED_MSG_SELECTOR);
+            if (msgWidget != null) {
+                String msg = msgWidget.getText();
+                Log.w(TAG, String.format("Found app crash dialog: %s", msg));
+                if (isFatalAppCrash(msg)) {
+                    Log.w(TAG, String.format("Fatal app crash. Test should abort.", msg));
+                    sFatalAppCrashMsg = msg;
+                    return true;
+                }
+                Log.w(TAG, String.format("Auto closing: %s", msg));
+                sFatalAppCrashMsg = null;
+                return safeClick(mUiDevice.findObject(APP_STOPPED_DIALOG_MUTE_SELECTOR));
+            }
+        }
+
+        return false;
+    }
+
+    /**
+     * Checks if app crash is fatal.
+     *
+     * @param appCrashMsg App crash message
+     * @return {@code true} if app crash is fatal, {@code false} otherwise.
+     */
+    private boolean isFatalAppCrash(String appCrashMsg) throws IOException {
+        for (String app : FATAL_APP_CRASHES) {
+            if (appCrashMsg.contains(app)) {
+                return true;
+            }
+        }
+
+        // if muting all app crash dialog is enabled, return
+        if (TestConfig.getDefault().muteAppCrashDialogs()) {
+            Log.i(TAG, String.format("%s=true", KEY_MUTE_APP_CRASH_DIALOGS));
+            return false;
+        }
+
+        // otherwise, auto close whitelisted app crashes, assert on others.
+        for (String app : mAppCrashWhitelist) {
+            if (appCrashMsg.contains(app)) {
+                Log.w(TAG, String.format("Whitelisted app crash: %s", app));
+                return false;
+            }
+        }
+
+        return true;
+    }
+
+    /**
+     * Clicks any visible accept button.
+     *
+     * @return {@code true} if any button found and clicked successfully;
+     *         {@code false} otherwise
+     */
+    private boolean checkAcceptButtons() {
+        return safeClickAny(mUiDevice.findObjects(ACCEPT_BTN_TEXT_SELECTOR)) ||
+                safeClickAny(mUiDevice.findObjects(ACCEPT_BTN_DESC_SELECTOR));
+    }
+}
diff --git a/libs/UiAutomatorLib/src/com/android/afwtest/uiautomator/utils/BySelectorHelper.java b/libs/UiAutomatorLib/src/com/android/afwtest/uiautomator/utils/BySelectorHelper.java
new file mode 100644
index 0000000..3763bac
--- /dev/null
+++ b/libs/UiAutomatorLib/src/com/android/afwtest/uiautomator/utils/BySelectorHelper.java
@@ -0,0 +1,71 @@
+/*
+ * 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.uiautomator.utils;
+
+import static com.android.afwtest.common.Constants.ACTION_CHECK;
+import static com.android.afwtest.common.Constants.ACTION_SCROLL;
+
+import android.support.test.uiautomator.By;
+import android.support.test.uiautomator.BySelector;
+
+import com.android.afwtest.common.test.OemWidget;
+
+/**
+ * Helper class for {@link BySelector}.
+ */
+public final class BySelectorHelper {
+
+    /**
+     * Gets the corresponding {@link BySelector} for {@link OemWidget}
+     *
+     * @param widget {@link OemWidget} object
+     * @return {@link BySelector} for the given {@link OemWidget}
+     */
+    public static BySelector getSelector(OemWidget widget) {
+        BySelector selector = By.enabled(true);
+
+        if (!widget.getText().isEmpty()) {
+            selector.text(widget.getText());
+        }
+
+        if (!widget.getDescription().isEmpty()) {
+            selector.desc(widget.getDescription());
+        }
+
+        if (!widget.getResourceId().isEmpty()) {
+            selector.res(widget.getResourceId());
+        }
+
+        if (!widget.getPackage().isEmpty()) {
+            selector.pkg(widget.getPackage());
+        }
+
+        if (!widget.getClassName().isEmpty()) {
+            selector.clazz(widget.getClassName());
+        }
+
+        if (widget.getAction().equals(ACTION_SCROLL)) {
+            selector.scrollable(true);
+        } else if (widget.getAction().equals(ACTION_CHECK)) {
+            selector.checkable(true).checked(false);
+        } else {
+            selector.clickable(true);
+        }
+
+        return selector;
+    }
+}
diff --git a/libs/UiAutomatorLib/src/com/android/afwtest/uiautomator/utils/Device.java b/libs/UiAutomatorLib/src/com/android/afwtest/uiautomator/utils/Device.java
new file mode 100644
index 0000000..f462718
--- /dev/null
+++ b/libs/UiAutomatorLib/src/com/android/afwtest/uiautomator/utils/Device.java
@@ -0,0 +1,46 @@
+/*
+ * 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.uiautomator.utils;
+
+import android.support.test.uiautomator.UiDevice;
+
+/**
+ * Device related util functions.
+ */
+public final class Device {
+    /**
+     * Swipes screen down for half page.
+     *
+     * @param uiDevice {@link UiDevice} object
+     */
+    public static void swipeDown(UiDevice device) {
+        int width = device.getDisplayWidth();
+        int height = device.getDisplayHeight();
+        device.swipe(width / 2, height / 4, width / 2, height * 3 / 4, 10);
+    }
+
+    /**
+     * Swipes screen up for half page.
+     *
+     * @param uiDevice {@link UiDevice} object
+     */
+    public static void swipeUp(UiDevice device) {
+        int width = device.getDisplayWidth();
+        int height = device.getDisplayHeight();
+        device.swipe(width / 2, height * 3 / 4, width / 2, height / 4, 10);
+    }
+}
\ No newline at end of file
diff --git a/libs/UiAutomatorLib/src/com/android/afwtest/uiautomator/utils/TextField.java b/libs/UiAutomatorLib/src/com/android/afwtest/uiautomator/utils/TextField.java
new file mode 100644
index 0000000..b82fc57
--- /dev/null
+++ b/libs/UiAutomatorLib/src/com/android/afwtest/uiautomator/utils/TextField.java
@@ -0,0 +1,153 @@
+/*
+ * 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.uiautomator.utils;
+
+import android.support.test.uiautomator.By;
+import android.support.test.uiautomator.BySelector;
+import android.support.test.uiautomator.UiDevice;
+import android.support.test.uiautomator.UiObject2;
+import android.util.Log;
+import android.view.KeyEvent;
+
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Text field related util functions.
+ */
+public final class TextField {
+
+    private static final String TAG = "afwtest.TextField";
+
+    /**
+     * Default UI waiting time, in milliseconds.
+     */
+    private static final long DEFAULT_TIMEOUT_MS = TimeUnit.SECONDS.toMillis(5);
+
+    /**
+     * Private constructor to prevent instantiation.
+     */
+    private TextField() {
+    }
+
+    /**
+     * Inputs text into a text field and activate the navigation button.
+     *
+     * <p>Normally {@link UiObject2}.setText() will input text into a text field.
+     * But on some activities, such as Add Google Account page, the navigation button (e.g. NEXT)
+     * will not be activated until there is some non-space characters entered into the text
+     * field. Calling {@link UiObject2}.setText() will set the text but not activate the
+     * navigate button.
+     * This util function solves this problem by simulating key pressing event to enter '0'
+     * into the text filed to activate the navigation button before calling {@link UiObject2}.
+     * setText() to set the text.
+     * </p>
+     *
+     * @param uiDevice {@link UiDevice} object
+     * @param textFieldSelector {@link BySelector} for the text field
+     * @param text text to set
+     * @param navigationBtnSelector navigation button to activate
+     */
+    public static void enterTextAndActivateNavigationBtn(
+            UiDevice uiDevice,
+            BySelector textFieldSelector,
+            String text,
+            BySelector navigationBtnSelector) throws Exception {
+
+        // Try 3 times
+        int maxAttempts = 3;
+        while (maxAttempts > 0) {
+
+            Log.i(TAG, String.format("Activating navigation button: %s.",
+                    navigationBtnSelector.toString()));
+
+            try {
+                activateNavigationBtn(uiDevice, textFieldSelector, navigationBtnSelector);
+                // Navigation button activated, exit loop
+                break;
+            } catch (Exception e) {
+
+                --maxAttempts;
+
+                // Don't throw in the retry loop
+                Log.e(TAG, String.format("Failed to activate navigation button, attempts left: %d.",
+                        maxAttempts), e);
+            }
+        }
+
+        // Throw exception if activation button is not activated.
+        if (maxAttempts <= 0) {
+            throw new RuntimeException(String.format("Failed to activate navigation button",
+                    navigationBtnSelector.toString()));
+        }
+
+        // Hide keyboard.
+        uiDevice.pressBack();
+
+        // Set the text now.
+        UiObject2 textField = WidgetUtils.safeWait(uiDevice,
+                textFieldSelector,
+                DEFAULT_TIMEOUT_MS,
+                3);
+        if (textField == null) {
+            throw new RuntimeException(
+                    "Failed to find text field: " + textFieldSelector.toString());
+        }
+
+
+        textField.setText(text);
+    }
+
+    /**
+     * Activates the navigation button by simulating a key pressing event to enter '0'.
+     *
+     * @param uiDevice {@link UiDevice} object
+     * @param textFieldSelector {@link BySelector} for the text field
+     * @param navigationBtnSelector navigation button to activate
+     */
+    private static void activateNavigationBtn(UiDevice uiDevice,
+            BySelector textFieldSelector,
+            BySelector navigationBtnSelector) throws Exception {
+        WidgetUtils.waitAndClick(uiDevice, textFieldSelector, DEFAULT_TIMEOUT_MS, 3);
+
+        // Clicking the text field will bring up the keyboard and change the view hierachy of
+        // current screen; textField may become stale. Try to find it again before simulating
+        // the keyboard events.
+        if (!WidgetUtils.waitForKeyboard(uiDevice, 3)) {
+            throw new RuntimeException("Could not find keyboard.");
+        }
+        if (WidgetUtils.safeWait(uiDevice, textFieldSelector, DEFAULT_TIMEOUT_MS, 3) == null) {
+            throw new RuntimeException(
+                    "Failed to find text field: " + textFieldSelector.toString());
+        }
+
+        // Simulate an event to enter '0' to the text field, this will activate the navigate button.
+        if (!uiDevice.pressKeyCode(KeyEvent.KEYCODE_0)) {
+            throw new RuntimeException(String.format("Failed to enter 0 into the text field %s",
+                    textFieldSelector.toString()));
+        }
+
+        // Wait for the navigation button to be activated.
+        BySelector newNavigationBtnSelector = By.copy(navigationBtnSelector).enabled(true);
+        if (WidgetUtils.safeWait(uiDevice,
+                newNavigationBtnSelector,
+                DEFAULT_TIMEOUT_MS,
+                3) == null) {
+            throw new RuntimeException(String.format("Failed to activate navigation button: %s",
+                    navigationBtnSelector.toString()));
+        }
+    }
+}
diff --git a/libs/UiAutomatorLib/src/com/android/afwtest/uiautomator/utils/WidgetUtils.java b/libs/UiAutomatorLib/src/com/android/afwtest/uiautomator/utils/WidgetUtils.java
new file mode 100644
index 0000000..4a3d692
--- /dev/null
+++ b/libs/UiAutomatorLib/src/com/android/afwtest/uiautomator/utils/WidgetUtils.java
@@ -0,0 +1,657 @@
+/*
+ * 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.uiautomator.utils;
+
+import android.graphics.Rect;
+import android.os.SystemClock;
+import android.support.test.uiautomator.BySelector;
+import android.support.test.uiautomator.Direction;
+import android.support.test.uiautomator.StaleObjectException;
+import android.support.test.uiautomator.UiDevice;
+import android.support.test.uiautomator.UiObject;
+import android.support.test.uiautomator.UiObject2;
+import android.support.test.uiautomator.UiObjectNotFoundException;
+import android.support.test.uiautomator.UiScrollable;
+import android.support.test.uiautomator.UiSelector;
+import android.support.test.uiautomator.Until;
+import android.util.Log;
+import android.widget.TextView;
+
+import java.io.IOException;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+import java.util.function.Supplier;
+
+/**
+ * Widget utils.
+ */
+public class WidgetUtils {
+
+    private static final String TAG = "afwtest.WidgetUtils";
+
+    /**
+     * Waiting time for each call to UiDevice.wait().
+     */
+    private static final long DEFAULT_UI_WAIT_TIME_MS = TimeUnit.SECONDS.toMillis(5);
+
+    /**
+     * Number of attempts to call {@link UiDevice#wait()} to avoid {@link StaleObjectException}.
+     */
+    private static final int DEFAULT_UI_ATTEMPTS_COUNT = 3;
+
+    /**
+     * Margin of swipe's starting and ending points inside the target.
+     */
+    private static final float DEFAULT_SWIPE_DEADZONE_PCT = 0.1f;
+
+    /**
+     * Swipe's length as object's dimension percentage.
+     */
+    private static final float DEFAULT_SWIPE_PCT = 0.7f;
+
+    /**
+     * Keyboard pop up waiting time, in milliseconds.
+     */
+    private static final long KEYBOARD_START_TIME_MS = TimeUnit.SECONDS.toMillis(2);
+
+    /**
+     * Shell command to execute in order to determine if keyboard is visible.
+     */
+    private static final String FIND_KEYBOARD_COMMAND = "dumpsys input_method";
+
+    /**
+     * String present in dumpsys command's output when keyboard is visible.
+     */
+    private static final String KEYBOARD_IS_SHOWN_STRING = "mInputShown=true";
+
+    /**
+     * Clicks on given {@link UiObject2} without throwing any exception.
+     *
+     * @param obj {@link UiObject2} to click
+     * @return {@code true} if clicked, {@code false} otherwise
+     */
+    public static boolean safeClick(UiObject2 obj) {
+        String widgetProps = getWidgetPropertiesAsString(obj);
+        try {
+            obj.click();
+            Log.d(TAG, String.format("Clicked: %s", widgetProps));
+            return true;
+        } catch(Exception e) {
+            Log.e(TAG, String.format("Failed to click: %s", widgetProps) , e);
+            return false;
+        }
+    }
+
+    /**
+     * Tries to click any of the given list of buttons; return if any button
+     * clicked successfully.
+     *
+     * @param btns list of buttons to click
+     * @return {@code true} if any button clicked successfully; {@code false} otherwise
+     */
+    public static boolean safeClickAny(List<UiObject2> btns) {
+        for (UiObject2 obj : btns) {
+            if (safeClick(obj)) {
+                return true;
+            }
+        }
+
+        return false;
+    }
+
+    /**
+     * Perform fling gesture on given {@link UiObject2} until it cannot scroll any more without
+     * throwing any exception.
+     *
+     * @param obj {@link UiObject2} to scroll
+     * @param direction The direction in which to fling
+     * @return {@code true} if fling performed, {@code false} otherwise
+     */
+    public static boolean safeFling(UiObject2 obj, Direction direction) {
+        String widgetProps = getWidgetPropertiesAsString(obj);
+
+        try {
+            // Set limit to 100 times.
+            for (int i = 0; i < 100; ++i) {
+                if (!obj.fling(direction)) {
+                    break;
+                }
+            }
+            Log.d(TAG, String.format("Scrolled: %s", widgetProps));
+            return true;
+        } catch(Exception e) {
+            Log.e(TAG, String.format("Failed to scroll: %s", widgetProps), e);
+            return false;
+        }
+    }
+
+    /**
+     * Waits for a widget without throwing any exception (N attempts).
+     *
+     * @param uiDevice {@link UiDevice} object
+     * @param selector {@link BySelector} of the widget to wait
+     * @param timeoutMS timeout in milliseconds
+     * @param attempts number of attempts.
+     * @return {@link UiObject2} if expected widget appears within timeout, {@code null} otherwise
+     */
+    public static UiObject2 safeWait(UiDevice uiDevice, BySelector selector, long timeoutMS,
+            int attempts) {
+        for (int i = 0; i < attempts; ++i) {
+            UiObject2 widget = safeWait(uiDevice, selector, timeoutMS);
+            if (widget != null) {
+                return  widget;
+            }
+        }
+
+        return null;
+    }
+
+    /**
+     * Waits for a widget without throwing any exception.
+     *
+     * @param uiDevice {@link UiDevice} object
+     * @param selector {@link BySelector} of the widget to wait
+     * @param timeoutMS timeout in milliseconds
+     * @return {@link UiObject2} if expected widget appears within timeout, {@code null} otherwise
+     */
+    public static UiObject2 safeWait(UiDevice uiDevice, BySelector selector, long timeoutMS) {
+        try {
+            return uiDevice.wait(Until.findObject(selector), timeoutMS);
+        } catch (Exception e) {
+            Log.e(TAG, "Failed to wait for widget ", e);
+        }
+
+        return null;
+    }
+
+    /**
+     * Waits for a widget without throwing any exception.
+     *
+     * @param uiDevice {@link UiDevice} object
+     * @param selector {@link BySelector} of the widget to wait
+     * @return {@link UiObject2} if expected widget appears within default timeout,
+     *         {@code null} otherwise
+     */
+    public static UiObject2 safeWait(UiDevice uiDevice, BySelector selector) {
+        return safeWait(uiDevice, selector, DEFAULT_UI_WAIT_TIME_MS, DEFAULT_UI_ATTEMPTS_COUNT);
+    }
+
+    /**
+     * Waits for a widget and click on it without throwing any exception.
+     *
+     * @param uiDevice {@link UiDevice} object
+     * @param selector {@link BySelector} of the widget to wait
+     * @return {@code true} if click action was performed, {@code false} otherwise.
+     */
+    public static boolean safeWaitAndClick(UiDevice uiDevice, BySelector selector) {
+        try {
+            waitAndClick(uiDevice, selector);
+            return true;
+        } catch (Exception e) {
+            Log.e(TAG, "Failed to wait and click for widget ", e);
+        }
+
+        return false;
+    }
+
+    /**
+     * Waits for a widget and click on it.
+     *
+     * @param uiDevice {@link UiDevice} object
+     * @param selector {@link BySelector} of the widget to wait
+     */
+    public static void waitAndClick(UiDevice uiDevice, BySelector selector)
+            throws Exception {
+        waitAndClick(uiDevice, selector, DEFAULT_UI_WAIT_TIME_MS, DEFAULT_UI_ATTEMPTS_COUNT);
+    }
+
+    /**
+     * Waits and click on a child widget without throwing any exception.
+     *
+     * @param uiDevice {@link UiDevice} object
+     * @param parent {@link BySelector} of the parent widget to wait
+     * @param child {@link BySelector} of the child widget to wait
+     * @param timeoutMS timeout in milliseconds
+     * @return {@code true} if click action was performed, {@code false} otherwise
+     */
+    public static boolean safeWaitAndClick(UiDevice uiDevice, BySelector parent, BySelector child,
+            long timeoutMS) {
+        try {
+            waitAndClick(uiDevice, parent, child, timeoutMS);
+            return true;
+        } catch (Exception e) {
+            Log.e(TAG, "Failed to wait and click on child widget ", e);
+        }
+
+        return false;
+    }
+
+    /**
+     * Waits and click on a child widget.
+     *
+     * @param uiDevice {@link UiDevice} object
+     * @param parent {@link BySelector} of the parent widget to wait
+     * @param child {@link BySelector} of the child widget to wait
+     * @param timeoutMS timeout in milliseconds
+     */
+    public static void waitAndClick(UiDevice uiDevice, BySelector parent, BySelector child,
+            long timeoutMS) throws Exception {
+        UiObject2 object = wait(uiDevice, parent, child, timeoutMS);
+        if (object == null) {
+            throw new Exception(String.format("Child widget(%s) not found", child.toString()));
+        }
+        object.click();
+    }
+
+    /**
+     * Waits for a child object into a parent widget.
+     *
+     * @param uiDevice {@link UiDevice} object
+     * @param parent {@link BySelector} of the parent widget to wait
+     * @param child {@link BySelector} of the child widget to wait
+     * @param timeoutMS timeout in milliseconds
+     * @return {@link UiObject2} if expected widget appears within timeout, {@code null} otherwise
+     */
+    public static UiObject2 wait(UiDevice uiDevice, BySelector parent, BySelector child,
+            long timeoutMS) throws Exception {
+
+        UiObject2 parentWidget = WidgetUtils.safeWait(uiDevice, parent, timeoutMS);
+        if (parentWidget == null) {
+            throw new Exception(String.format("Parent(%s) widget not found", parent.toString()));
+        }
+
+        return parentWidget.findObject(child);
+    }
+
+    /**
+     * Waits for a widget and click on it without throwing any exception.
+     *
+     * @param uiDevice {@link UiDevice} object
+     * @param selector {@link BySelector} of the widget to wait
+     * @param timeoutMS timeout in milliseconds
+     * @return {@code true} if click action was performed, {@code false} otherwise.
+     */
+    public static boolean safeWaitAndClick(UiDevice uiDevice, BySelector selector, long timeoutMS) {
+        try {
+            waitAndClick(uiDevice, selector, timeoutMS);
+            return true;
+        } catch (Exception e) {
+            Log.e(TAG, "Failed to wait and click for widget ", e);
+        }
+
+        return false;
+    }
+
+    /**
+     * Waits for a widget and click on it (N attempts).
+     *
+     * @param uiDevice {@link UiDevice} object
+     * @param selector {@link BySelector} of the widget to wait
+     * @param timeoutMS timeout in milliseconds
+     * @param attempts number of attempts
+     */
+    public static void waitAndClick(UiDevice uiDevice, BySelector selector, long timeoutMS,
+            int attempts) throws Exception {
+        for (int i = 0; i < attempts; ++i) {
+            if(safeWaitAndClick(uiDevice, selector, timeoutMS)) {
+                return;
+            }
+        }
+
+        throw new Exception(String.format("UI object not found: %s after %d attempts",
+                selector.toString(),
+                attempts));
+    }
+
+    /**
+     * Waits for a widget and click on it.
+     *
+     * @param uiDevice {@link UiDevice} object
+     * @param selector {@link BySelector} of the widget to wait
+     * @param timeoutMS timeout in milliseconds
+     */
+    public static void waitAndClick(UiDevice uiDevice, BySelector selector, long timeoutMS)
+            throws Exception {
+        UiObject2 object = uiDevice.wait(Until.findObject(selector), timeoutMS);
+        if (object == null) {
+            throw new Exception(String.format("UI object not found: %s", selector.toString()));
+        }
+        object.click();
+    }
+
+    /**
+     * Waits for all elements with given {@link BySelector} to be gone.
+     *
+     * @param uiDevice {@link UiDevice} object
+     * @param selector {@link BySelector} of the UI elements to wait
+     * @param timeoutMs timeout in mmilliseconds
+     */
+    public static void waitToBeGone(UiDevice uiDevice, BySelector selector, long timeoutMs)
+            throws Exception {
+        uiDevice.wait(Until.gone(selector), timeoutMs);
+    }
+
+    /**
+     * Waits for all elements with given {@link BySelector} to be gone.
+     *
+     * @param uiDevice {@link UiDevice} object
+     * @param selector {@link BySelector} of the UI elements to wait
+     */
+    public static void waitToBeGone(UiDevice uiDevice, BySelector selector) throws Exception {
+        waitToBeGone(uiDevice, selector, DEFAULT_UI_WAIT_TIME_MS);
+    }
+
+    /**
+     * Scrolls a scrollable UI widget so that an item with certain {@link TextView} text is in view.
+     *
+     * @param container Selector for the scrollable widget.
+     * @param item Text that appears in the target item.
+     * @return The target item as a {@link UiObject} when it first appears, or {@code null} if
+     *         not found.
+     * @throws UiObjectNotFoundException If the scrollable does not exist.
+     */
+    public static UiObject scrollToItem(UiSelector container, String item)
+            throws UiObjectNotFoundException {
+        UiSelector itemSelector = new UiSelector().className(TextView.class).text(item);
+        return scrollToItem(container, itemSelector);
+    }
+
+    /**
+     * Scrolls a scrollable UI widget so that a certain item is in view.
+     *
+     * @param container Selector for the scrollable widget.
+     * @param item Selector that specifies the target item.
+     * @return The target item as a {@link UiObject} when it first appears, or {@code null} if
+     *         not found.
+     * @throws UiObjectNotFoundException If the scrollable does not exist.
+     */
+    public static UiObject scrollToItem(UiSelector container, UiSelector item)
+            throws UiObjectNotFoundException {
+        UiScrollable scrollable = new UiScrollable(container);
+        if (!scrollable.waitForExists(DEFAULT_UI_WAIT_TIME_MS)) {
+            throw new UiObjectNotFoundException("Cannot find scrollable " + scrollable);
+        }
+
+        if (scrollable.scrollIntoView(item)) {
+            return scrollable.getChild(item);
+        } else {
+            return null;
+        }
+    }
+
+    /**
+     * Scrolls vertically a scrollable UI widget so that a certain item is in view.
+     *
+     * @param uiDevice current {@link UiDevice}.
+     * @param container {@link BySelector} of a scrollable container.
+     * @param item {@link BySelector} of an item to scroll to.
+     * @return The target item as a {@link UiObject2} when it first appears, or {@code null} if
+     *         not found.
+     * @throws UiObjectNotFoundException If the scrollable does not exist.
+     */
+    public static UiObject2 scrollToItem(UiDevice uiDevice,
+            BySelector container,
+            BySelector item) throws UiObjectNotFoundException {
+        return scrollToItem(uiDevice, container, item, false);
+    }
+
+    /**
+     * Scrolls a scrollable UI widget so that a certain item is in view.
+     *
+     * @param uiDevice current {@link UiDevice}.
+     * @param container {@link BySelector} of a scrollable container.
+     * @param item {@link BySelector} of an item to scroll to.
+     * @param isHorizontal {@code true} if scrollable is horizontal, {@code false} otherwise.
+     * @return The target item as a {@link UiObject2} when it first appears, or {@code null} if
+     *         not found.
+     * @throws UiObjectNotFoundException If the scrollable does not exist.
+     */
+    public static UiObject2 scrollToItem(UiDevice uiDevice,
+            BySelector container,
+            BySelector item,
+            boolean isHorizontal) throws UiObjectNotFoundException {
+
+        // Find scrollable object
+        final UiObject2 scrollable = safeWait(uiDevice, container);
+        if (scrollable == null) {
+            throw new UiObjectNotFoundException("Cannot find scrollable " + container);
+        }
+
+        setGestureMargins(scrollable, DEFAULT_SWIPE_DEADZONE_PCT);
+
+        // Determine scrolling direction
+        final Direction scrollDirection = isHorizontal ? Direction.RIGHT : Direction.DOWN;
+        final Direction oppositeDirection = Direction.reverse(scrollDirection);
+
+        // Scroll to the beginning of the scrollable
+        while (scrollable.scroll(oppositeDirection, DEFAULT_SWIPE_PCT));
+
+        // Scroll while target item is not visible and scrolling is still possible
+        UiObject2 foundItem = scrollable.findObject(item);
+        boolean isScrollingPossible = true;
+        while (foundItem == null && isScrollingPossible) {
+            isScrollingPossible = scrollable.scroll(scrollDirection, DEFAULT_SWIPE_PCT);
+            foundItem = scrollable.findObject(item);
+        }
+        return foundItem;
+    }
+
+    /**
+     * Scrolls a scrollable UI widget so that a certain item is in view and clicks this item.
+     *
+     * @param uiDevice current {@link UiDevice}.
+     * @param container {@link BySelector} of a scrollable container.
+     * @param item {@link BySelector} of an item to scroll to.
+     * @throws UiObjectNotFoundException if either scrollable or item could not be found.
+     */
+    public static void scrollToItemAndClick(UiDevice uiDevice,
+            BySelector container,
+            BySelector item) throws UiObjectNotFoundException {
+
+        scrollToItemAndClick(uiDevice, container, item, false);
+    }
+
+    /**
+     * Scrolls a scrollable UI widget so that a certain item is in view and clicks this item.
+     *
+     * @param uiDevice current {@link UiDevice}.
+     * @param container {@link BySelector} of a scrollable container.
+     * @param item {@link BySelector} of an item to scroll to.
+     * @param isHorizontal {@code true} if scrollable is horizontal, {@code false} otherwise.
+     * @throws UiObjectNotFoundException if either scrollable or item could not be found.
+     */
+    public static void scrollToItemAndClick(UiDevice uiDevice,
+            BySelector container,
+            BySelector item,
+            boolean isHorizontal) throws UiObjectNotFoundException {
+
+        UiObject2 foundItem = scrollToItem(uiDevice, container, item, isHorizontal);
+        if (foundItem == null) {
+            throw new UiObjectNotFoundException(item.toString() + " could not be found.");
+        }
+        foundItem.click();
+    }
+
+    /**
+     * Set target object's gesture margins.
+     * @param target target object.
+     * @param marginPct gesture margin as target's dimension percentage.
+     */
+    private static void setGestureMargins(UiObject2 target, float marginPct) {
+        final Rect bounds = callWithRetry(target::getVisibleBounds, DEFAULT_UI_ATTEMPTS_COUNT);
+        final int horizontalMargin = (int)(bounds.width() * marginPct);
+        final int verticalMargin = (int)(bounds.height() * marginPct);
+        target.setGestureMargins(horizontalMargin,
+                verticalMargin,
+                horizontalMargin,
+                verticalMargin);
+    }
+
+    /**
+     * Gets properties of a {@link UiObject2} as a String, for debugging purpose.
+     *
+     * @param widget {@link UiObject2} to get properties from
+     * @return properties of given {@link UiObject2} as String
+     */
+    public static String getWidgetPropertiesAsString(UiObject2 widget) {
+        try {
+            return String.format("text=[%s],desc=[%s],res=[%s],pkg=[%s],class=[%s]",
+                    widget.getText(),
+                    widget.getContentDescription(),
+                    widget.getResourceName(),
+                    widget.getApplicationPackage(),
+                    widget.getClassName());
+        } catch (Exception e) {
+            Log.e(TAG, "Failed to get properties from a widget", e);
+        }
+
+        return null;
+    }
+
+    /**
+     * Gets the package name of a {@link UiObject2} safely.
+     *
+     * @param widget {@link UiObject2} to get property from
+     * @return package name of given widget or null if there is any error
+     */
+    public static String getPackageName(UiObject2 widget) {
+        try {
+            return widget.getApplicationPackage();
+        } catch (Exception e) {
+            Log.e(TAG, "Failed to get package name from a widget", e);
+        }
+
+        return null;
+    }
+
+    /**
+     * Gets the text of a {@link UiObject2} safely.
+     *
+     * @param widget {@link UiObject2} to get property from
+     * @return text of given widget or null if there is any error
+     */
+    public static String getText(UiObject2 widget) {
+        try {
+            return widget.getText();
+        } catch (Exception e) {
+            Log.e(TAG, "Failed to get text from a widget", e);
+        }
+
+        return null;
+    }
+
+    /**
+     * Gets the content description of a {@link UiObject2} safely.
+     *
+     * @param widget {@link UiObject2} to get property from
+     * @return content description of given widget or null if there is any error
+     */
+    public static String getContentDescription(UiObject2 widget) {
+        try {
+            return widget.getContentDescription();
+        } catch (Exception e) {
+            Log.e(TAG, "Failed to get text from a widget", e);
+        }
+
+        return null;
+    }
+
+    /**
+     * Presses back until element specified by either {@code targetSelector}
+     * or {@code limiterSelector} is found. If element specified by {@code targetSelector} is found,
+     * it is returned. {@code null} is returned otherwise.
+     *
+     * @param uiDevice current {@link UiDevice}.
+     * @param targetSelector {@link BySelector} of element to found.
+     * @param limiterSelector {@link BySelector} of boundary element to represent failed search.
+     * @param maxAttempts Max number of attempts
+     * @return element specified by {@code targetSelector} if found, {@code null} otherwise.
+     */
+    public static UiObject2 safeGoBackUntilFound(UiDevice uiDevice, BySelector targetSelector,
+            BySelector limiterSelector, int maxAttempts) {
+        UiObject2 foundObject = null;
+        while (maxAttempts-- > 0
+                && (foundObject = WidgetUtils.safeWait(uiDevice, targetSelector)) == null
+                && WidgetUtils.safeWait(uiDevice, limiterSelector) == null) {
+            uiDevice.pressBack();
+        }
+        return foundObject;
+    }
+
+    /**
+     * Presses back until element specified by either {@code targetSelector}
+     * or {@code limiterSelector} is found. If element specified by {@code targetSelector} is found,
+     * it is returned. {@code null} is returned otherwise.
+     *
+     * @param uiDevice current {@link UiDevice}.
+     * @param targetSelector {@link BySelector} of element to found.
+     * @param limiterSelector {@link BySelector} of boundary element to represent failed search.
+     * @return element specified by {@code targetSelector} if found, {@code null} otherwise.
+     */
+    public static UiObject2 safeGoBackUntilFound(UiDevice uiDevice, BySelector targetSelector,
+        BySelector limiterSelector) {
+        return safeGoBackUntilFound(uiDevice, targetSelector, limiterSelector, 10);
+    }
+
+    /**
+     * Waits until keyboard is visible.
+     *
+     * @param uiDevice current {@link UiDevice}.
+     * @param maxAttempts maximum attempts to find keyboard before reporting failure.
+     * @return {@code true} if keyboard was found and is visible, {@code false} otherwise.
+     * @throws IOException if could not run shell command to get dumpsys info.
+     */
+    public static boolean waitForKeyboard(UiDevice uiDevice, int maxAttempts) throws IOException {
+        while ((maxAttempts--) > 0) {
+            SystemClock.sleep(KEYBOARD_START_TIME_MS);
+            final String output = uiDevice.executeShellCommand(FIND_KEYBOARD_COMMAND);
+            if (output.contains(KEYBOARD_IS_SHOWN_STRING)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Executes provided action. In case the call throws
+     * {@link StaleObjectException} it is retried at most {@code maxAttempts} times.
+     *
+     * @param action action to execute.
+     * @param maxAttempts maximum attempt to perform.
+     * @param <T> return type of provided action.
+     * @return value returned by provided action.
+     */
+    public static <T> T callWithRetry(Supplier<T> action, int maxAttempts) {
+        if (maxAttempts <= 0) {
+            throw new IllegalArgumentException("maxAttempts must be positive integer value.");
+        }
+
+        T result = null;
+        while ((maxAttempts--) > 0) {
+            try {
+                result = action.get();
+                break;
+            } catch (StaleObjectException e) {
+                if (maxAttempts == 0) {
+                    throw e;
+                }
+            }
+        }
+
+        return result;
+    }
+}
diff --git a/licenses.html b/licenses.html
new file mode 100644
index 0000000..197722a
--- /dev/null
+++ b/licenses.html
@@ -0,0 +1,311 @@
+<html>
+<head>
+  <style>
+    body { margin: 0; font-family: sans-serif; }
+    span { background-color: #eeeeee; display:block; padding: 1em; white-space: pre-wrap; font-family: monospace; font-size: 8pt; }
+    li { list-style: none; }
+    h3 { margin: 0.5em; }
+    p { margin: 1em; white-space: nowrap; }
+  </style>
+</head>
+<body>
+<p>
+  Notice for BouncyCastle<br />
+</p>
+<span>
+Copyright (c) 2000-2015 The Legion of the Bouncy Castle Inc.<br />
+Website: http://www.bouncycastle.org<br />
+
+The MIT License (MIT)
+http://opensource.org/licenses/mit-license.php<br />
+
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the "Software"),
+to deal in the Software without restriction, including without limitation
+the rights to use, copy, modify, merge, publish, distribute, sublicense,
+and/or sell copies of the Software, and to permit persons to whom the
+Software is furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+DEALINGS IN THE SOFTWARE.
+</span>
+<p>
+  Notice for Conscrypt:<br />
+</p>
+<span>
+Website: https://conscrypt.org/<br />
+
+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.</span>
+<p>
+  Notice for JUnit<br />
+</p>
+<span>
+URL: http://junit.org<br />
+
+LICENSE:
+JUNIT
+
+Common Public License - v 1.0
+
+THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS COMMON
+PUBLIC LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR DISTRIBUTION OF
+THE PROGRAM CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT.
+
+1. DEFINITIONS
+
+"Contribution" means:
+
+a) in the case of the initial Contributor, the initial code and
+   documentation distributed under this Agreement, and
+b) in the case of each subsequent Contributor:
+  i)  changes to the Program, and
+  ii) additions to the Program;
+where such changes and/or additions to the Program originate from
+and are distributed by that particular Contributor. A Contribution
+'originates' from a Contributor if it was added to the Program by
+such Contributor itself or anyone acting on such Contributor's behalf.
+Contributions do not include additions to the Program which: (i) are
+separate modules of software distributed in conjunction with the
+Program under their own license agreement, and (ii) are not derivative
+works of the Program.
+
+
+"Contributor" means any person or entity that distributes the Program.
+
+
+"Licensed Patents " mean patent claims licensable by a Contributor which
+are necessarily infringed by the use or sale of its Contribution alone
+or when combined with the Program.
+
+
+"Program" means the Contributions distributed in accordance with this
+ Agreement.
+
+
+"Recipient" means anyone who receives the Program under this Agreement,
+including all Contributors.
+
+
+2. GRANT OF RIGHTS
+
+a) Subject to the terms of this Agreement, each Contributor hereby grants
+Recipient a non-exclusive, worldwide, royalty-free copyright license to
+reproduce, prepare derivative works of, publicly display, publicly perform,
+distribute and sublicense the Contribution of such Contributor, if any,
+and such derivative works, in source code and object code form.
+b) Subject to the terms of this Agreement, each Contributor hereby grants
+Recipient a non-exclusive, worldwide, royalty-free patent license under
+Licensed Patents to make, use, sell, offer to sell, import and otherwise
+transfer the Contribution of such Contributor, if any, in source code and
+object code form. This patent license shall apply to the combination of
+the Contribution and the Program if, at the time the Contribution is added
+by the Contributor, such addition of the Contribution causes such combination
+to be covered by the Licensed Patents. The patent license shall not apply to
+any other combinations which include the Contribution. No hardware per se is
+licensed hereunder.
+c) Recipient understands that although each Contributor grants the
+licenses to its Contributions set forth herein, no assurances are provided
+by any Contributor that the Program does not infringe the patent or other
+intellectual property rights of any other entity. Each Contributor disclaims
+any liability to Recipient for claims brought by any other entity based on
+infringement of intellectual property rights or otherwise. As a condition to
+exercising the rights and licenses granted hereunder, each Recipient hereby
+assumes sole responsibility to secure any other intellectual property rights
+needed, if any. For example, if a third party patent license is required to
+allow Recipient to distribute the Program, it is Recipient's responsibility
+to acquire that license before distributing the Program.
+d) Each Contributor represents that to its knowledge it has sufficient
+copyright rights in its Contribution, if any, to grant the copyright license
+set forth in this Agreement.
+
+3. REQUIREMENTS
+
+A Contributor may choose to distribute the Program in object code form under
+its own license agreement, provided that:
+
+a) it complies with the terms and conditions of this Agreement; and
+b) its license agreement:
+i) effectively disclaims on behalf of all Contributors all warranties and
+conditions, express and implied, including warranties or conditions of title
+and non-infringement, and implied warranties or conditions of merchantability
+and fitness for a particular purpose;
+ii) effectively excludes on behalf of all Contributors all liability for
+damages, including direct, indirect, special, incidental and consequential
+damages, such as lost profits;
+iii) states that any provisions which differ from this Agreement are offered
+by that Contributor alone and not by any other party; and
+iv) states that source code for the Program is available from such Contributor,
+and informs licensees how to obtain it in a reasonable manner on or through a
+medium customarily used for software exchange.
+When the Program is made available in source code form:
+
+a) it must be made available under this Agreement; and
+b) a copy of this Agreement must be included with each copy of the Program.
+
+Contributors may not remove or alter any copyright notices contained within the Program.
+
+
+Each Contributor must identify itself as the originator of its Contribution,
+if any, in a manner that reasonably allows subsequent Recipients to identify
+the originator of the Contribution.
+
+
+4. COMMERCIAL DISTRIBUTION
+
+Commercial distributors of software may accept certain responsibilities with
+respect to end users, business partners and the like. While this license is
+intended to facilitate the commercial use of the Program, the Contributor
+who includes the Program in a commercial product offering should do so in a
+manner which does not create potential liability for other Contributors.
+Therefore, if a Contributor includes the Program in a commercial product
+offering, such Contributor ("Commercial Contributor") hereby agrees to defend
+and indemnify every other Contributor ("Indemnified Contributor") against any
+losses, damages and costs (collectively "Losses") arising from claims, lawsuits
+and other legal actions brought by a third party against the Indemnified
+Contributor to the extent caused by the acts or omissions of such Commercial
+Contributor in connection with its distribution of the Program in a commercial
+product offering. The obligations in this section do not apply to any claims
+or Losses relating to any actual or alleged intellectual property infringement.
+In order to qualify, an Indemnified Contributor must: a) promptly notify the
+Commercial Contributor in writing of such claim, and b) allow the Commercial
+Contributor to control, and cooperate with the Commercial Contributor in, the
+defense and any related settlement negotiations. The Indemnified Contributor
+may participate in any such claim at its own expense.
+
+
+For example, a Contributor might include the Program in a commercial product
+offering, Product X. That Contributor is then a Commercial Contributor. If
+that Commercial Contributor then makes performance claims, or offers warranties
+related to Product X, those performance claims and warranties are such Commercial
+Contributor's responsibility alone. Under this section, the Commercial Contributor
+would have to defend claims against the other Contributors related to those
+performance claims and warranties, and if a court requires any other Contributor
+to pay any damages as a result, the Commercial Contributor must pay those damages.
+
+
+5. NO WARRANTY
+
+EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, THE PROGRAM IS PROVIDED ON AN
+"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER EXPRESS OR
+IMPLIED INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES OR CONDITIONS OF TITLE,
+NON-INFRINGEMENT, MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. Each
+Recipient is solely responsible for determining the appropriateness of using
+and distributing the Program and assumes all risks associated with its exercise
+of rights under this Agreement, including but not limited to the risks and costs
+of program errors, compliance with applicable laws, damage to or loss of data,
+programs or equipment, and unavailability or interruption of operations.
+
+
+6. DISCLAIMER OF LIABILITY
+
+EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, NEITHER RECIPIENT NOR ANY
+CONTRIBUTORS SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING WITHOUT LIMITATION
+LOST PROFITS), HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ARISING IN ANY WAY OUT OF THE USE OR DISTRIBUTION OF THE PROGRAM OR THE
+EXERCISE OF ANY RIGHTS GRANTED HEREUNDER, EVEN IF ADVISED OF THE POSSIBILITY
+OF SUCH DAMAGES.
+
+
+7. GENERAL
+
+If any provision of this Agreement is invalid or unenforceable under applicable
+law, it shall not affect the validity or enforceability of the remainder of
+the terms of this Agreement, and without further action by the parties hereto,
+such provision shall be reformed to the minimum extent necessary to make such
+provision valid and enforceable.
+
+If Recipient institutes patent litigation against a Contributor with respect
+to a patent applicable to software (including a cross-claim or counterclaim
+in a lawsuit), then any patent licenses granted by that Contributor to such
+Recipient under this Agreement shall terminate as of the date such litigation
+is filed. In addition, if Recipient institutes patent litigation against any
+entity (including a cross-claim or counterclaim in a lawsuit) alleging that
+the Program itself (excluding combinations of the Program with other software
+or hardware) infringes such Recipient's patent(s), then such Recipient's rights
+granted under Section 2(b) shall terminate as of the date such litigation is filed.
+
+All Recipient's rights under this Agreement shall terminate if it fails to comply
+with any of the material terms or conditions of this Agreement and does not cure
+such failure in a reasonable period of time after becoming aware of such
+noncompliance. If all Recipient's rights under this Agreement terminate, Recipient
+agrees to cease use and distribution of the Program as soon as reasonably
+practicable. However, Recipient's obligations under this Agreement and any
+licenses granted by Recipient relating to the Program shall continue and survive.
+
+Everyone is permitted to copy and distribute copies of this Agreement, but in
+order to avoid inconsistency the Agreement is copyrighted and may only be
+modified in the following manner. The Agreement Steward reserves the right to
+publish new versions (including revisions) of this Agreement from time to time.
+No one other than the Agreement Steward has the right to modify this Agreement.
+IBM is the initial Agreement Steward. IBM may assign the responsibility to serve
+as the Agreement Steward to a suitable separate entity. Each new version of the
+Agreement will be given a distinguishing version number. The Program (including
+Contributions) may always be distributed subject to the version of the Agreement
+under which it was received. In addition, after a new version of the Agreement is
+published, Contributor may elect to distribute the Program (including its Contributions)
+under the new version. Except as expressly stated in Sections 2(a) and 2(b) above,
+Recipient receives no rights or licenses to the intellectual property of any
+Contributor under this Agreement, whether expressly, by implication, estoppel or
+otherwise. All rights in the Program not expressly granted under this Agreement
+are reserved.
+
+This Agreement is governed by the laws of the State of New York and the intellectual
+property laws of the United States of America. No party to this Agreement will bring
+a legal action under this Agreement more than one year after the cause of action arose.
+Each party waives its rights to a jury trial in any resulting litigation.
+</span>
+<p>
+  Notice for libphonenumber<br />
+</p>
+<span>
+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.</span>
+<p>
+  Notice for OkHttp<br />
+</p>
+<span>
+Copyright 2014 Square, Inc.<br />
+
+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.</span>
+</body>
+</html>
diff --git a/tests/Android.mk b/tests/Android.mk
new file mode 100644
index 0000000..41a41d0
--- /dev/null
+++ b/tests/Android.mk
@@ -0,0 +1,17 @@
+#
+# 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.
+#
+
+include $(call all-subdir-makefiles)
diff --git a/tests/NfcProvisioning/Android.mk b/tests/NfcProvisioning/Android.mk
new file mode 100644
index 0000000..882e7d1
--- /dev/null
+++ b/tests/NfcProvisioning/Android.mk
@@ -0,0 +1,33 @@
+# 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.
+
+LOCAL_PATH:= $(call my-dir)
+
+include $(CLEAR_VARS)
+
+LOCAL_PACKAGE_NAME := AfwTestNfcProvisioningTestCases
+
+LOCAL_MODULE_TAGS := optional
+
+LOCAL_MODULE_PATH := $(TARGET_OUT_DATA_APPS)
+
+LOCAL_STATIC_JAVA_LIBRARIES := ub-uiautomator AfwThCommonLib AfwThUiAutomatorLib android-support-test
+
+LOCAL_SRC_FILES := $(call all-java-files-under, src)
+
+LOCAL_AFW_TEST_MODULE_CONFIG := $(LOCAL_PATH)/$(AFW_TEST_MODULE_TEST_CONFIG)
+
+LOCAL_SDK_VERSION := 22
+
+include $(BUILD_AFW_TEST_PACKAGE)
diff --git a/tests/NfcProvisioning/AndroidManifest.xml b/tests/NfcProvisioning/AndroidManifest.xml
new file mode 100644
index 0000000..5ba861d
--- /dev/null
+++ b/tests/NfcProvisioning/AndroidManifest.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="com.android.afwtest.nfcprovisioning">
+
+    <uses-sdk android:minSdkVersion="22" android:targetSdkVersion="23"/>
+
+    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
+
+    <application>
+        <uses-library android:name="android.test.runner" />
+    </application>
+
+    <instrumentation android:name="android.support.test.runner.AndroidJUnitRunner"
+                     android:targetPackage="com.android.afwtest.nfcprovisioning"
+                     android:label="AfW NFC Provisioning Test Package">
+    </instrumentation>
+</manifest>
diff --git a/tests/NfcProvisioning/AndroidTest.xml b/tests/NfcProvisioning/AndroidTest.xml
new file mode 100644
index 0000000..7572cf2
--- /dev/null
+++ b/tests/NfcProvisioning/AndroidTest.xml
@@ -0,0 +1,44 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+
+<!-- This file contains configurations used to prepare a testing device for running the
+     test in in AfwTestNfcProvisioningTestCases.apk.
+-->
+<configuration description="Test configurations for AfwTestNfcProvisioningTestCases">
+
+    <include name="afw-test-factory-reset"/>
+
+    <!-- General device setup. -->
+    <include name="afw-test-common"/>
+    <!-- Turn on NFC -->
+    <target_preparer class="com.android.tradefed.targetprep.RunCommandTargetPreparer">
+        <option name="run-command" value="svc nfc enable" />
+    </target_preparer>
+
+    <!-- Dump testing environment info to android-cts/repository/logs/ -->
+    <target_preparer class="com.android.afwtest.tradefed.targetprep.AfwTestEnvDumper" >
+        <option name="file-name-prefix" value="AfwTestNfcProvisioningTestCases"/>
+    </target_preparer>
+
+    <target_preparer class="com.android.compatibility.common.tradefed.targetprep.ApkInstaller">
+        <option name="cleanup-apks" value="true"/>
+        <option name="test-file-name" value="AfwTestNfcProvisioningTestCases.apk"/>
+    </target_preparer>
+
+    <test class="com.android.afwtest.tradefed.testtype.AfwAndroidJUnitTest">
+        <option name="package" value="com.android.afwtest.nfcprovisioning"/>
+    </test>
+</configuration>
diff --git a/tests/NfcProvisioning/src/com/android/afwtest/nfcprovisioning/NfcProvisioningTest.java b/tests/NfcProvisioning/src/com/android/afwtest/nfcprovisioning/NfcProvisioningTest.java
new file mode 100644
index 0000000..9199d79
--- /dev/null
+++ b/tests/NfcProvisioning/src/com/android/afwtest/nfcprovisioning/NfcProvisioningTest.java
@@ -0,0 +1,97 @@
+/*
+ * 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.nfcprovisioning;
+
+import static com.android.afwtest.common.test.TestConfig.DEFAULT_TEST_CONFIG_FILE;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+import android.app.admin.DevicePolicyManager;
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.nfc.NfcAdapter;
+import android.nfc.NfcManager;
+import android.support.test.runner.AndroidJUnit4;
+import android.util.Log;
+
+import com.android.afwtest.common.nfcprovisioning.Utils;
+import com.android.afwtest.common.test.TestConfig;
+import com.android.afwtest.uiautomator.provisioning.AutomationDriver;
+import com.android.afwtest.uiautomator.test.AbstractTestCase;
+import com.android.afwtest.uiautomator.test.AfwTestUiWatcher;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/**
+ * NFC provisioning test.
+ */
+@RunWith(AndroidJUnit4.class)
+public class NfcProvisioningTest extends AbstractTestCase {
+
+    private static final String TAG = "afwtest.NfcProvisioningTest";
+
+    /**
+     * {@inheritDoc}
+     */
+    @Before
+    public void setUp() throws Exception {
+        AfwTestUiWatcher.register(getUiDevice());
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @After
+    public void tearDown() throws Exception {
+        AfwTestUiWatcher.unregister(getUiDevice());
+    }
+
+    @Test
+    public void testNfcProvisioning() throws Exception {
+
+        // Skip the test if no nfc
+        if (!getContext().getPackageManager().hasSystemFeature(PackageManager.FEATURE_NFC)) {
+            Log.w(TAG, "Device doesn't support NFC, skipping Nfc provisioning test!");
+            return;
+        }
+
+        // Verify if NFC is available and enabled
+        NfcManager nfcManager = (NfcManager) getContext().getSystemService(Context.NFC_SERVICE);
+        assertNotNull("Failed to get NfcManager", nfcManager);
+        NfcAdapter nfcAdapter = nfcManager.getDefaultAdapter();
+        assertNotNull("No NFC adapter found!", nfcAdapter);
+        assertTrue("NFC is disabled.", nfcAdapter.isEnabled());
+
+        // Start provisioning
+        String deviceAdminPkgName = Utils.startProvisioning(getContext(), DEFAULT_TEST_CONFIG_FILE);
+        assertNotNull(deviceAdminPkgName);
+
+        // Navigate the pages.
+        AutomationDriver driver = new AutomationDriver(getUiDevice());
+        assertTrue("NFC provisioning didn't finish",
+                driver.runNfcProvisioning(TestConfig.getDefault()));
+
+        DevicePolicyManager devicePolicyManager =
+                (DevicePolicyManager) getContext().getSystemService(Context.DEVICE_POLICY_SERVICE);
+
+        // Verify if the device is provisioned.
+        assertTrue("Provisioning failed", devicePolicyManager.isDeviceOwnerApp(deviceAdminPkgName));
+    }
+}
diff --git a/tests/NonSuwPoProvisioning/Android.mk b/tests/NonSuwPoProvisioning/Android.mk
new file mode 100644
index 0000000..69b4063
--- /dev/null
+++ b/tests/NonSuwPoProvisioning/Android.mk
@@ -0,0 +1,33 @@
+# 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.
+
+LOCAL_PATH:= $(call my-dir)
+
+include $(CLEAR_VARS)
+
+LOCAL_PACKAGE_NAME := AfwTestNonSuwPoProvisioningTestCases
+
+LOCAL_MODULE_TAGS := optional
+
+LOCAL_MODULE_PATH := $(TARGET_OUT_DATA_APPS)
+
+LOCAL_STATIC_JAVA_LIBRARIES := ub-uiautomator AfwThCommonLib AfwThUiAutomatorLib android-support-test
+
+LOCAL_SRC_FILES := $(call all-java-files-under, src)
+
+LOCAL_SDK_VERSION := 22
+
+LOCAL_AFW_TEST_MODULE_CONFIG := $(LOCAL_PATH)/$(AFW_TEST_MODULE_TEST_CONFIG)
+
+include $(BUILD_AFW_TEST_PACKAGE)
diff --git a/tests/NonSuwPoProvisioning/AndroidManifest.xml b/tests/NonSuwPoProvisioning/AndroidManifest.xml
new file mode 100644
index 0000000..3db6052
--- /dev/null
+++ b/tests/NonSuwPoProvisioning/AndroidManifest.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="com.android.afwtest.nonsuwpoprovisioning">
+
+    <uses-sdk android:minSdkVersion="22" android:targetSdkVersion="23"/>
+
+    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
+
+    <application>
+        <uses-library android:name="android.test.runner" />
+    </application>
+
+    <instrumentation android:name="android.support.test.runner.AndroidJUnitRunner"
+                     android:targetPackage="com.android.afwtest.nonsuwpoprovisioning"
+                     android:label="AfW Non-SuW PO Provisioning Test Package">
+    </instrumentation>
+</manifest>
diff --git a/tests/NonSuwPoProvisioning/AndroidTest.xml b/tests/NonSuwPoProvisioning/AndroidTest.xml
new file mode 100644
index 0000000..7e0aa4c
--- /dev/null
+++ b/tests/NonSuwPoProvisioning/AndroidTest.xml
@@ -0,0 +1,60 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+
+<!-- This file contains configurations used to prepare a testing device for running the
+     test in in AfwTestNonSuwPoProvisioningTestCases.apk.
+-->
+<configuration description="Test configurations for AfwTestNonSuwPoProvisioningTestCases">
+
+    <!-- Disable adb root option as this test is intended to be running on production build. -->
+    <target_preparer class="com.android.afwtest.tradefed.targetprep.AfwTestAdbRootOptionPreparer">
+        <option name="enable-root-option" value="false"/>
+    </target_preparer>
+
+    <!-- General device setup -->
+    <include name="afw-test-common"/>
+
+    <!-- Connect wifi -->
+    <include name="afw-test-wifi"/>
+
+    <!-- Uninstall TestDpc and reset users. -->
+    <include name="afw-test-uninstall-testdpc-and-reset-users"/>
+
+    <!-- Force-stop Google Search App after the test as it often becomes not responding. -->
+    <target_preparer class="com.android.tradefed.targetprep.RunCommandTargetPreparer">
+        <option name="teardown-command" value="am force-stop com.google.android.googlequicksearchbox"/>
+    </target_preparer>
+
+    <!-- Dump testing environment info to android-cts/repository/logs/ -->
+    <target_preparer class="com.android.afwtest.tradefed.targetprep.AfwTestEnvDumper" >
+        <option name="file-name-prefix" value="AfwTestNonSuwPoProvisioningTestCases"/>
+    </target_preparer>
+
+    <!-- Copy Provisioning-Stats.csv to local file -->
+    <target_preparer class="com.android.afwtest.tradefed.targetprep.AfwTestPullExternalFile" >
+        <option name="remote-file" value="Provisioning-Stats.csv"/>
+        <option name="local-file" value="/stats/NonSuwPoProvisioning/Provisioning-Stats.csv"/>
+    </target_preparer>
+
+    <target_preparer class="com.android.compatibility.common.tradefed.targetprep.ApkInstaller">
+        <option name="cleanup-apks" value="true"/>
+        <option name="test-file-name" value="AfwTestNonSuwPoProvisioningTestCases.apk"/>
+    </target_preparer>
+
+    <test class="com.android.afwtest.tradefed.testtype.AfwAndroidJUnitTest">
+        <option name="package" value="com.android.afwtest.nonsuwpoprovisioning"/>
+    </test>
+</configuration>
diff --git a/tests/NonSuwPoProvisioning/src/com/android/afwtest/nonsuwpoprovisioning/NonSuwPoProvisioningTest.java b/tests/NonSuwPoProvisioning/src/com/android/afwtest/nonsuwpoprovisioning/NonSuwPoProvisioningTest.java
new file mode 100644
index 0000000..f2f505a
--- /dev/null
+++ b/tests/NonSuwPoProvisioning/src/com/android/afwtest/nonsuwpoprovisioning/NonSuwPoProvisioningTest.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.afwtest.nonsuwpoprovisioning;
+
+import static org.junit.Assert.assertTrue;
+
+import android.app.admin.DevicePolicyManager;
+import android.content.Intent;
+import android.support.test.runner.AndroidJUnit4;
+
+import com.android.afwtest.common.AccountManagerUtils;
+import com.android.afwtest.common.test.TestConfig;
+import com.android.afwtest.uiautomator.provisioning.AutomationDriver;
+import com.android.afwtest.uiautomator.test.AbstractTestCase;
+import com.android.afwtest.uiautomator.test.AfwTestUiWatcher;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/**
+ * Test profile owner provisioning started from Settings.
+ */
+@RunWith(AndroidJUnit4.class)
+public class NonSuwPoProvisioningTest extends AbstractTestCase {
+
+    /**
+     * {@inheritDoc}
+     */
+    @Before
+    public void setUp() throws Exception {
+        AfwTestUiWatcher.register(getUiDevice());
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @After
+    public void tearDown() throws Exception {
+        AfwTestUiWatcher.unregister(getUiDevice());
+    }
+
+    /**
+     * Tests non-suw profile owner provisioning flow.
+     */
+    @Test
+    public void testNonSuwPoProvisioning() throws Exception {
+
+        // Start Add Account activity.
+        AccountManagerUtils.startAddGoogleAccountActivity(getContext(), false);
+        AutomationDriver automationDriver = new AutomationDriver(getUiDevice());
+
+        // Navigate provisioning.
+        assertTrue("Non-SuW PO provisioning didn't finish",
+                automationDriver.runNonSuwPoProvisioning(TestConfig.getDefault()));
+    }
+
+    /**
+     * Tests provisioning a non-existing package.
+     */
+    @Test
+    public void testDisallowedProvisioning() throws Exception {
+        Intent startProvisioning = new Intent(DevicePolicyManager.ACTION_PROVISION_MANAGED_PROFILE);
+        // We're asking ManagedProvisioning to provision a non-existing package. It should not be
+        // allowed.
+        startProvisioning.putExtra(
+                DevicePolicyManager.EXTRA_PROVISIONING_DEVICE_ADMIN_PACKAGE_NAME,
+                "package.that.does.not.exist");
+        startProvisioning.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+
+        AutomationDriver automationDriver = new AutomationDriver(getUiDevice());
+        getContext().startActivity(startProvisioning);
+        assertTrue("Non-SuW PO provisioning disallowed didn't finish",
+                automationDriver.runNonSuwPoProvisioningDisallowed(TestConfig.getDefault()));
+    }
+}
diff --git a/tests/QRCodeProvisioning/Android.mk b/tests/QRCodeProvisioning/Android.mk
new file mode 100644
index 0000000..d75171c
--- /dev/null
+++ b/tests/QRCodeProvisioning/Android.mk
@@ -0,0 +1,33 @@
+# 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.
+
+LOCAL_PATH:= $(call my-dir)
+
+include $(CLEAR_VARS)
+
+LOCAL_PACKAGE_NAME := AfwTestQRCodeProvisioningTestCases
+
+LOCAL_MODULE_TAGS := optional
+
+LOCAL_MODULE_PATH := $(TARGET_OUT_DATA_APPS)
+
+LOCAL_STATIC_JAVA_LIBRARIES := ub-uiautomator AfwThCommonLib AfwThUiAutomatorLib android-support-test
+
+LOCAL_SRC_FILES := $(call all-java-files-under, src)
+
+LOCAL_AFW_TEST_MODULE_CONFIG := $(LOCAL_PATH)/$(AFW_TEST_MODULE_TEST_CONFIG)
+
+LOCAL_SDK_VERSION := 24
+
+include $(BUILD_AFW_TEST_PACKAGE)
diff --git a/tests/QRCodeProvisioning/AndroidManifest.xml b/tests/QRCodeProvisioning/AndroidManifest.xml
new file mode 100644
index 0000000..4f52706
--- /dev/null
+++ b/tests/QRCodeProvisioning/AndroidManifest.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="com.android.afwtest.qrcodeprovisioning">
+
+    <uses-sdk android:minSdkVersion="24" android:targetSdkVersion="24"/>
+
+    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
+
+    <application>
+        <uses-library android:name="android.test.runner" />
+    </application>
+
+    <instrumentation android:name="android.support.test.runner.AndroidJUnitRunner"
+                     android:targetPackage="com.android.afwtest.qrcodeprovisioning"
+                     android:label="AfW QR Code Provisioning Test Package">
+    </instrumentation>
+</manifest>
diff --git a/tests/QRCodeProvisioning/AndroidTest.xml b/tests/QRCodeProvisioning/AndroidTest.xml
new file mode 100644
index 0000000..a90a003
--- /dev/null
+++ b/tests/QRCodeProvisioning/AndroidTest.xml
@@ -0,0 +1,43 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+
+<!-- This file contains configurations used to prepare a testing device for running the
+     test in AfwTestQRCodeProvisioningTestCases.apk.
+-->
+<configuration description="Test configurations for AfwTestQRCodeProvisioningTestCases">
+
+    <include name="afw-test-factory-reset"/>
+
+    <!-- General device setup. -->
+    <include name="afw-test-common"/>
+
+    <!-- Connect wifi -->
+    <include name="afw-test-wifi"/>
+
+    <!-- Dump testing environment info to android-cts/repository/logs/ -->
+    <target_preparer class="com.android.afwtest.tradefed.targetprep.AfwTestEnvDumper" >
+        <option name="file-name-prefix" value="AfwTestQRCodeProvisioningTestCases"/>
+    </target_preparer>
+
+    <target_preparer class="com.android.compatibility.common.tradefed.targetprep.ApkInstaller">
+        <option name="cleanup-apks" value="true"/>
+        <option name="test-file-name" value="AfwTestQRCodeProvisioningTestCases.apk"/>
+    </target_preparer>
+
+    <test class="com.android.afwtest.tradefed.testtype.AfwAndroidJUnitTest">
+        <option name="package" value="com.android.afwtest.qrcodeprovisioning"/>
+    </test>
+</configuration>
diff --git a/tests/QRCodeProvisioning/src/com/android/afwtest/qrcodeprovisioning/QRCodeProvisioningTest.java b/tests/QRCodeProvisioning/src/com/android/afwtest/qrcodeprovisioning/QRCodeProvisioningTest.java
new file mode 100644
index 0000000..2cbe953
--- /dev/null
+++ b/tests/QRCodeProvisioning/src/com/android/afwtest/qrcodeprovisioning/QRCodeProvisioningTest.java
@@ -0,0 +1,111 @@
+/*
+ * 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.qrcodeprovisioning;
+
+import static com.android.afwtest.common.test.TestConfig.DEFAULT_TEST_CONFIG_FILE;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+import android.app.admin.DevicePolicyManager;
+import android.content.Context;
+import android.os.SystemClock;
+import android.support.test.runner.AndroidJUnit4;
+import android.support.test.uiautomator.By;
+import android.support.test.uiautomator.BySelector;
+import android.support.test.uiautomator.UiObject2;
+import android.support.test.uiautomator.UiObjectNotFoundException;
+
+import com.android.afwtest.common.qrcodeprovisioning.Utils;
+import com.android.afwtest.common.test.TestConfig;
+import com.android.afwtest.uiautomator.provisioning.AutomationDriver;
+import com.android.afwtest.uiautomator.test.AbstractTestCase;
+import com.android.afwtest.uiautomator.test.AfwTestUiWatcher;
+import com.android.afwtest.uiautomator.utils.WidgetUtils;
+
+import java.util.regex.Pattern;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/**
+ * QR code provisioning test.
+ */
+@RunWith(AndroidJUnit4.class)
+public class QRCodeProvisioningTest extends AbstractTestCase {
+
+    private static final String SUW_PACKAGE_NAME = "com.google.android.setupwizard";
+    private static final int TAP_COUNT = 6;
+    private static final int TAP_INTERVAL = 300;
+
+    private static final int QR_READER_INSTALL_TIMEOUT = 30000;
+    private static final int QR_READER_INSTALL_ATTEMPTS = 6;
+
+    private static final BySelector NEXT_BUTTON_SELECTOR =
+        By.text(Pattern.compile("next", Pattern.CASE_INSENSITIVE)).clickable(true);
+    private static final BySelector SWITCH_CAMERA_BUTTON_SELECTOR =
+        By.res(SUW_PACKAGE_NAME, "switch_camera_button");
+    private static final BySelector WELCOME_TITLE_SELECTOR =
+        By.text(Pattern.compile("welcome|hi there", Pattern.CASE_INSENSITIVE));
+
+    /**
+     * {@inheritDoc}
+     */
+    @Before
+    public void setUp() throws Exception {
+        AfwTestUiWatcher.register(getUiDevice());
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @After
+    public void tearDown() throws Exception {
+        AfwTestUiWatcher.unregister(getUiDevice());
+    }
+
+    /**
+     * Test QR code provisioning flow.
+     */
+    @Test
+    public void testQRCodeProvisioning() throws Exception {
+        UiObject2 welcomeTitle = WidgetUtils.safeWait(getUiDevice(), WELCOME_TITLE_SELECTOR);
+        for (int i = 0; i < TAP_COUNT; i++) {
+            welcomeTitle.click();
+            SystemClock.sleep(TAP_INTERVAL);
+        }
+        WidgetUtils.waitAndClick(getUiDevice(), NEXT_BUTTON_SELECTOR);
+        UiObject2 frontCameraButton =
+            WidgetUtils.safeWait(getUiDevice(), SWITCH_CAMERA_BUTTON_SELECTOR,
+                QR_READER_INSTALL_TIMEOUT, QR_READER_INSTALL_ATTEMPTS);
+        if (frontCameraButton == null) {
+            throw new UiObjectNotFoundException("Cannot identify QR reader");
+        }
+
+        // Start provisioning
+        String deviceAdminPkgName = Utils.startProvisioning(getContext(), DEFAULT_TEST_CONFIG_FILE);
+        assertNotNull(deviceAdminPkgName);
+        AutomationDriver driver = new AutomationDriver(getUiDevice());
+        assertTrue("QR Code provisioning didn't finish",
+                driver.runQRCodeProvisioning(TestConfig.getDefault()));
+        DevicePolicyManager devicePolicyManager =
+                (DevicePolicyManager) getContext().getSystemService(Context.DEVICE_POLICY_SERVICE);
+        // Verify if the device is provisioned.
+        assertTrue("Provisioning failed", devicePolicyManager.isDeviceOwnerApp(deviceAdminPkgName));
+    }
+}
diff --git a/tests/SuwDoProvisioning/Android.mk b/tests/SuwDoProvisioning/Android.mk
new file mode 100644
index 0000000..308e9c1
--- /dev/null
+++ b/tests/SuwDoProvisioning/Android.mk
@@ -0,0 +1,33 @@
+# 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.
+
+LOCAL_PATH:= $(call my-dir)
+
+include $(CLEAR_VARS)
+
+LOCAL_PACKAGE_NAME := AfwTestSuwDoProvisioningTestCases
+
+LOCAL_MODULE_TAGS := optional
+
+LOCAL_MODULE_PATH := $(TARGET_OUT_DATA_APPS)
+
+LOCAL_STATIC_JAVA_LIBRARIES := ub-uiautomator AfwThCommonLib AfwThUiAutomatorLib android-support-test
+
+LOCAL_SRC_FILES := $(call all-java-files-under, src)
+
+LOCAL_SDK_VERSION := 22
+
+LOCAL_AFW_TEST_MODULE_CONFIG := $(LOCAL_PATH)/$(AFW_TEST_MODULE_TEST_CONFIG)
+
+include $(BUILD_AFW_TEST_PACKAGE)
diff --git a/tests/SuwDoProvisioning/AndroidManifest.xml b/tests/SuwDoProvisioning/AndroidManifest.xml
new file mode 100644
index 0000000..9688e98
--- /dev/null
+++ b/tests/SuwDoProvisioning/AndroidManifest.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="com.android.afwtest.suwdoprovisioning">
+
+    <uses-sdk android:minSdkVersion="22" android:targetSdkVersion="23"/>
+
+    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
+
+    <application>
+        <uses-library android:name="android.test.runner" />
+    </application>
+
+    <instrumentation android:name="android.support.test.runner.AndroidJUnitRunner"
+                     android:targetPackage="com.android.afwtest.suwdoprovisioning"
+                     android:label="AfW SUW DO Provisioning Test Package">
+    </instrumentation>
+</manifest>
diff --git a/tests/SuwDoProvisioning/AndroidTest.xml b/tests/SuwDoProvisioning/AndroidTest.xml
new file mode 100644
index 0000000..0e8b9e8
--- /dev/null
+++ b/tests/SuwDoProvisioning/AndroidTest.xml
@@ -0,0 +1,47 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+
+<!-- This file contains configurations used to prepare a testing device for running the
+     test in AfwTestSuwDoProvisioningTestCases.apk.
+-->
+<configuration description="Test configurations for AfwTestSuwDoProvisioningTestCases">
+
+    <!-- Factory Reset Device -->
+    <include name="afw-test-factory-reset"/>
+
+    <!-- General device setup -->
+    <include name="afw-test-common"/>
+
+    <!-- Encrypt Device -->
+    <include name="afw-test-encrypt-device"/>
+
+    <!-- Connect wifi -->
+    <include name="afw-test-wifi"/>
+
+    <!-- Dump testing environment info to android-cts/repository/logs/ -->
+    <target_preparer class="com.android.afwtest.tradefed.targetprep.AfwTestEnvDumper" >
+        <option name="file-name-prefix" value="AfwTestSuwDoProvisioningTestCases"/>
+    </target_preparer>
+
+    <target_preparer class="com.android.compatibility.common.tradefed.targetprep.ApkInstaller">
+        <option name="cleanup-apks" value="true"/>
+        <option name="test-file-name" value="AfwTestSuwDoProvisioningTestCases.apk"/>
+    </target_preparer>
+
+    <test class="com.android.afwtest.tradefed.testtype.AfwAndroidJUnitTest">
+        <option name="package" value="com.android.afwtest.suwdoprovisioning"/>
+    </test>
+</configuration>
diff --git a/tests/SuwDoProvisioning/src/com/android/afwtest/suwdoprovisioning/SuwDoProvisioningTest.java b/tests/SuwDoProvisioning/src/com/android/afwtest/suwdoprovisioning/SuwDoProvisioningTest.java
new file mode 100644
index 0000000..d81eff9
--- /dev/null
+++ b/tests/SuwDoProvisioning/src/com/android/afwtest/suwdoprovisioning/SuwDoProvisioningTest.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.suwdoprovisioning;
+
+import static com.android.afwtest.uiautomator.Constants.TESTDPC_PKG_NAME;
+import static org.junit.Assert.assertTrue;
+
+import android.app.admin.DevicePolicyManager;
+import android.content.Context;
+import android.support.test.runner.AndroidJUnit4;
+
+import com.android.afwtest.common.AccountManagerUtils;
+import com.android.afwtest.common.test.TestConfig;
+import com.android.afwtest.uiautomator.provisioning.AutomationDriver;
+import com.android.afwtest.uiautomator.test.AbstractTestCase;
+import com.android.afwtest.uiautomator.test.AfwTestUiWatcher;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/**
+ * SuW device owner provisioning test.
+ */
+@RunWith(AndroidJUnit4.class)
+public class SuwDoProvisioningTest extends AbstractTestCase{
+
+    /**
+     * {@inheritDoc}
+     */
+    @Before
+    public void setUp() throws Exception {
+        AfwTestUiWatcher.register(getUiDevice());
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @After
+    public void tearDown() throws Exception {
+        AfwTestUiWatcher.unregister(getUiDevice());
+    }
+
+    /**
+     * Tests Device Owner provisioning flow.
+     */
+    @Test
+    public void testDoProvisioning() throws Exception {
+
+        AccountManagerUtils.startAddGoogleAccountActivity(getContext(), true);
+
+        AutomationDriver runner = new AutomationDriver(getUiDevice());
+        assertTrue("SuW DO provisioning didn't finish",
+                runner.runSuwDoProvisioning(TestConfig.getDefault()));
+
+        DevicePolicyManager devicePolicyManager =
+                (DevicePolicyManager) getContext().getSystemService(Context.DEVICE_POLICY_SERVICE);
+
+        // Verify if the device is provisioned.
+        assertTrue("Provisioning failed",
+                devicePolicyManager.isDeviceOwnerApp(TESTDPC_PKG_NAME));
+    }
+}
diff --git a/tests/SuwPoProvisioning/Android.mk b/tests/SuwPoProvisioning/Android.mk
new file mode 100644
index 0000000..49bf2f7
--- /dev/null
+++ b/tests/SuwPoProvisioning/Android.mk
@@ -0,0 +1,33 @@
+# 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.
+
+LOCAL_PATH:= $(call my-dir)
+
+include $(CLEAR_VARS)
+
+LOCAL_PACKAGE_NAME := AfwTestSuwPoProvisioningTestCases
+
+LOCAL_MODULE_TAGS := optional
+
+LOCAL_MODULE_PATH := $(TARGET_OUT_DATA_APPS)
+
+LOCAL_STATIC_JAVA_LIBRARIES := ub-uiautomator AfwThCommonLib AfwThUiAutomatorLib android-support-test
+
+LOCAL_SRC_FILES := $(call all-java-files-under, src)
+
+LOCAL_SDK_VERSION := 22
+
+LOCAL_AFW_TEST_MODULE_CONFIG := $(LOCAL_PATH)/$(AFW_TEST_MODULE_TEST_CONFIG)
+
+include $(BUILD_AFW_TEST_PACKAGE)
diff --git a/tests/SuwPoProvisioning/AndroidManifest.xml b/tests/SuwPoProvisioning/AndroidManifest.xml
new file mode 100644
index 0000000..4e5172d
--- /dev/null
+++ b/tests/SuwPoProvisioning/AndroidManifest.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="com.android.afwtest.suwpoprovisioning">
+
+    <uses-sdk android:minSdkVersion="22" android:targetSdkVersion="23"/>
+
+    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
+
+    <application>
+        <uses-library android:name="android.test.runner" />
+    </application>
+
+    <instrumentation android:name="android.support.test.runner.AndroidJUnitRunner"
+                     android:targetPackage="com.android.afwtest.suwpoprovisioning"
+                     android:label="AfW SUW PO Provisioning Test Package">
+    </instrumentation>
+</manifest>
diff --git a/tests/SuwPoProvisioning/AndroidTest.xml b/tests/SuwPoProvisioning/AndroidTest.xml
new file mode 100644
index 0000000..fe2ef05
--- /dev/null
+++ b/tests/SuwPoProvisioning/AndroidTest.xml
@@ -0,0 +1,57 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+
+<!-- This file contains configurations used to prepare a testing device for running the
+     test in AfwTestSuwPoProvisioningTestCases.apk.
+-->
+<configuration description="Test configurations for AfwTestSuwPoProvisioningTestCases">
+
+    <!-- Factory Reset Device -->
+    <include name="afw-test-factory-reset"/>
+
+    <!-- General device setup -->
+    <include name="afw-test-common"/>
+
+    <!-- Encrypt Device -->
+    <include name="afw-test-encrypt-device"/>
+
+    <!-- Connect wifi -->
+    <include name="afw-test-wifi"/>
+
+    <!-- Remove non-primary user after the test. -->
+    <target_preparer class="com.android.afwtest.tradefed.targetprep.AfwTestUserRemover">
+        <option name="remove-users-before-test" value="false"/>
+    </target_preparer>
+
+    <!-- Force-stop Google Search App after the test as it often becomes not responding. -->
+    <target_preparer class="com.android.tradefed.targetprep.RunCommandTargetPreparer">
+        <option name="teardown-command" value="am force-stop com.google.android.googlequicksearchbox"/>
+    </target_preparer>
+
+    <!-- Dump testing environment info to android-cts/repository/logs/ -->
+    <target_preparer class="com.android.afwtest.tradefed.targetprep.AfwTestEnvDumper" >
+        <option name="file-name-prefix" value="AfwTestSuwPoProvisioningTestCases"/>
+    </target_preparer>
+
+    <target_preparer class="com.android.compatibility.common.tradefed.targetprep.ApkInstaller">
+        <option name="cleanup-apks" value="true"/>
+        <option name="test-file-name" value="AfwTestSuwPoProvisioningTestCases.apk"/>
+    </target_preparer>
+
+    <test class="com.android.afwtest.tradefed.testtype.AfwAndroidJUnitTest">
+        <option name="package" value="com.android.afwtest.suwpoprovisioning"/>
+    </test>
+</configuration>
diff --git a/tests/SuwPoProvisioning/src/com/android/afwtest/suwpoprovisioning/SuwPoProvisioningTest.java b/tests/SuwPoProvisioning/src/com/android/afwtest/suwpoprovisioning/SuwPoProvisioningTest.java
new file mode 100644
index 0000000..da3b6fe
--- /dev/null
+++ b/tests/SuwPoProvisioning/src/com/android/afwtest/suwpoprovisioning/SuwPoProvisioningTest.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.suwpoprovisioning;
+
+import static org.junit.Assert.assertTrue;
+
+import android.support.test.runner.AndroidJUnit4;
+
+import com.android.afwtest.common.test.TestConfig;
+import com.android.afwtest.uiautomator.provisioning.AutomationDriver;
+import com.android.afwtest.uiautomator.test.AbstractTestCase;
+import com.android.afwtest.uiautomator.test.AfwTestUiWatcher;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/**
+ * Test Profile Owner provisioning flow from Setup Wizard.
+ */
+@RunWith(AndroidJUnit4.class)
+public class SuwPoProvisioningTest extends AbstractTestCase {
+
+    /**
+     * {@inheritDoc}
+     */
+    @Before
+    public void setUp() throws Exception {
+        AfwTestUiWatcher.register(getUiDevice());
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @After
+    public void tearDown() throws Exception {
+        AfwTestUiWatcher.unregister(getUiDevice());
+    }
+
+    @Test
+    public void testPoProvisioning() throws Exception {
+
+        AutomationDriver runner = new AutomationDriver(getUiDevice());
+        assertTrue("SuW PO provisioning didn't finish",
+                runner.runSuwPoProvisioning(TestConfig.getDefault()));
+    }
+}
diff --git a/tools/Android.mk b/tools/Android.mk
new file mode 100644
index 0000000..dad6c47
--- /dev/null
+++ b/tools/Android.mk
@@ -0,0 +1,31 @@
+#
+# 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.
+#
+
+AFW_TEST_TF_JAR := $(HOST_OUT_JAVA_LIBRARIES)/afw-test-tradefed.jar
+AFW_TEST_TF_EXEC_PATH ?= $(HOST_OUT_EXECUTABLES)/afw-test-tradefed
+
+afw_test_prebuilt_jar := $(HOST_OUT)/afw-th/android-cts/tools/afw-test-prebuilt.jar
+$(afw_test_prebuilt_jar): PRIVATE_TESTS_DIR := $(HOST_OUT)/afw-th/android-cts/testcases
+$(afw_test_prebuilt_jar): PRIVATE_TOOLS_DIR := $(HOST_OUT)/afw-th/android-cts/tools
+$(afw_test_prebuilt_jar): $(TF_JAR) $(AFW_TEST_TF_JAR) $(AFW_TEST_TF_EXEC_PATH) | $(ACP) $(HOST_OUT_EXECUTABLES)/adb
+	mkdir -p $(PRIVATE_TESTS_DIR)
+	mkdir -p $(PRIVATE_TOOLS_DIR)
+	$(ACP) -fp $(TF_JAR) $(AFW_TEST_TF_JAR) $(AFW_TEST_TF_EXEC_PATH) $(PRIVATE_TOOLS_DIR)
+
+.PHONY: afw-test-tools
+afw-test-tools : $(afw_test_prebuilt_jar)
+
+include $(call all-subdir-makefiles)
diff --git a/tools/prebuilt/CtsDeviceInfo.apk b/tools/prebuilt/CtsDeviceInfo.apk
new file mode 100644
index 0000000..4071caa
--- /dev/null
+++ b/tools/prebuilt/CtsDeviceInfo.apk
Binary files differ
diff --git a/tools/prebuilt/README b/tools/prebuilt/README
new file mode 100644
index 0000000..7bd6280
--- /dev/null
+++ b/tools/prebuilt/README
@@ -0,0 +1,5 @@
+CtsDeviceInfo.apk: prebuilt from oc-release (BID:3842764 OPR1.170323.001)
+https://android-build.googleplex.com/builds/submitted/3842764/cts_arm64/latest
+
+CtsDeviceInfo.apk: prebuilt from oc-release (BID:3751078 OPR1.170221.001)
+https://android-build.googleplex.com/builds/submitted/3751078/cts_arm64/latest
diff --git a/tools/prebuilt/TestDpc.apk b/tools/prebuilt/TestDpc.apk
new file mode 100644
index 0000000..aaf2904
--- /dev/null
+++ b/tools/prebuilt/TestDpc.apk
Binary files differ
diff --git a/tools/tradefed-host/Android.mk b/tools/tradefed-host/Android.mk
new file mode 100644
index 0000000..b2d488b
--- /dev/null
+++ b/tools/tradefed-host/Android.mk
@@ -0,0 +1,47 @@
+#
+# 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.
+#
+
+LOCAL_PATH := $(call my-dir)
+
+include $(CLEAR_VARS)
+
+LOCAL_PROTOC_OPTIMIZE_TYPE := nano
+
+LOCAL_SRC_FILES := $(call all-java-files-under, src)
+LOCAL_SRC_FILES += $(call all-proto-files-under, src)
+LOCAL_SRC_FILES += $(call all-java-files-under, ../../../../cts/common/host-side/tradefed/src)
+
+LOCAL_JAVA_RESOURCE_DIRS := res
+LOCAL_JAVA_RESOURCE_DIRS += ../../../../cts/common/host-side/tradefed/res
+
+LOCAL_MODULE := afw-test-tradefed
+
+LOCAL_MODULE_TAGS := optional
+
+LOCAL_ADDITIONAL_DEPENDENCIES := $(LOCAL_PATH)/Android.mk
+
+LOCAL_JAR_MANIFEST := MANIFEST.mf
+LOCAL_STATIC_JAVA_LIBRARIES := host-framework-protos host-libprotobuf-java-nano
+LOCAL_SUITE_BUILD_NUMBER := $(BUILD_NUMBER_FROM_FILE)
+LOCAL_SUITE_TARGET_ARCH := $(TARGET_ARCH)
+LOCAL_SUITE_NAME := CTS
+LOCAL_SUITE_FULLNAME := "for Work Test Harness"
+LOCAL_SUITE_VERSION := 3.1
+
+include $(BUILD_COMPATIBILITY_SUITE)
+
+# Build all sub-directories
+include $(call all-makefiles-under,$(LOCAL_PATH))
diff --git a/tools/tradefed-host/MANIFEST.mf b/tools/tradefed-host/MANIFEST.mf
new file mode 100644
index 0000000..5528c06
--- /dev/null
+++ b/tools/tradefed-host/MANIFEST.mf
@@ -0,0 +1,3 @@
+Manifest-Version: 1.0
+Main-Class: com.android.cts.tradefed.testtype
+Implementation-Version: %BUILD_NUMBER%
diff --git a/tools/tradefed-host/README b/tools/tradefed-host/README
new file mode 100644
index 0000000..25bffb6
--- /dev/null
+++ b/tools/tradefed-host/README
@@ -0,0 +1,50 @@
+Android For Work (Afw) Test Trade Federation
+-------------------------------------------
+
+AfW Test Trade Federation, afw-test-tradefed for short, is the built on top
+of the cts-tradefed test harness.
+
+It works in a similar manner to cts-tradefed harness but with customized
+features for AFW testing automation, such as customized device preparers.
+
+Configuring afw-test-tradefed
+----------------------------
+
+1. Ensure 'adb' is in your current PATH. adb can be found in the
+Android SDK available from http://developer.android.com
+
+Example:
+  PATH=$PATH:/home/myuser/android-sdk-linux_x86/platform-tools
+
+2. Connect the device to the host machine.
+
+3. Ensure device is visible via 'adb devices'
+
+Using afw-test-tradefed
+----------------------
+
+To run a test plan on a single device:
+
+1. Make sure you have at least one device connected.
+2. Launch the afw-test-tradefed console by running the 'afw-test-tradefed'.
+  The "afw-test-tradefed' script can be found at android-cts/tools/afw-test-tradefed
+  from the android-afw-test.zip you downloaded.
+
+3. Type: 'run cts --plan afw-userdebug-build' to run test plan afw-userdebug-build.
+
+   Type 'list plans' to find all test plans.
+
+Some other useful commands are:
+
+To run a test package:
+'run cts --package <packagename>'
+
+To run a test class:
+'run cts --class <full test class name>'
+
+To shard a plan test run on multiple devices
+'run cts --plan afw --shards <number of shards>
+note: all connected devices must be running the same build
+
+For more options:
+'run cts --help'
diff --git a/tools/tradefed-host/etc/Android.mk b/tools/tradefed-host/etc/Android.mk
new file mode 100644
index 0000000..3d0920e
--- /dev/null
+++ b/tools/tradefed-host/etc/Android.mk
@@ -0,0 +1,26 @@
+#
+# Copyright (C) 2016 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+LOCAL_PATH := $(call my-dir)
+
+include $(CLEAR_VARS)
+
+LOCAL_MODULE_TAGS := optional
+
+LOCAL_PREBUILT_EXECUTABLES := afw-test-tradefed
+
+include $(BUILD_HOST_PREBUILT)
+
diff --git a/tools/tradefed-host/etc/afw-test-tradefed b/tools/tradefed-host/etc/afw-test-tradefed
new file mode 100755
index 0000000..b524042
--- /dev/null
+++ b/tools/tradefed-host/etc/afw-test-tradefed
@@ -0,0 +1,103 @@
+#!/bin/bash
+#
+# 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.
+#
+
+# launcher script for afw-test-tradefed harness
+# Can be used from an Android build environment, or a standalone
+# android-afw-test-harness zip
+
+checkFile() {
+    if [ ! -f "$1" ]; then
+        echo "Unable to locate $1"
+        exit
+    fi;
+}
+
+checkPath() {
+    if ! type -P $1 &> /dev/null; then
+        echo "Unable to find $1 in path."
+        exit
+    fi;
+}
+
+checkPath adb
+checkPath java
+
+# check java version
+JAVA_VERSION=$(java -version 2>&1 | head -n 2 | grep '[ "]1\.[78][\. "$$]')
+if [ "${JAVA_VERSION}" == "" ]; then
+    echo "Wrong java version. 1.7 or 1.8 is required."
+    exit
+fi
+
+# check debug flag and set up remote debugging
+if [ -n "${TF_DEBUG}" ]; then
+  if [ -z "${TF_DEBUG_PORT}" ]; then
+    TF_DEBUG_PORT=10088
+  fi
+  RDBG_FLAG=-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=${TF_DEBUG_PORT}
+fi
+
+# get OS
+HOST=`uname`
+if [ "$HOST" == "Linux" ]; then
+    OS="linux-x86"
+elif [ "$HOST" == "Darwin" ]; then
+    OS="darwin-x86"
+else
+    echo "Unrecognized OS"
+    exit
+fi
+
+# check if in Android build env
+if [ ! -z "${ANDROID_BUILD_TOP}" ]; then
+    if [ ! -z "${ANDROID_HOST_OUT}" ]; then
+      CTS_ROOT=${ANDROID_HOST_OUT}/afw-th
+    else
+      CTS_ROOT=${ANDROID_BUILD_TOP}/${OUT_DIR:-out}/host/${OS}/afw-th
+    fi
+    if [ ! -d ${CTS_ROOT} ]; then
+        echo "Could not find $CTS_ROOT in Android build environment. Try 'make afw-test-harness'"
+        exit
+    fi;
+fi;
+
+if [ -z ${CTS_ROOT} ]; then
+    # assume we're in an extracted afw-test install
+    CTS_ROOT="$(dirname $0)/../.."
+fi;
+
+JAR_DIR=${CTS_ROOT}/android-cts/tools
+JARS="tradefed.jar afw-test-tradefed.jar compatibility-host-util.jar"
+
+for JAR in $JARS; do
+    checkFile ${JAR_DIR}/${JAR}
+    JAR_PATH=${JAR_PATH}:${JAR_DIR}/${JAR}
+done
+
+# load any shared libraries for host-side executables
+LIB_DIR=${CTS_ROOT}/android-cts/lib
+if [ "$HOST" == "Linux" ]; then
+    LD_LIBRARY_PATH=${LIB_DIR}:${LIB_DIR}64:${LD_LIBRARY_PATH}
+    export LD_LIBRARY_PATH
+elif [ "$HOST" == "Darwin" ]; then
+    DYLD_LIBRARY_PATH=${LIB_DIR}:${LIB_DIR}64:${DYLD_LIBRARY_PATH}
+    export DYLD_LIBRARY_PATH
+fi
+
+java $RDBG_FLAG \
+  -cp ${JAR_PATH} -DCTS_ROOT=${CTS_ROOT} com.android.compatibility.common.tradefed.command.CompatibilityConsole "$@"
+
diff --git a/tools/tradefed-host/res/config/afw-test-common-base.xml b/tools/tradefed-host/res/config/afw-test-common-base.xml
new file mode 100644
index 0000000..18ca542
--- /dev/null
+++ b/tools/tradefed-host/res/config/afw-test-common-base.xml
@@ -0,0 +1,38 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2017 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<configuration description="Common device setup configuration base">
+
+    <!-- General device setup. -->
+    <target_preparer class="com.android.tradefed.targetprep.RunCommandTargetPreparer">
+        <!-- Disable heads up notifications. -->
+        <option name="run-command" value="settings put global heads_up_notifications_enabled 0"/>
+        <!-- Skip encryption, works for L only; for M+ it's disabled in nfc bump. -->
+        <option name="run-command" value="setprop persist.sys.no_req_encrypt true"/>
+        <!-- Set device screen always on. -->
+        <option name="run-command" value="svc power stayon true"/>
+        <!-- Turn off NFC to avoid devices sending bumps to each other -->
+        <option name="run-command" value="svc nfc disable" />
+        <!-- Disable package verification -->
+        <option name="run-command" value="settings put global package_verifier_enable 0" />
+        <!-- Increase logcat buffer size -->
+        <option name="run-command" value="logcat -G 16M" />
+    </target_preparer>
+
+    <!-- Push afw-test.props -->
+    <include name="afw-test-push-test-suite-config"/>
+
+</configuration>
diff --git a/tools/tradefed-host/res/config/afw-test-common.xml b/tools/tradefed-host/res/config/afw-test-common.xml
new file mode 100644
index 0000000..f604281
--- /dev/null
+++ b/tools/tradefed-host/res/config/afw-test-common.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+
+<configuration description="Common device setup configuration">
+
+    <!-- Include base common configuration -->
+    <include name="afw-test-common-base" />
+
+</configuration>
diff --git a/tools/tradefed-host/res/config/afw-test-create-managed-profile.xml b/tools/tradefed-host/res/config/afw-test-create-managed-profile.xml
new file mode 100644
index 0000000..6682ee6
--- /dev/null
+++ b/tools/tradefed-host/res/config/afw-test-create-managed-profile.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+
+<configuration description="Create managed profile">
+    <target_preparer class="com.android.afwtest.tradefed.targetprep.AfwTestManagedProfileCreator">
+        <option name="profile-owner-apk" value="TestDpc.apk"/>
+        <option name="profile-owner-component" value="com.afwsamples.testdpc/.DeviceAdminReceiver"/>
+    </target_preparer>
+</configuration>
\ No newline at end of file
diff --git a/tools/tradefed-host/res/config/afw-test-encrypt-device.xml b/tools/tradefed-host/res/config/afw-test-encrypt-device.xml
new file mode 100644
index 0000000..f6a8d9a
--- /dev/null
+++ b/tools/tradefed-host/res/config/afw-test-encrypt-device.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+
+<configuration description="Encrypt device">
+    <target_preparer class="com.android.afwtest.tradefed.targetprep.AfwTestEncryptDevice"/>
+</configuration>
diff --git a/tools/tradefed-host/res/config/afw-test-factory-reset.xml b/tools/tradefed-host/res/config/afw-test-factory-reset.xml
new file mode 100644
index 0000000..a8cf2d0
--- /dev/null
+++ b/tools/tradefed-host/res/config/afw-test-factory-reset.xml
@@ -0,0 +1,47 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+
+<configuration description="Factory reset configuration">
+
+    <!-- Install privileged apps -->
+    <target_preparer class="com.android.afwtest.tradefed.targetprep.AfwTestPrivAppInstaller">
+        <option name="app-filename" value="AfwThSystemUtil.apk"/>
+        <option name="permission-file" value="afw-test-system-util-permissions.xml"/>
+    </target_preparer>
+
+    <!-- Remove Google accounts before and after test to avoid FRP -->
+    <target_preparer class="com.android.afwtest.tradefed.targetprep.AfwTestGoogleAccountRemover"/>
+
+    <!-- Disable package verification so that AfwThDeviceAdmin can be installed-->
+    <target_preparer class="com.android.tradefed.targetprep.RunCommandTargetPreparer">
+        <option name="run-command" value="settings put global package_verifier_enable 0" />
+    </target_preparer>
+
+    <!-- Install device admin app -->
+    <target_preparer class="com.android.compatibility.common.tradefed.targetprep.ApkInstaller">
+        <option name="test-file-name" value="AfwThDeviceAdmin.apk" />
+    </target_preparer>
+
+    <!-- Set device admin -->
+    <target_preparer class="com.android.tradefed.targetprep.RunCommandTargetPreparer">
+        <!-- 'dpm set-active-admin' requires Android min Sdk Version 22 -->
+        <option name="run-command" value="dpm set-active-admin com.android.afwtest.deviceadmin/com.android.afwtest.deviceadmin.AdminReceiver"/>
+    </target_preparer>
+
+    <!-- Factory reset. -->
+    <target_preparer class="com.android.afwtest.tradefed.targetprep.AfwTestFactoryReset"/>
+
+</configuration>
diff --git a/tools/tradefed-host/res/config/afw-test-push-test-suite-config.xml b/tools/tradefed-host/res/config/afw-test-push-test-suite-config.xml
new file mode 100644
index 0000000..fc6cfd1
--- /dev/null
+++ b/tools/tradefed-host/res/config/afw-test-push-test-suite-config.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+
+<configuration description="Push the test suite configuration file: afw-test.props">
+
+    <!-- Push test suite configuration file -->
+    <target_preparer class="com.android.compatibility.common.tradefed.targetprep.FilePusher">
+        <option name="push" value="afw-test.props->/data/local/tmp/afw-test.props"/>
+        <option name="cleanup" value="true"/>
+    </target_preparer>
+
+</configuration>
diff --git a/tools/tradefed-host/res/config/afw-test-remove-users.xml b/tools/tradefed-host/res/config/afw-test-remove-users.xml
new file mode 100644
index 0000000..bc6c8db
--- /dev/null
+++ b/tools/tradefed-host/res/config/afw-test-remove-users.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+
+<configuration description="Remove non-primary users.">
+
+    <!-- Remove all non-primary users before and after the test. -->
+    <target_preparer class="com.android.afwtest.tradefed.targetprep.AfwTestUserRemover">
+        <option name="remove-users-before-test" value="true"/>
+        <option name="remove-users-after-test" value="true"/>
+    </target_preparer>
+
+</configuration>
diff --git a/tools/tradefed-host/res/config/afw-test-uninstall-testdpc-after-test.xml b/tools/tradefed-host/res/config/afw-test-uninstall-testdpc-after-test.xml
new file mode 100644
index 0000000..db392d2
--- /dev/null
+++ b/tools/tradefed-host/res/config/afw-test-uninstall-testdpc-after-test.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+
+<configuration description="Uninstall TestDpc after the test">
+
+    <target_preparer class="com.android.afwtest.tradefed.targetprep.AfwTestAppUninstaller" >
+        <option name="after-test" value="com.afwsamples.testdpc"/>
+        <option name="reboot-before-uninstall" value="true"/>
+    </target_preparer>
+
+</configuration>
diff --git a/tools/tradefed-host/res/config/afw-test-uninstall-testdpc-and-reset-users.xml b/tools/tradefed-host/res/config/afw-test-uninstall-testdpc-and-reset-users.xml
new file mode 100644
index 0000000..43227f8
--- /dev/null
+++ b/tools/tradefed-host/res/config/afw-test-uninstall-testdpc-and-reset-users.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+
+<configuration description="Uninstall TestDpc and reset users, before and after the test">
+
+    <!-- Uninstall TestDpc after the test. -->
+    <include name="afw-test-uninstall-testdpc-after-test"/>
+
+    <!-- Remove all non-primary users before and after the test. -->
+    <include name="afw-test-remove-users"/>
+
+    <!-- Uninstall TestDpc before the test. -->
+    <include name="afw-test-uninstall-testdpc-before-test"/>
+
+</configuration>
diff --git a/tools/tradefed-host/res/config/afw-test-uninstall-testdpc-before-test.xml b/tools/tradefed-host/res/config/afw-test-uninstall-testdpc-before-test.xml
new file mode 100644
index 0000000..5f25742
--- /dev/null
+++ b/tools/tradefed-host/res/config/afw-test-uninstall-testdpc-before-test.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+
+<configuration description="Uninstall TestDpc before the test">
+
+    <target_preparer class="com.android.afwtest.tradefed.targetprep.AfwTestAppUninstaller" >
+        <option name="before-test" value="com.afwsamples.testdpc"/>
+        <option name="reboot-before-uninstall" value="true"/>
+    </target_preparer>
+
+</configuration>
diff --git a/tools/tradefed-host/res/config/afw-test-wifi.xml b/tools/tradefed-host/res/config/afw-test-wifi.xml
new file mode 100644
index 0000000..70428df
--- /dev/null
+++ b/tools/tradefed-host/res/config/afw-test-wifi.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+
+<configuration description="Connect to wifi network">
+
+    <!-- Install device admin app -->
+    <target_preparer class="com.android.compatibility.common.tradefed.targetprep.ApkInstaller">
+        <option name="test-file-name" value="AfwThUtil.apk" />
+    </target_preparer>
+
+    <target_preparer class="com.android.afwtest.tradefed.targetprep.AfwTestWifiPreparer" >
+        <option name="wifi-config-file" value="afw-test.props"/>
+        <option name="wifi-ssid-key" value="wifi_ssid"/>
+        <option name="wifi-security-type-key" value="wifi_security_type"/>
+        <option name="wifi-password-key" value="wifi_password"/>
+        <option name="disconnect-wifi-after-test" value="false"/>
+    </target_preparer>
+
+</configuration>
diff --git a/tools/tradefed-host/res/config/afw-user-build.xml b/tools/tradefed-host/res/config/afw-user-build.xml
new file mode 100644
index 0000000..fb6d96a
--- /dev/null
+++ b/tools/tradefed-host/res/config/afw-user-build.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+<configuration description="Runs AFW test on user build">
+
+  <include name="cts" />
+
+  <option name="compatibility-build-provider:plan" value="afw-user-build" />
+
+  <option name="compatibility:include-filter" value="AfwTestNonSuwPoProvisioningTestCases" />
+
+</configuration>
diff --git a/tools/tradefed-host/res/config/afw-userdebug-build.xml b/tools/tradefed-host/res/config/afw-userdebug-build.xml
new file mode 100644
index 0000000..a13903f
--- /dev/null
+++ b/tools/tradefed-host/res/config/afw-userdebug-build.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+<configuration description="Runs AFW test on user-debug build">
+
+  <include name="cts" />
+
+  <option name="compatibility-build-provider:plan" value="afw-userdebug-build" />
+
+  <option name="compatibility:include-filter" value="AfwTestNfcProvisioningTestCases" />
+  <option name="compatibility:include-filter" value="AfwTestQRCodeProvisioningTestCases" />
+  <option name="compatibility:include-filter" value="AfwTestSuwDoProvisioningTestCases" />
+  <option name="compatibility:include-filter" value="AfwTestSuwPoProvisioningTestCases" />
+
+</configuration>
diff --git a/tools/tradefed-host/res/config/cts.xml b/tools/tradefed-host/res/config/cts.xml
new file mode 100644
index 0000000..76871b9
--- /dev/null
+++ b/tools/tradefed-host/res/config/cts.xml
@@ -0,0 +1,55 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+<configuration
+    description="Runs a test plan from Afw Test Harness installation">
+
+    <build_provider class="com.android.compatibility.common.tradefed.build.CompatibilityBuildProvider" />
+    <option name="compatibility-build-provider:plan" value="cts"/>
+    <device_recovery class="com.android.tradefed.device.WaitDeviceRecovery" >
+        <option name="device-wait-time" value="480000"/>
+        <option name="bootloader-wait-time" value="60000"/>
+        <option name="shell-wait-time" value="60000"/>
+    </device_recovery>
+    <test class="com.android.afwtest.tradefed.testtype.AfwTest">
+        <option name="screenshot-on-failure" value="true"/>
+        <option name="screenshot" value="true"/>
+        <option name="bugreport-on-failure" value="true"/>
+        <option name="primary-abi-only" value="true"/>
+        <option name="skip-all-system-status-check" value="true"/>
+    </test>
+    <logger class="com.android.tradefed.log.FileLogger" />
+    <result_reporter class="com.android.compatibility.common.tradefed.result.ConsoleReporter" />
+    <result_reporter class="com.android.compatibility.common.tradefed.result.ResultReporter" />
+
+    <!-- Retry logic is overwritten in AfwTestWifiPreparer. Please use its wifi-attempts
+         option directly. -->
+    <option name="wifi-attempts" value="1" />
+    <option name="wifi-retry-wait-time" value="30000" />
+
+    <target_preparer class="com.android.tradefed.targetprep.RunCommandTargetPreparer">
+        <option name="run-command" value="rm -rf /sdcard/device-info-files" />
+        <option name="run-command" value="rm -rf /sdcard/report-log-files" />
+    </target_preparer>
+
+    <target_preparer class="com.android.compatibility.common.tradefed.targetprep.DeviceInfoCollector">
+        <option name="apk" value="CtsDeviceInfo.apk"/>
+        <option name="package" value="com.android.compatibility.common.deviceinfo"/>
+        <option name="src-dir" value="/sdcard/device-info-files/"/>
+        <option name="dest-dir" value="device-info-files/"/>
+        <option name="temp-dir" value="temp-device-info-files/"/>
+    </target_preparer>
+
+</configuration>
diff --git a/tools/tradefed-host/res/report/cts_result.xsl b/tools/tradefed-host/res/report/cts_result.xsl
new file mode 100644
index 0000000..c21b26f
--- /dev/null
+++ b/tools/tradefed-host/res/report/cts_result.xsl
@@ -0,0 +1,610 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+
+<!DOCTYPE xsl:stylesheet [ <!ENTITY nbsp "&#160;"> ]>
+<xsl:stylesheet version="2.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
+    <xsl:output method="html" version="1.0" encoding="UTF-8" indent="yes"/>
+
+    <xsl:template match="/">
+
+        <html>
+            <head>
+                <title>Test Report for <xsl:value-of select="TestResult/DeviceInfo/BuildInfo/@build_model" /> - <xsl:value-of select="TestResult/DeviceInfo/BuildInfo/@deviceID"/></title>
+                <script>
+                    function toggle(id) {
+                        e = document.getElementById(id)
+                        e.style.display = e.style.display == "none" ? "block" : "none"
+                    }
+                </script>
+                <STYLE type="text/css">
+                    @import "cts_result.css";
+                </STYLE>
+            </head>
+            <body>
+                <DIV>
+                    <TABLE class="title">
+                        <TR>
+                            <TD width="40%" align="left"><img src="logo.gif"></img></TD>
+                            <TD width="60%" align="left">
+                                <h1>Test Report for <xsl:value-of select="TestResult/DeviceInfo/BuildInfo/@build_model"/> -
+                                    <xsl:value-of select="TestResult/DeviceInfo/BuildInfo/@deviceID"/>
+                                </h1>
+                            </TD>
+                        </TR>
+                    </TABLE>
+                </DIV>
+                <img src="newrule-green.png" align="left"></img>
+
+                <br></br>
+
+                <center>
+                    <a href="#" onclick="toggle('summary');">Show Device Information</a>
+                </center>
+
+                <br></br>
+
+                <DIV id="summary" style="display: none">
+                    <TABLE class="summary">
+                        <TR>
+                            <TH colspan="2">Device Information</TH>
+                        </TR>
+                        <TR>
+                            <TD width="50%">
+                                <!-- Device information -->
+                                <TABLE>
+                                    <TR>
+                                        <TD class="rowtitle">Build Model</TD>
+                                        <TD>
+                                            <xsl:value-of select="TestResult/DeviceInfo/BuildInfo/@build_model"/>
+                                        </TD>
+                                    </TR>
+                                    <TR>
+                                        <TD class="rowtitle">Build Product</TD>
+                                        <TD>
+                                            <xsl:value-of select="TestResult/DeviceInfo/BuildInfo/@buildName"/>
+                                        </TD>
+                                    </TR>
+                                    <TR>
+                                        <TD class="rowtitle">Build Brand</TD>
+                                        <TD>
+                                            <xsl:value-of select="TestResult/DeviceInfo/BuildInfo/@build_brand"/>
+                                        </TD>
+                                    </TR>
+                                    <TR>
+                                        <TD class="rowtitle">Build Manufacturer</TD>
+                                        <TD>
+                                            <xsl:value-of select="TestResult/DeviceInfo/BuildInfo/@build_manufacturer"/>
+                                        </TD>
+                                    </TR>
+                                    <TR>
+                                        <TD class="rowtitle">Device ID</TD>
+                                        <TD>
+                                            <xsl:value-of select="TestResult/DeviceInfo/BuildInfo/@deviceID"/>
+                                        </TD>
+                                    </TR>
+                                    <TR>
+                                        <TD class="rowtitle">Android Version</TD>
+                                        <TD>
+                                            <xsl:value-of select="TestResult/DeviceInfo/BuildInfo/@buildVersion"/>
+                                        </TD>
+                                    </TR>
+                                    <TR>
+                                        <TD class="rowtitle">Build ID</TD>
+                                        <TD>
+                                            <xsl:value-of select="TestResult/DeviceInfo/BuildInfo/@buildID"/>
+                                        </TD>
+                                    </TR>
+                                    <TR>
+                                        <TD class="rowtitle">Build Fingerprint</TD>
+                                        <TD>
+                                            <xsl:value-of select="TestResult/DeviceInfo/BuildInfo/@build_fingerprint"/>
+                                        </TD>
+                                    </TR>
+                                    <TR>
+                                        <TD class="rowtitle">Build ABI</TD>
+                                        <TD>
+                                            <xsl:value-of
+                                              select="TestResult/DeviceInfo/BuildInfo/@build_abi"/>
+                                        </TD>
+                                    </TR>
+                                    <TR>
+                                        <TD class="rowtitle">Build ABI2</TD>
+                                        <TD>
+                                            <xsl:value-of
+                                              select="TestResult/DeviceInfo/BuildInfo/@build_abi2"/>
+                                        </TD>
+                                    </TR>
+                                    <TR>
+                                        <TD class="rowtitle">Android API Level</TD>
+                                        <TD>
+                                            <xsl:value-of select="TestResult/DeviceInfo/BuildInfo/@androidPlatformVersion"/>
+                                        </TD>
+                                    </TR>
+                                    <TR>
+                                        <TD class="rowtitle">Supported Locales</TD>
+                                        <TD>
+                                            <xsl:call-template name="formatDelimitedString">
+                                                <xsl:with-param name="string" select="TestResult/DeviceInfo/BuildInfo/@locales"/>
+                                            </xsl:call-template>
+                                        </TD>
+                                    </TR>
+                                    <TR>
+                                        <TD class="rowtitle">Screen Size</TD>
+                                        <TD>
+                                            <xsl:value-of select="TestResult/DeviceInfo/Screen/@screen_size"/>
+                                        </TD>
+                                    </TR>
+                                    <TR>
+                                        <TD class="rowtitle">Resolution</TD>
+                                        <TD>
+                                            <xsl:value-of select="TestResult/DeviceInfo/Screen/@resolution"/>
+                                        </TD>
+                                    </TR>
+                                    <TR>
+                                        <TD class="rowtitle">Density</TD>
+                                        <TD>
+                                            <xsl:value-of select="TestResult/DeviceInfo/Screen/@screen_density"/>
+                                            (<xsl:value-of select="TestResult/DeviceInfo/Screen/@screen_density_bucket"/>)
+                                        </TD>
+                                    </TR>
+                                    <TR>
+                                        <TD class="rowtitle">Phone number</TD>
+                                        <TD>
+                                            <xsl:value-of select="TestResult/DeviceInfo/PhoneSubInfo/@subscriberId"/>
+                                        </TD>
+                                    </TR>
+                                    <TR>
+                                        <TD class="rowtitle">X dpi</TD>
+                                        <TD>
+                                            <xsl:value-of select="TestResult/DeviceInfo/BuildInfo/@Xdpi"/>
+                                        </TD>
+                                    </TR>
+                                    <TR>
+                                        <TD class="rowtitle">Y dpi</TD>
+                                        <TD>
+                                            <xsl:value-of select="TestResult/DeviceInfo/BuildInfo/@Ydpi"/>
+                                        </TD>
+                                    </TR>
+                                    <TR>
+                                        <TD class="rowtitle">Touch</TD>
+                                        <TD>
+                                            <xsl:value-of select="TestResult/DeviceInfo/BuildInfo/@touch"/>
+                                        </TD>
+                                    </TR>
+                                    <TR>
+                                        <TD class="rowtitle">Navigation</TD>
+                                        <TD>
+                                            <xsl:value-of select="TestResult/DeviceInfo/BuildInfo/@navigation"/>
+                                        </TD>
+                                    </TR>
+                                    <TR>
+                                        <TD class="rowtitle">Keypad</TD>
+                                        <TD>
+                                            <xsl:value-of select="TestResult/DeviceInfo/BuildInfo/@keypad"/>
+                                        </TD>
+                                    </TR>
+                                    <TR>
+                                        <TD class="rowtitle">Network</TD>
+                                        <TD>
+                                            <xsl:value-of select="TestResult/DeviceInfo/BuildInfo/@network"/>
+                                        </TD>
+                                    </TR>
+                                    <TR>
+                                        <TD class="rowtitle">IMEI</TD>
+                                        <TD>
+                                            <xsl:value-of select="TestResult/DeviceInfo/BuildInfo/@imei"/>
+                                        </TD>
+                                    </TR>
+                                    <TR>
+                                        <TD class="rowtitle">IMSI</TD>
+                                        <TD>
+                                            <xsl:value-of select="TestResult/DeviceInfo/BuildInfo/@imsi"/>
+                                        </TD>
+                                    </TR>
+                                    <TR>
+                                        <TD class="rowtitle">Open GL ES Version</TD>
+                                        <TD>
+                                            <xsl:value-of select="TestResult/DeviceInfo/BuildInfo/@openGlEsVersion"/>
+                                        </TD>
+                                    </TR>
+                                    <TR>
+                                        <TD class="rowtitle">Open GL Compressed Texture Formats</TD>
+                                        <TD>
+                                            <UL>
+                                                <xsl:for-each select="TestResult/DeviceInfo/OpenGLCompressedTextureFormatsInfo/TextureFormat">
+                                                    <LI><xsl:value-of select="@name" /></LI>
+                                                </xsl:for-each>
+                                            </UL>
+                                        </TD>
+                                    </TR>
+                                    <TR>
+                                        <TD class="rowtitle">Root Processes</TD>
+                                        <TD>
+                                            <UL>
+                                                <xsl:for-each select="TestResult/DeviceInfo/ProcessInfo/Process[@uid='0']">
+                                                    <LI><xsl:value-of select="@name" /></LI>
+                                                </xsl:for-each>
+                                            </UL>
+                                        </TD>
+                                    </TR>
+
+                                </TABLE>
+                            </TD>
+
+                            <TD width="50%">
+                                <TABLE>
+
+                                    <TR>
+                                        <TD class="rowtitle">Features</TD>
+                                        <TD>
+                                            <xsl:for-each select="TestResult/DeviceInfo/FeatureInfo/Feature[@type='sdk']">
+                                                <xsl:text>[</xsl:text>
+                                                <xsl:choose>
+                                                    <xsl:when test="@available = 'true'">
+                                                        <xsl:text>X</xsl:text>
+                                                    </xsl:when>
+                                                    <xsl:otherwise>
+                                                        <xsl:text>_</xsl:text>
+                                                    </xsl:otherwise>
+                                                </xsl:choose>
+                                                <xsl:text>] </xsl:text>
+
+                                                <xsl:value-of select="@name" />
+                                                <br />
+                                            </xsl:for-each>
+                                        </TD>
+                                    </TR>
+                                    <TR>
+                                        <TD class="rowtitle">Other Features</TD>
+                                        <TD>
+                                            <UL>
+                                                <xsl:for-each select="TestResult/DeviceInfo/FeatureInfo/Feature[@type='other']">
+                                                    <LI><xsl:value-of select="@name" /></LI>
+                                                </xsl:for-each>
+                                            </UL>
+                                        </TD>
+                                    </TR>
+                                    <TR>
+                                        <TD class="rowtitle">System Libraries</TD>
+                                        <TD>
+                                            <UL>
+                                                <xsl:for-each select="TestResult/DeviceInfo/SystemLibrariesInfo/Library">
+                                                    <LI><xsl:value-of select="@name" /></LI>
+                                                </xsl:for-each>
+                                            </UL>
+                                        </TD>
+                                    </TR>
+                                    <TR>
+                                        <TD class="rowtitle">Partitions</TD>
+                                        <TD>
+                                            <pre>
+                                                <xsl:call-template name="formatDelimitedString">
+                                                    <xsl:with-param name="string" select="TestResult/DeviceInfo/BuildInfo/@partitions" />
+                                                    <xsl:with-param name="numTokensPerRow" select="1" />
+                                                </xsl:call-template>
+                                            </pre>
+                                        </TD>
+                                    </TR>
+                                    <TR>
+                                        <TD class="rowtitle">Storage devices</TD>
+                                        <TD>
+                                            <xsl:value-of select="TestResult/DeviceInfo/BuildInfo/@storage_devices"/>
+                                        </TD>
+                                    </TR>
+                                    <TR>
+                                        <TD class="rowtitle">Multi-user support</TD>
+                                        <TD>
+                                            <xsl:value-of select="TestResult/DeviceInfo/BuildInfo/@multi_user"/>
+                                        </TD>
+                                    </TR>
+                                </TABLE>
+                            </TD>
+                        </TR>
+                    </TABLE>
+                    <br />
+                    <br />
+                </DIV>
+
+                <DIV>
+                    <TABLE class="summary">
+                        <TR>
+                            <TH colspan="2">Test Summary</TH>
+                        </TR>
+                        <TR>
+                            <TD class="rowtitle">Afw Test Harness Version</TD>
+                            <TD>3.0.1</TD>
+                        </TR>
+                        <TR>
+                            <TD class="rowtitle">Test timeout</TD>
+                            <TD>
+                                <xsl:value-of select="TestResult/HostInfo/Cts/IntValue[@name='testStatusTimeoutMs']/@value" /> ms
+                            </TD>
+                        </TR>
+                        <TR>
+                            <TD class="rowtitle">Host Info</TD>
+                            <TD>
+                                <xsl:value-of select="TestResult/HostInfo/@name"/>
+                                (<xsl:value-of select="TestResult/HostInfo/Os/@name"/> - <xsl:value-of select="TestResult/HostInfo/Os/@version"/>)
+                            </TD>
+                        </TR>
+                        <TR>
+                            <TD class="rowtitle">Plan name</TD>
+                            <TD>
+                                <xsl:value-of select="TestResult/@testPlan"/>
+                            </TD>
+                        </TR>
+                        <TR>
+                            <TD class="rowtitle">Start time</TD>
+                            <TD>
+                                <xsl:value-of select="TestResult/@starttime"/>
+                            </TD>
+                        </TR>
+                        <TR>
+                            <TD class="rowtitle">End time</TD>
+                            <TD>
+                                <xsl:value-of select="TestResult/@endtime"/>
+                            </TD>
+                        </TR>
+                        <TR>
+                            <TD class="rowtitle">Tests Passed</TD>
+                            <TD>
+                                <xsl:value-of select="TestResult/Summary/@pass"/>
+                            </TD>
+                        </TR>
+                        <TR>
+                            <TD class="rowtitle">Tests Failed</TD>
+                            <TD>
+                                <xsl:value-of select="TestResult/Summary/@failed"/>
+                            </TD>
+                        </TR>
+                        <TR>
+                            <TD class="rowtitle">Tests Timed out</TD>
+                            <TD>
+                                <xsl:value-of select="TestResult/Summary/@timeout"/>
+                            </TD>
+                        </TR>
+                        <TR>
+                            <TD class="rowtitle">Tests Not Executed</TD>
+                            <TD>
+                                <xsl:value-of select="TestResult/Summary/@notExecuted"/>
+                            </TD>
+                        </TR>
+                    </TABLE>
+                </DIV>
+
+                <!-- High level summary of test execution -->
+                <h2 align="center">Test Summary by Package</h2>
+                <DIV>
+                    <TABLE class="testsummary">
+                        <TR>
+                            <TH>Test Package</TH>
+                            <TH>Passed</TH>
+                            <TH>Failed</TH>
+                            <TH>Timed Out</TH>
+                            <TH>Not Executed</TH>
+                            <TH>Total Tests</TH>
+                        </TR>
+                        <xsl:for-each select="TestResult/TestPackage">
+                            <TR>
+                                <TD>
+                                    <xsl:variable name="href"><xsl:value-of select="@appPackageName"/></xsl:variable>
+                                    <a href="#{$href}"><xsl:value-of select="@appPackageName"/></a>
+                                </TD>
+                                <TD>
+                                    <xsl:value-of select="count(TestSuite//Test[@result = 'pass'])"/>
+                                </TD>
+                                <TD>
+                                    <xsl:value-of select="count(TestSuite//Test[@result = 'fail'])"/>
+                                </TD>
+                                <TD>
+                                    <xsl:value-of select="count(TestSuite//Test[@result = 'timeout'])"/>
+                                </TD>
+                                <TD>
+                                    <xsl:value-of select="count(TestSuite//Test[@result = 'notExecuted'])"/>
+                                </TD>
+                                <TD>
+                                    <xsl:value-of select="count(TestSuite//Test)"/>
+                                </TD>
+                            </TR>
+                        </xsl:for-each> <!-- end package -->
+                    </TABLE>
+                </DIV>
+
+                <xsl:call-template name="filteredResultTestReport">
+                    <xsl:with-param name="header" select="'Test Failures'" />
+                    <xsl:with-param name="resultFilter" select="'fail'" />
+                </xsl:call-template>
+
+                <xsl:call-template name="filteredResultTestReport">
+                    <xsl:with-param name="header" select="'Test Timeouts'" />
+                    <xsl:with-param name="resultFilter" select="'timeout'" />
+                </xsl:call-template>
+
+                <h2 align="center">Detailed Test Report</h2>
+                <xsl:call-template name="detailedTestReport" />
+
+            </body>
+        </html>
+    </xsl:template>
+
+    <xsl:template name="filteredResultTestReport">
+        <xsl:param name="header" />
+        <xsl:param name="resultFilter" />
+        <xsl:variable name="numMatching" select="count(TestResult/TestPackage/TestSuite//TestCase/Test[@result=$resultFilter])" />
+        <xsl:if test="$numMatching &gt; 0">
+            <h2 align="center"><xsl:value-of select="$header" /> (<xsl:value-of select="$numMatching"/>)</h2>
+            <xsl:call-template name="detailedTestReport">
+                <xsl:with-param name="resultFilter" select="$resultFilter"/>
+            </xsl:call-template>
+        </xsl:if>
+    </xsl:template>
+
+    <xsl:template name="detailedTestReport">
+        <xsl:param name="resultFilter" />
+        <DIV>
+            <xsl:for-each select="TestResult/TestPackage">
+                <xsl:if test="$resultFilter=''
+                        or count(TestSuite//TestCase/Test[@result=$resultFilter]) &gt; 0">
+
+                    <TABLE class="testdetails">
+                        <TR>
+                            <TD class="package" colspan="3">
+                                <xsl:variable name="href"><xsl:value-of select="@appPackageName"/></xsl:variable>
+                                <a name="{$href}">Compatibility Test Package: <xsl:value-of select="@appPackageName"/>
+                                <xsl:if test="@abi">
+                                  ABI: <xsl:value-of select="@abi"/>
+                                </xsl:if>
+                                </a>
+                            </TD>
+                        </TR>
+
+                        <TR>
+                            <TH width="30%">Test</TH>
+                            <TH width="5%">Result</TH>
+                            <TH>Details</TH>
+                        </TR>
+
+                        <!-- test case -->
+                        <xsl:for-each select="TestSuite//TestCase">
+
+                            <xsl:if test="$resultFilter='' or count(Test[@result=$resultFilter]) &gt; 0">
+                                <!-- emit a blank row before every test suite name -->
+                                <xsl:if test="position()!=1">
+                                    <TR><TD class="testcasespacer" colspan="3"></TD></TR>
+                                </xsl:if>
+
+                                <TR>
+                                    <TD class="testcase" colspan="3">
+                                        <xsl:for-each select="ancestor::TestSuite">
+                                            <xsl:if test="position()!=1">.</xsl:if>
+                                            <xsl:value-of select="@name"/>
+                                        </xsl:for-each>
+                                        <xsl:text>.</xsl:text>
+                                        <xsl:value-of select="@name"/>
+                                    </TD>
+                                </TR>
+                            </xsl:if>
+
+                            <!-- test -->
+                            <xsl:for-each select="Test">
+                                <xsl:if test="$resultFilter='' or $resultFilter=@result">
+                                    <TR>
+                                        <TD class="testname"> -- <xsl:value-of select="@name"/></TD>
+
+                                        <!-- test results -->
+                                        <xsl:choose>
+                                            <xsl:when test="string(@KnownFailure)">
+                                                <!-- "pass" indicates the that test actually passed (results have been inverted already) -->
+                                                <xsl:if test="@result='pass'">
+                                                    <TD class="pass">
+                                                        <div style="text-align: center; margin-left:auto; margin-right:auto;">
+                                                            Known problem
+                                                        </div>
+                                                    </TD>
+                                                    <TD class="failuredetails"></TD>
+                                                </xsl:if>
+
+                                                <!-- "fail" indicates that a known failure actually passed (results have been inverted already) -->
+                                                <xsl:if test="@result='fail'">
+                                                    <TD class="failed">
+                                                        <div style="text-align: center; margin-left:auto; margin-right:auto;">
+                                                            <xsl:value-of select="@result"/>
+                                                        </div>
+                                                    </TD>
+                                                   <TD class="failuredetails">
+                                                        <div class="details">
+                                                            A test that was a known failure actually passed. Please check.
+                                                        </div>
+                                                   </TD>
+                                                </xsl:if>
+                                            </xsl:when>
+
+                                            <xsl:otherwise>
+                                                <xsl:if test="@result='pass'">
+                                                    <TD class="pass">
+                                                        <div style="text-align: center; margin-left:auto; margin-right:auto;">
+                                                            <xsl:value-of select="@result"/>
+                                                        </div>
+                                                    </TD>
+                                                    <TD class="failuredetails"/>
+                                                </xsl:if>
+
+                                                <xsl:if test="@result='fail'">
+                                                    <TD class="failed">
+                                                        <div style="text-align: center; margin-left:auto; margin-right:auto;">
+                                                            <xsl:value-of select="@result"/>
+                                                        </div>
+                                                    </TD>
+                                                    <TD class="failuredetails">
+                                                        <div class="details">
+                                                            <xsl:value-of select="FailedScene/@message"/>
+                                                        </div>
+                                                    </TD>
+                                                </xsl:if>
+
+                                                <xsl:if test="@result='timeout'">
+                                                    <TD class="timeout">
+                                                        <div style="text-align: center; margin-left:auto; margin-right:auto;">
+                                                            <xsl:value-of select="@result"/>
+                                                        </div>
+                                                    <TD class="failuredetails"></TD>
+                                                    </TD>
+                                                </xsl:if>
+
+                                                <xsl:if test="@result='notExecuted'">
+                                                    <TD class="notExecuted">
+                                                        <div style="text-align: center; margin-left:auto; margin-right:auto;">
+                                                            <xsl:value-of select="@result"/>
+                                                        </div>
+                                                    </TD>
+                                                    <TD class="failuredetails"></TD>
+                                                </xsl:if>
+                                            </xsl:otherwise>
+                                        </xsl:choose>
+                                    </TR> <!-- finished with a row -->
+                                </xsl:if>
+                            </xsl:for-each> <!-- end test -->
+                        </xsl:for-each> <!-- end test case -->
+                    </TABLE>
+                </xsl:if>
+            </xsl:for-each> <!-- end test package -->
+        </DIV>
+    </xsl:template>
+
+    <!-- Take a delimited string and insert line breaks after a some number of elements. -->
+    <xsl:template name="formatDelimitedString">
+        <xsl:param name="string" />
+        <xsl:param name="numTokensPerRow" select="10" />
+        <xsl:param name="tokenIndex" select="1" />
+        <xsl:if test="$string">
+            <!-- Requires the last element to also have a delimiter after it. -->
+            <xsl:variable name="token" select="substring-before($string, ';')" />
+            <xsl:value-of select="$token" />
+            <xsl:text>&#160;</xsl:text>
+
+            <xsl:if test="$tokenIndex mod $numTokensPerRow = 0">
+                <br />
+            </xsl:if>
+
+            <xsl:call-template name="formatDelimitedString">
+                <xsl:with-param name="string" select="substring-after($string, ';')" />
+                <xsl:with-param name="numTokensPerRow" select="$numTokensPerRow" />
+                <xsl:with-param name="tokenIndex" select="$tokenIndex + 1" />
+            </xsl:call-template>
+        </xsl:if>
+    </xsl:template>
+
+</xsl:stylesheet>
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")));
+    }
+
+}
diff --git a/zip_exclude.lst b/zip_exclude.lst
new file mode 100644
index 0000000..0fbdb2b
--- /dev/null
+++ b/zip_exclude.lst
@@ -0,0 +1,3 @@
+*resource*
+*repository/logs*
+*repository/results*
\ No newline at end of file