diff options
author | kuantung <kuantung@google.com> | 2017-09-07 12:57:39 -0700 |
---|---|---|
committer | kuantung <kuantung@google.com> | 2017-09-07 12:57:39 -0700 |
commit | 5da3c14f79d42c132c3663775254cb0b2d111319 (patch) | |
tree | 646d291bb7cf65b95219d1037cc1ae2a084e9e2d | |
parent | 307c18ff4aa64197526f06cb1db3d756c389065b (diff) | |
parent | 2f6763757c432159b9e8668669b8b3cb14cff03d (diff) | |
download | AfwTestHarness-oreo-dev.tar.gz |
import history from sso://googleplex-android/platform/test/AfwTestHarness oc-dev b/65199386oreo-dev
155 files changed, 13939 insertions, 0 deletions
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) @@ -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. @@ -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 Binary files differnew file mode 100644 index 0000000..4071caa --- /dev/null +++ b/tools/prebuilt/CtsDeviceInfo.apk 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 Binary files differnew file mode 100644 index 0000000..aaf2904 --- /dev/null +++ b/tools/prebuilt/TestDpc.apk 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 " "> ]> +<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 > 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]) > 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]) > 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> </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 |