fix inconsistent sub directory name for test list file locations
am: 865d98a720
Change-Id: I75445fa4ec24d61ea0b792907f3607199ee06249
diff --git a/build/tasks/continuous_instrumentation_metric_tests.mk b/build/tasks/continuous_instrumentation_metric_tests.mk
index a93649b..90cf9f8 100644
--- a/build/tasks/continuous_instrumentation_metric_tests.mk
+++ b/build/tasks/continuous_instrumentation_metric_tests.mk
@@ -24,16 +24,13 @@
my_package_name := continuous_instrumentation_metric_tests
-my_package_zip :=
-
-ifneq ($(strip $(my_modules)),)
include $(BUILD_SYSTEM)/tasks/tools/package-modules.mk
-name := $(TARGET_PRODUCT)-continuous_instrumentation_metric_tests-$(FILE_NAME_TAG)
-$(call dist-for-goals, continuous_instrumentation_metric_tests, $(my_package_zip):$(name).zip)
-endif
.PHONY: continuous_instrumentation_metric_tests
continuous_instrumentation_metric_tests : $(my_package_zip)
+name := $(TARGET_PRODUCT)-continuous_instrumentation_metric_tests-$(FILE_NAME_TAG)
+$(call dist-for-goals, continuous_instrumentation_metric_tests, $(my_package_zip):$(name).zip)
+
# Also build this when you run "make tests".
tests: continuous_instrumentation_metric_tests
diff --git a/build/tasks/tests/instrumentation_metric_test_list.mk b/build/tasks/tests/instrumentation_metric_test_list.mk
index fd79cb8..8b14aed 100644
--- a/build/tasks/tests/instrumentation_metric_test_list.mk
+++ b/build/tasks/tests/instrumentation_metric_test_list.mk
@@ -12,4 +12,9 @@
# See the License for the specific language governing permissions and
# limitations under the License.
-instrumentation_metric_tests :=
+instrumentation_metric_tests := \
+ crashcollector \
+ DocumentsUIPerfTests \
+ DocumentsUIAppPerfTests \
+ perf-setup.sh \
+ SurfaceComposition
diff --git a/build/tasks/tests/instrumentation_test_list.mk b/build/tasks/tests/instrumentation_test_list.mk
index 8079cb6..9465728 100644
--- a/build/tasks/tests/instrumentation_test_list.mk
+++ b/build/tasks/tests/instrumentation_test_list.mk
@@ -13,15 +13,28 @@
# limitations under the License.
instrumentation_tests := \
+ crashcollector \
ManagedProvisioningTests \
FrameworksCoreTests \
FrameworksServicesTests \
FrameworksUtilsTests \
+ MtpDocumentsProviderTests \
DocumentsUITests \
+ ShellTests \
SystemUITests \
RecyclerViewTests \
+ FrameworksWifiTests \
+ FrameworksTelephonyTests \
ContactsProviderTests \
- AndroidVCardTests \
+ SettingsUnitTests \
TelecomUnitTests \
+ AndroidVCardTests \
+ PermissionFunctionalTests \
+ BlockedNumberProviderTest \
+ SettingsFunctionalTests \
+ LauncherFunctionalTests \
+ DownloadAppFunctionalTests \
+ NotificationFunctionalTests \
DownloadProviderTests \
+ EmergencyInfoTests \
CalendarProviderTests
diff --git a/build/tasks/tests/native_metric_test_list.mk b/build/tasks/tests/native_metric_test_list.mk
index 310ab50..1ca9567 100644
--- a/build/tasks/tests/native_metric_test_list.mk
+++ b/build/tasks/tests/native_metric_test_list.mk
@@ -15,5 +15,7 @@
native_metric_tests := \
binderAddInts \
bionic-benchmarks \
+ crashcollector \
libjavacore-benchmarks \
- mmapPerf
+ mmapPerf \
+ perf-setup.sh
diff --git a/build/tasks/tests/native_test_list.mk b/build/tasks/tests/native_test_list.mk
index 739704c..10f5d2e 100644
--- a/build/tasks/tests/native_test_list.mk
+++ b/build/tasks/tests/native_test_list.mk
@@ -19,6 +19,7 @@
bluetoothtbd_test \
camera2_test \
camera_client_test \
+ crashcollector \
debuggerd_test \
hwui_unit_tests \
init_tests \
@@ -36,11 +37,14 @@
malloc_debug_unit_tests \
memory_replay_tests \
minadbd_test \
+ minikin_tests \
net_test_bluetooth \
net_test_btcore \
net_test_device \
net_test_hci \
net_test_osi \
+ netd_integration_test \
+ netd_unit_test \
pagemap_test \
perfprofd_test \
simpleperf_cpu_hotplug_test \
diff --git a/libraries/annotations/Android.mk b/libraries/annotations/Android.mk
new file mode 100644
index 0000000..9c4c2e1
--- /dev/null
+++ b/libraries/annotations/Android.mk
@@ -0,0 +1,28 @@
+#
+# Copyright (C) 2016 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+LOCAL_PATH := $(call my-dir)
+
+# Build for device side tests
+include $(CLEAR_VARS)
+LOCAL_MODULE := platform-test-annotations
+LOCAL_SRC_FILES := $(call all-java-files-under, src)
+include $(BUILD_STATIC_JAVA_LIBRARY)
+
+# Build for host side tests
+include $(CLEAR_VARS)
+LOCAL_MODULE := platform-test-annotations-host
+LOCAL_SRC_FILES := $(call all-java-files-under, src)
+include $(BUILD_HOST_JAVA_LIBRARY)
diff --git a/libraries/annotations/src/android/platform/test/annotations/ApiTest.java b/libraries/annotations/src/android/platform/test/annotations/ApiTest.java
new file mode 100644
index 0000000..e3cd1f7
--- /dev/null
+++ b/libraries/annotations/src/android/platform/test/annotations/ApiTest.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.platform.test.annotations;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Marks the type of test with purpose of asserting API functionalities and behaviors.
+ *
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target({ElementType.METHOD, ElementType.TYPE})
+public @interface ApiTest {
+
+}
diff --git a/libraries/annotations/src/android/platform/test/annotations/Postsubmit.java b/libraries/annotations/src/android/platform/test/annotations/Postsubmit.java
new file mode 100644
index 0000000..a435159
--- /dev/null
+++ b/libraries/annotations/src/android/platform/test/annotations/Postsubmit.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.platform.test.annotations;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Marks a test that should run as part of the postsubmit suite for platform development. By logic,
+ * all presubmit tests should be automatically included in postsubmit testing. This annotation is
+ * intended for additional tests that asserts behaviors of higher level of complexity and/or with
+ * deeper dependencies into the system.
+ * <p>
+ * @see Presubmit
+ *
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target({ElementType.METHOD, ElementType.TYPE})
+public @interface Postsubmit {
+
+}
diff --git a/libraries/annotations/src/android/platform/test/annotations/Presubmit.java b/libraries/annotations/src/android/platform/test/annotations/Presubmit.java
new file mode 100644
index 0000000..5f08acc
--- /dev/null
+++ b/libraries/annotations/src/android/platform/test/annotations/Presubmit.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.platform.test.annotations;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Marks a test that should run as part of the presubmit suite for platform development.
+ *
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target({ElementType.METHOD, ElementType.TYPE})
+public @interface Presubmit {
+
+}
diff --git a/libraries/annotations/src/android/platform/test/annotations/QualityTest.java b/libraries/annotations/src/android/platform/test/annotations/QualityTest.java
new file mode 100644
index 0000000..a02f902
--- /dev/null
+++ b/libraries/annotations/src/android/platform/test/annotations/QualityTest.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.platform.test.annotations;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Marks the type of test with purpose of evaluating qualities such as performance and stability.
+ *
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target({ElementType.METHOD, ElementType.TYPE})
+public @interface QualityTest {
+
+}
diff --git a/libraries/annotations/src/android/platform/test/annotations/RestrictedBuildTest.java b/libraries/annotations/src/android/platform/test/annotations/RestrictedBuildTest.java
new file mode 100644
index 0000000..adb4147
--- /dev/null
+++ b/libraries/annotations/src/android/platform/test/annotations/RestrictedBuildTest.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.platform.test.annotations;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Marks a test as only suitable for execution on restricted builds. Such builds are typically
+ * user builds for release to end users, as opposed to userdebug or eng builds meant for
+ * development. Tests that only passes on restricted builds typically assert on tightened security
+ * restrictions not implemented on userdebug/eng builds, therefore it's important to distinguish
+ * with this annotation.
+ *
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target({ElementType.METHOD, ElementType.TYPE})
+public @interface RestrictedBuildTest {
+
+}
diff --git a/libraries/annotations/src/android/platform/test/annotations/RootPermissionTest.java b/libraries/annotations/src/android/platform/test/annotations/RootPermissionTest.java
new file mode 100644
index 0000000..03a964d
--- /dev/null
+++ b/libraries/annotations/src/android/platform/test/annotations/RootPermissionTest.java
@@ -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.
+ */
+
+package android.platform.test.annotations;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Marks host test that performs actions requiring root permission, or device test that requires
+ * certain pre-test setup steps only possible with root permission.
+ *
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target({ElementType.METHOD, ElementType.TYPE})
+public @interface RootPermissionTest {
+
+}
diff --git a/libraries/annotations/src/android/platform/test/annotations/SecurityTest.java b/libraries/annotations/src/android/platform/test/annotations/SecurityTest.java
new file mode 100644
index 0000000..8d07d4e
--- /dev/null
+++ b/libraries/annotations/src/android/platform/test/annotations/SecurityTest.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.platform.test.annotations;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Marks the type of test with purpose of evaluating security vulnerabilities.
+ *
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target({ElementType.METHOD, ElementType.TYPE})
+public @interface SecurityTest {
+
+}
diff --git a/libraries/app-helpers/Android.mk b/libraries/app-helpers/Android.mk
new file mode 100644
index 0000000..25d4913
--- /dev/null
+++ b/libraries/app-helpers/Android.mk
@@ -0,0 +1,39 @@
+#
+# Copyright (C) 2016 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT 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 := app-helpers
+LOCAL_STATIC_JAVA_LIBRARIES := launcher-helper-lib base-app-helpers google-camera-app-helper \
+ youtube-app-helper photos-app-helper play-music-app-helper \
+ chrome-app-helper play-store-app-helper play-movies-app-helper \
+ gmail-app-helper maps-app-helper recents-app-helper \
+ facebook-app-helper google-keyboard-app-helper \
+ google-messenger-app-helper reddit-app-helper \
+ play-books-app-helper tunein-app-helper \
+ google-docs-app-helper flightdemo-app-helper
+LOCAL_SRC_FILES := $(call all-java-files-under, src)
+
+include $(BUILD_STATIC_JAVA_LIBRARY)
+
+######################################
+
+include $(CLEAR_VARS)
+LOCAL_MODULE := leanback-app-helpers
+LOCAL_STATIC_JAVA_LIBRARIES := launcher-helper-lib base-app-helpers
+LOCAL_SRC_FILES := $(call all-java-files-under, src)
+
+include $(BUILD_STATIC_JAVA_LIBRARY)
diff --git a/libraries/aupt-lib/AndroidManifest.xml b/libraries/aupt-lib/AndroidManifest.xml
index d7560a7..8e0f65c 100644
--- a/libraries/aupt-lib/AndroidManifest.xml
+++ b/libraries/aupt-lib/AndroidManifest.xml
@@ -18,7 +18,7 @@
<uses-sdk android:minSdkVersion="2" android:targetSdkVersion="21" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.INTERNET" />
- <uses-permission android:name="android.permission.GET_ACCOUNTS" />
+ <uses-permission android:name="android.permission.WAKE_LOCK" />
<application>
<uses-library android:name="android.test.runner"/>
</application>
diff --git a/libraries/aupt-lib/src/android/support/test/aupt/AuptTestCase.java b/libraries/aupt-lib/src/android/support/test/aupt/AuptTestCase.java
index 964ccfc..d7f95b6 100644
--- a/libraries/aupt-lib/src/android/support/test/aupt/AuptTestCase.java
+++ b/libraries/aupt-lib/src/android/support/test/aupt/AuptTestCase.java
@@ -35,6 +35,9 @@
import java.util.regex.Matcher;
import java.util.regex.Pattern;
+import android.accounts.Account;
+import android.accounts.AccountManager;
+import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
@@ -49,6 +52,7 @@
import android.test.InstrumentationTestRunner;
import android.util.Log;
+import junit.framework.Assert;
/**
* Base class for AuptTests.
@@ -197,7 +201,7 @@
mDevice = UiDevice.getInstance(getInstrumentation());
mWatchers = new UiWatchers();
- mWatchers.registerAnrAndCrashWatchers();
+ mWatchers.registerAnrAndCrashWatchers(getInstrumentation());
mDevice.registerWatcher("LockScreenWatcher", new LockScreenWatcher());
mRecordMeminfo = "true".equals(getParams().getString(RECORD_MEMINFO_PARAM, "false"));
@@ -550,4 +554,21 @@
return version;
}
+
+ /**
+ * Get registered accounts
+ * Ensures there is at least one account registered
+ * returns the google account name
+ */
+ public String getRegisteredEmailAccount() {
+ Account[] accounts = AccountManager.get(getInstrumentation().getContext()).getAccounts();
+ Assert.assertTrue("Device doesn't have any account registered", accounts.length >= 1);
+ for(int i =0; i < accounts.length; ++i) {
+ if(accounts[i].type.equals("com.google")) {
+ return accounts[i].name;
+ }
+ }
+
+ throw new RuntimeException("The device is not registered with a google account");
+ }
}
diff --git a/libraries/aupt-lib/src/android/support/test/aupt/AuptTestRunner.java b/libraries/aupt-lib/src/android/support/test/aupt/AuptTestRunner.java
index c91cfd7..75030bf 100644
--- a/libraries/aupt-lib/src/android/support/test/aupt/AuptTestRunner.java
+++ b/libraries/aupt-lib/src/android/support/test/aupt/AuptTestRunner.java
@@ -43,7 +43,9 @@
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.Collections;
+import java.util.Comparator;
import java.util.concurrent.TimeUnit;
import java.util.HashMap;
import java.util.List;
@@ -57,9 +59,9 @@
* collecting bugreports and procrank data while the test is running.
*/
public class AuptTestRunner extends InstrumentationTestRunner {
-
- private static final String TAG = "AuptTestRunner";
private static final String DEFAULT_JAR_PATH = "/data/local/tmp/";
+
+ private static final String LOG_TAG = "AuptTestRunner";
private static final String DEX_OPT_PATH = "aupt-opt";
private static final String PARAM_JARS = "jars";
private Bundle mParams;
@@ -71,6 +73,9 @@
private DataCollector mDataCollector;
private File mResultsDirectory;
+ private boolean mDeleteOldFiles;
+ private long mFileRetainCount;
+
private AuptPrivateTestRunner mRunner = new AuptPrivateTestRunner();
private ClassLoader mLoader = null;
private Context mTargetContextWrapper;
@@ -122,13 +127,20 @@
GraphicsStatsMonitor.DEFAULT_INTERVAL_RATE);
mGraphicsStatsMonitor.setIntervalRate(interval);
}
-
mRunner.addTestListener(new PidChecker());
mResultsDirectory = new File(Environment.getExternalStorageDirectory(),
parseStringParam("outputLocation", "aupt_results"));
if (!mResultsDirectory.exists() && !mResultsDirectory.mkdirs()) {
- Log.w(TAG, "Did not create output directory");
+ Log.w(LOG_TAG, "Did not create output directory");
}
+
+ mFileRetainCount = parseLongParam("fileRetainCount", -1);
+ if (mFileRetainCount == -1) {
+ mDeleteOldFiles = false;
+ } else {
+ mDeleteOldFiles = true;
+ }
+
mDataCollector = new DataCollector(
TimeUnit.MINUTES.toMillis(parseLongParam("bugreportInterval", 0)),
TimeUnit.MINUTES.toMillis(parseLongParam("meminfoInterval", 0)),
@@ -136,6 +148,7 @@
TimeUnit.MINUTES.toMillis(parseLongParam("fragmentationInterval", 0)),
TimeUnit.MINUTES.toMillis(parseLongParam("ionInterval", 0)),
TimeUnit.MINUTES.toMillis(parseLongParam("pagetypeinfoInterval", 0)),
+ TimeUnit.MINUTES.toMillis(parseLongParam("traceInterval", 0)),
mResultsDirectory, this);
String jars = params.getString(PARAM_JARS);
if (jars != null) {
@@ -217,7 +230,7 @@
fos.flush();
fos.close();
} catch (IOException ioe) {
- Log.e(TAG, "error saving progress file", ioe);
+ Log.e(LOG_TAG, "error saving progress file", ioe);
}
}
@@ -270,7 +283,7 @@
List<JankStat> mergedStats = mGraphicsStatsMonitor.aggregateStatsImages();
String mergedStatsString = JankStat.statsListToString(mergedStats);
- Log.d(TAG, "Writing jank metrics to the graphics file");
+ Log.d(LOG_TAG, "Writing jank metrics to the graphics file");
writeGraphicsMessage(mergedStatsString);
}
}
@@ -329,14 +342,14 @@
// but trigger a service ANR first
if (mGenerateAnr) {
Context ctx = getTargetContext();
- Log.d(TAG, "About to induce artificial ANR for debugging");
+ Log.d(LOG_TAG, "About to induce artificial ANR for debugging");
ctx.startService(new Intent(ctx, BadService.class));
// intentional delay to allow the service ANR to happen then resolve
try {
Thread.sleep(BadService.DELAY + BadService.DELAY / 4);
} catch (InterruptedException e) {
// ignore
- Log.d(TAG, "interrupted in wait on BadService");
+ Log.d(LOG_TAG, "interrupted in wait on BadService");
return;
}
} else {
@@ -352,6 +365,10 @@
for (TestCase testCase : mTestCases) {
setInstrumentationIfInstrumentationTestCase(testCase, mInstrumentation);
setupAuptIfAuptTestCase(testCase);
+
+ // Remove device storage as necessary
+ removeOldImagesFromDcimCameraFolder();
+
Thread timeBombThread = null;
if (mTestCaseTimeout > 0) {
timeBombThread = new Thread(timeBomb);
@@ -422,6 +439,32 @@
}
}
+ private void removeOldImagesFromDcimCameraFolder() {
+ if (!mDeleteOldFiles) {
+ return;
+ }
+
+ File dcimFolder = new File(Environment.getExternalStorageDirectory(), "DCIM");
+ if (dcimFolder != null) {
+ File cameraFolder = new File(dcimFolder, "Camera");
+ if (cameraFolder != null) {
+ File[] allMediaFiles = cameraFolder.listFiles();
+ Arrays.sort(allMediaFiles, new Comparator<File> () {
+ public int compare(File f1, File f2) {
+ return Long.valueOf(f1.lastModified()).compareTo(f2.lastModified());
+ }
+ });
+ for (int i = 0; i < allMediaFiles.length - mFileRetainCount; i++) {
+ allMediaFiles[i].delete();
+ }
+ } else {
+ Log.w(LOG_TAG, "No Camera folder found to delete from.");
+ }
+ } else {
+ Log.w(LOG_TAG, "No DCIM folder found to delete from.");
+ }
+ }
+
@Override
public void clearTestListeners() {
mTestListeners.clear();
@@ -458,7 +501,7 @@
@Override
public void addError(Test test, Throwable t) {
- Log.e(TAG, "Caught exception from a test", t);
+ Log.e(LOG_TAG, "Caught exception from a test", t);
if ((t instanceof AuptTerminator)) {
throw (AuptTerminator)t;
} else {
@@ -468,12 +511,12 @@
}
// if previous line did not throw an exception, we are interested to know what
// caused the UI exception
- Log.v(TAG, "Dumping UI hierarchy");
+ Log.v(LOG_TAG, "Dumping UI hierarchy");
try {
UiDevice.getInstance(AuptTestRunner.this).dumpWindowHierarchy(
new File("/data/local/tmp/error_dump.xml"));
} catch (IOException e) {
- Log.w(TAG, "Failed to create UI hierarchy dump for UI error", e);
+ Log.w(LOG_TAG, "Failed to create UI hierarchy dump for UI error", e);
}
}
@@ -596,9 +639,9 @@
@Override
public int onStartCommand(Intent intent, int flags, int id) {
- Log.i(TAG, "in service start -- about to hang");
- try { Thread.sleep(DELAY); } catch (InterruptedException e) { Log.wtf(TAG, e); }
- Log.i(TAG, "service hang finished -- stopping and returning");
+ Log.i(LOG_TAG, "in service start -- about to hang");
+ try { Thread.sleep(DELAY); } catch (InterruptedException e) { Log.wtf(LOG_TAG, e); }
+ Log.i(LOG_TAG, "service hang finished -- stopping and returning");
stopSelf();
return START_NOT_STICKY;
}
diff --git a/libraries/aupt-lib/src/android/support/test/aupt/DataCollector.java b/libraries/aupt-lib/src/android/support/test/aupt/DataCollector.java
index 0ff1636..bec2daa 100644
--- a/libraries/aupt-lib/src/android/support/test/aupt/DataCollector.java
+++ b/libraries/aupt-lib/src/android/support/test/aupt/DataCollector.java
@@ -36,7 +36,7 @@
public class DataCollector {
private static final String TAG = "AuptDataCollector";
private long mBugreportInterval, mMeminfoInterval, mCpuinfoInterval, mFragmentationInterval,
- mIonHeapInterval, mPageTypeInfoInterval;
+ mIonHeapInterval, mPageTypeInfoInterval, mTraceInterval;
private File mResultsDirectory;
private Thread mLoggerThread;
@@ -45,7 +45,7 @@
public DataCollector(long bugreportInterval, long meminfoInterval, long cpuinfoInterval,
long fragmentationInterval, long ionHeapInterval, long pagetypeinfoInterval,
- File outputLocation, Instrumentation intrumentation) {
+ long traceInterval, File outputLocation, Instrumentation intrumentation) {
mBugreportInterval = bugreportInterval;
mMeminfoInterval = meminfoInterval;
mCpuinfoInterval = cpuinfoInterval;
@@ -53,6 +53,7 @@
mIonHeapInterval = ionHeapInterval;
mPageTypeInfoInterval = pagetypeinfoInterval;
mResultsDirectory = outputLocation;
+ mTraceInterval = traceInterval;
mInstrumentation = intrumentation;
}
@@ -74,11 +75,12 @@
private class Logger implements Runnable {
private final long mIntervals[] = {
mBugreportInterval, mMeminfoInterval, mCpuinfoInterval, mFragmentationInterval,
- mIonHeapInterval, mPageTypeInfoInterval
+ mIonHeapInterval, mPageTypeInfoInterval, mTraceInterval
};
private final LogGenerator mLoggers[] = {
new BugreportGenerator(), new CompactMemInfoGenerator(), new CpuInfoGenerator(),
- new FragmentationGenerator(), new IonHeapGenerator(), new PageTypeInfoGenerator()
+ new FragmentationGenerator(), new IonHeapGenerator(), new PageTypeInfoGenerator(),
+ new TraceGenerator()
};
private final long mLastUpdate[] = new long[mLoggers.length];
@@ -226,6 +228,17 @@
}
}
+ private class TraceGenerator implements LogGenerator {
+ @Override
+ public void createLog() throws InterruptedException {
+ try {
+ saveTrace(mResultsDirectory + "/trace-%s.txt");
+ } catch (IOException e) {
+ Log.w(TAG, String.format("Failed to save trace: %s", e.getMessage()));
+ }
+ }
+ }
+
public void saveCompactMeminfo(String filename)
throws FileNotFoundException, IOException, InterruptedException {
saveProcessOutput("dumpsys meminfo -c -S", filename);
@@ -251,6 +264,11 @@
saveProcessOutput("cat /proc/pagetypeinfo", filename);
}
+ public void saveTrace(String filename)
+ throws FileNotFoundException, IOException, InterruptedException {
+ saveProcessOutput("cat /sys/kernel/debug/tracing/trace", filename);
+ }
+
public void saveBugreport(String filename)
throws IOException, InterruptedException {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
diff --git a/libraries/aupt-lib/src/android/support/test/aupt/GraphicsStatsMonitor.java b/libraries/aupt-lib/src/android/support/test/aupt/GraphicsStatsMonitor.java
index ec5f259..ba11c75 100644
--- a/libraries/aupt-lib/src/android/support/test/aupt/GraphicsStatsMonitor.java
+++ b/libraries/aupt-lib/src/android/support/test/aupt/GraphicsStatsMonitor.java
@@ -67,7 +67,9 @@
mIntervalTask = new TimerTask() {
@Override
public void run () {
- grabStatsImage();
+ if (mIsRunning) {
+ grabStatsImage();
+ }
}
};
mIntervalRate = DEFAULT_INTERVAL_RATE;
@@ -205,45 +207,61 @@
while ((line = stream.readLine()) != null) {
String proc = JankStat.StatPattern.PACKAGE.parse(line);
if (proc != null) {
- // Line 1 = "Package: a.b.c"
+ // "Package: a.b.c"
Log.v(TAG, String.format("Found process, %s. Gathering jank info.", proc));
- // Line 2 = "Stats since: ###ns"
+ // "Stats since: ###ns"
line = stream.readLine();
Long since = Long.parseLong(JankStat.StatPattern.STATS_SINCE.parse(line));
- // Line 3 = "Total frames rendered: ###"
+ // "Total frames rendered: ###"
line = stream.readLine();
int total = Integer.valueOf(JankStat.StatPattern.TOTAL_FRAMES.parse(line));
- // Line 4 = "Janky frames: ## (#.#%)"
- // OR "Janky frames: ## (nan%)"
+ // "Janky frames: ## (#.#%)" OR
+ // "Janky frames: ## (nan%)"
line = stream.readLine();
int janky = Integer.valueOf(JankStat.StatPattern.NUM_JANKY.parse(line));
- // Line 5 = "90th percentile: ##ms"
+ // (optional, N+) "50th percentile: ##ms"
line = stream.readLine();
+ int perc50;
+ String parsed50 = JankStat.StatPattern.FRAME_TIME_50TH.parse(line);
+ if (parsed50 != null || !parsed50.isEmpty()) {
+ perc50 = Integer.valueOf(parsed50);
+ line = stream.readLine();
+ } else {
+ perc50 = -1;
+ }
+ // "90th percentile: ##ms"
int perc90 = Integer.valueOf(JankStat.StatPattern.FRAME_TIME_90TH.parse(line));
- // Line 6 = "95th percentile: ##ms"
+ // "95th percentile: ##ms"
line = stream.readLine();
int perc95 = Integer.valueOf(JankStat.StatPattern.FRAME_TIME_95TH.parse(line));
- // Line 7 = "99th percentile: ##ms"
+ // "99th percentile: ##ms"
line = stream.readLine();
int perc99 = Integer.valueOf(JankStat.StatPattern.FRAME_TIME_99TH.parse(line));
- // Line 8 = "Number Missed Vsync: #"
+ // "Slowest frames last 24h: ##ms ##ms ..."
line = stream.readLine();
+ String slowest = JankStat.StatPattern.SLOWEST_FRAMES_24H.parse(line);
+ if (slowest != null && !slowest.isEmpty()) {
+ line = stream.readLine();
+ }
+ // "Number Missed Vsync: #"
int vsync = Integer.valueOf(JankStat.StatPattern.NUM_MISSED_VSYNC.parse(line));
- // Line 9 = "Number High input latency: #"
+ // "Number High input latency: #"
line = stream.readLine();
- int latency = Integer.valueOf(JankStat.StatPattern.NUM_HIGH_INPUT_LATENCY.parse(line));
- // Line 10 = "Number slow UI thread: #"
+ int latency = Integer.valueOf(
+ JankStat.StatPattern.NUM_HIGH_INPUT_LATENCY.parse(line));
+ // "Number slow UI thread: #"
line = stream.readLine();
int ui = Integer.valueOf(JankStat.StatPattern.NUM_SLOW_UI_THREAD.parse(line));
- // Line 11 = "Number Slow bitmap uploads: #"
+ // "Number Slow bitmap uploads: #"
line = stream.readLine();
- int bmp = Integer.valueOf(JankStat.StatPattern.NUM_SLOW_BITMAP_UPLOADS.parse(line));
- // Line 12 = "Number slow issue draw commands: #"
+ int bmp = Integer.valueOf(
+ JankStat.StatPattern.NUM_SLOW_BITMAP_UPLOADS.parse(line));
+ // "Number slow issue draw commands: #"
line = stream.readLine();
int draw = Integer.valueOf(JankStat.StatPattern.NUM_SLOW_DRAW.parse(line));
- JankStat stat = new JankStat(proc, since, total, janky, perc90, perc95, perc99,
- vsync, latency, ui, bmp, draw, 1);
+ JankStat stat = new JankStat(proc, since, total, janky, perc50, perc90, perc95,
+ perc99, slowest, vsync, latency, ui, bmp, draw, 1);
result.add(stat);
}
}
diff --git a/libraries/aupt-lib/src/android/support/test/aupt/JankStat.java b/libraries/aupt-lib/src/android/support/test/aupt/JankStat.java
index b0d5d50..8c555be 100644
--- a/libraries/aupt-lib/src/android/support/test/aupt/JankStat.java
+++ b/libraries/aupt-lib/src/android/support/test/aupt/JankStat.java
@@ -38,9 +38,11 @@
STATS_SINCE(Pattern.compile("\\s*Stats since: (\\d+)ns"), 1),
TOTAL_FRAMES(Pattern.compile("\\s*Total frames rendered: (\\d+)"), 1),
NUM_JANKY(Pattern.compile("\\s*Janky frames: (\\d+) (.*)"), 1),
+ FRAME_TIME_50TH(Pattern.compile("\\s*50th percentile: (\\d+)ms"), 1),
FRAME_TIME_90TH(Pattern.compile("\\s*90th percentile: (\\d+)ms"), 1),
FRAME_TIME_95TH(Pattern.compile("\\s*95th percentile: (\\d+)ms"), 1),
FRAME_TIME_99TH(Pattern.compile("\\s*99th percentile: (\\d+)ms"), 1),
+ SLOWEST_FRAMES_24H(Pattern.compile("\\s*Slowest frames over last 24h: (.*)"), 1),
NUM_MISSED_VSYNC(Pattern.compile("\\s*Number Missed Vsync: (\\d+)"), 1),
NUM_HIGH_INPUT_LATENCY(Pattern.compile("\\s*Number High input latency: (\\d+)"), 1),
NUM_SLOW_UI_THREAD(Pattern.compile("\\s*Number Slow UI thread: (\\d+)"), 1),
@@ -69,9 +71,11 @@
public long statsSince;
public int totalFrames;
public int jankyFrames;
+ public int frameTime50th;
public int frameTime90th;
public int frameTime95th;
public int frameTime99th;
+ public String slowestFrames24h;
public int numMissedVsync;
public int numHighLatency;
public int numSlowUiThread;
@@ -80,16 +84,18 @@
public int aggregateCount;
- public JankStat (String pkg, long since, int total, int janky, int ft90, int ft95,
- int ft99, int vsync, int latency, int slowUi, int slowBmp, int slowDraw,
+ public JankStat (String pkg, long since, int total, int janky, int ft50, int ft90, int ft95,
+ int ft99, String slow24h, int vsync, int latency, int slowUi, int slowBmp, int slowDraw,
int aggCount) {
packageName = pkg;
statsSince = since;
totalFrames = total;
jankyFrames = janky;
+ frameTime50th = ft50;
frameTime90th = ft90;
frameTime95th = ft95;
frameTime99th = ft99;
+ slowestFrames24h = slow24h;
numMissedVsync = vsync;
numHighLatency = latency;
numSlowUiThread = slowUi;
@@ -125,9 +131,11 @@
"\nStats since: " + statsSince +
"\nTotal frames: " + totalFrames +
"\nJanky frames: " + jankyFrames +
+ "\n50th percentile: " + frameTime50th +
"\n90th percentile: " + frameTime90th +
"\n95th percentile: " + frameTime95th +
"\n99th percentile: " + frameTime99th +
+ "\nSlowest frames over last 24h: " + slowestFrames24h +
"\nNumber Missed Vsync: " + numMissedVsync +
"\nNumber High input latency: " + numHighLatency +
"\nNumber Slow UI thread: " + numSlowUiThread +
@@ -145,7 +153,9 @@
* ## = 90, 95, and 99
*/
public static JankStat mergeStatHistory (List<JankStat> statHistory) {
- if (statHistory.size() == 1)
+ if (statHistory.size() == 0)
+ return null;
+ else if (statHistory.size() == 1)
return statHistory.get(0);
String pkg = statHistory.get(0).packageName;
@@ -157,6 +167,7 @@
int totalNumSlowUiThread = 0;
int totalNumSlowBitmap = 0;
int totalNumSlowDraw = 0;
+ String totalSlow24h = "";
for (JankStat stat : statHistory) {
totalTotalFrames += stat.totalFrames;
@@ -166,26 +177,30 @@
totalNumSlowUiThread += stat.numSlowUiThread;
totalNumSlowBitmap += stat.numSlowBitmap;
totalNumSlowDraw += stat.numSlowDraw;
+ totalSlow24h += stat.slowestFrames24h;
}
+ float wgtAvgPercentile50 = 0f;
float wgtAvgPercentile90 = 0f;
float wgtAvgPercentile95 = 0f;
float wgtAvgPercentile99 = 0f;
for (JankStat stat : statHistory) {
float weight = ((float)stat.totalFrames / totalTotalFrames);
Log.v(TAG, String.format("Calculated weight is %f", weight));
+ wgtAvgPercentile90 += stat.frameTime50th * weight;
wgtAvgPercentile90 += stat.frameTime90th * weight;
wgtAvgPercentile95 += stat.frameTime95th * weight;
wgtAvgPercentile99 += stat.frameTime99th * weight;
}
+ int perc50 = (int)Math.ceil(wgtAvgPercentile50);
int perc90 = (int)Math.ceil(wgtAvgPercentile90);
int perc95 = (int)Math.ceil(wgtAvgPercentile95);
int perc99 = (int)Math.ceil(wgtAvgPercentile99);
return new JankStat(pkg, totalStatsSince, totalTotalFrames,
- totalJankyFrames, perc90, perc95, perc99, totalNumMissedVsync,
- totalNumHighLatency, totalNumSlowUiThread, totalNumSlowBitmap,
+ totalJankyFrames, perc50, perc90, perc95, perc99, totalSlow24h,
+ totalNumMissedVsync, totalNumHighLatency, totalNumSlowUiThread, totalNumSlowBitmap,
totalNumSlowDraw, statHistory.size());
}
diff --git a/libraries/aupt-lib/src/android/support/test/aupt/ProcessStatusTracker.java b/libraries/aupt-lib/src/android/support/test/aupt/ProcessStatusTracker.java
index 6ba1a59..d7ddd13 100644
--- a/libraries/aupt-lib/src/android/support/test/aupt/ProcessStatusTracker.java
+++ b/libraries/aupt-lib/src/android/support/test/aupt/ProcessStatusTracker.java
@@ -139,6 +139,7 @@
mPidExclusions.remove(processName);
Log.v(TAG, "Started tracking pid changes: " + processName);
}
+ verifyRunningProcess();
}
@Override
@@ -186,7 +187,7 @@
// Enumerate status for all currently tracked processes
for (String proc : procSet) {
// Execute shell command and parse results
- BufferedReader stream = executeShellCommand(String.format("ps %s", proc));
+ BufferedReader stream = executeShellCommand("ps");
try {
String line;
while ((line = stream.readLine()) != null) {
diff --git a/libraries/aupt-lib/src/android/support/test/aupt/UiWatchers.java b/libraries/aupt-lib/src/android/support/test/aupt/UiWatchers.java
index 41e89d1..a8483da 100644
--- a/libraries/aupt-lib/src/android/support/test/aupt/UiWatchers.java
+++ b/libraries/aupt-lib/src/android/support/test/aupt/UiWatchers.java
@@ -16,13 +16,13 @@
package android.support.test.aupt;
-import android.util.Log;
-
+import android.app.Instrumentation;
+import android.support.test.uiautomator.By;
import android.support.test.uiautomator.UiDevice;
-import android.support.test.uiautomator.UiObject;
-import android.support.test.uiautomator.UiObjectNotFoundException;
-import android.support.test.uiautomator.UiSelector;
+import android.support.test.uiautomator.UiObject2;
import android.support.test.uiautomator.UiWatcher;
+import android.support.test.uiautomator.Until;
+import android.util.Log;
import java.util.ArrayList;
import java.util.Collections;
@@ -39,83 +39,34 @@
* This is a sample watcher looking for ANR and crashes. it closes it and moves on. You should
* create your own watchers and handle error logging properly for your type of tests.
*/
- public void registerAnrAndCrashWatchers() {
-
- UiDevice.getInstance().registerWatcher("ANR", new UiWatcher() {
- @Override
- public boolean checkForCondition() {
- UiObject window = new UiObject(new UiSelector().className(
- "com.android.server.am.AppNotRespondingDialog"));
- String errorText = null;
- if (window.exists()) {
- try {
- errorText = window.getText();
- } catch (UiObjectNotFoundException e) {
- Log.e(LOG_TAG, "dialog gone?", e);
- }
- onAnrDetected(errorText);
- postHandler();
- return true; // triggered
- }
- return false; // no trigger
- }
- });
+ public void registerAnrAndCrashWatchers(Instrumentation instr) {
+ final UiDevice device = UiDevice.getInstance(instr);
// class names may have changed
- UiDevice.getInstance().registerWatcher("ANR2", new UiWatcher() {
+ device.registerWatcher("AnrWatcher", new UiWatcher() {
@Override
public boolean checkForCondition() {
- UiObject window = new UiObject(new UiSelector().packageName("android")
- .textContains("isn't responding."));
- if (window.exists()) {
- String errorText = null;
- try {
- errorText = window.getText();
- } catch (UiObjectNotFoundException e) {
- Log.e(LOG_TAG, "dialog gone?", e);
- }
+ UiObject2 window = device.findObject(
+ By.pkg("android").textContains("isn't responding"));
+ if (window != null) {
+ String errorText = window.getText();
onAnrDetected(errorText);
- postHandler();
+ postHandler(device);
return true; // triggered
}
return false; // no trigger
}
});
- UiDevice.getInstance().registerWatcher("CRASH", new UiWatcher() {
+ device.registerWatcher("CrashWatcher", new UiWatcher() {
@Override
public boolean checkForCondition() {
- UiObject window = new UiObject(new UiSelector().className(
- "com.android.server.am.AppErrorDialog"));
- if (window.exists()) {
- String errorText = null;
- try {
- errorText = window.getText();
- } catch (UiObjectNotFoundException e) {
- Log.e(LOG_TAG, "dialog gone?", e);
- }
+ UiObject2 window = device.findObject(
+ By.pkg("android").textContains("has stopped"));
+ if (window != null) {
+ String errorText = window.getText();
onCrashDetected(errorText);
- postHandler();
- return true; // triggered
- }
- return false; // no trigger
- }
- });
-
- UiDevice.getInstance().registerWatcher("CRASH2", new UiWatcher() {
- @Override
- public boolean checkForCondition() {
- UiObject window = new UiObject(new UiSelector().packageName("android")
- .textContains("has stopped"));
- if (window.exists()) {
- String errorText = null;
- try {
- errorText = window.getText();
- } catch (UiObjectNotFoundException e) {
- Log.e(LOG_TAG, "dialog gone?", e);
- }
- onCrashDetected(errorText);
- postHandler();
+ postHandler(device);
return true; // triggered
}
return false; // no trigger
@@ -125,6 +76,12 @@
Log.i(LOG_TAG, "Registed GUI Exception watchers");
}
+ public void removeAnrAndCrashWatchers(Instrumentation instr) {
+ final UiDevice device = UiDevice.getInstance(instr);
+ device.removeWatcher("AnrWatcher");
+ device.removeWatcher("CrashWatcher");
+ }
+
public void onAnrDetected(String errorText) {
mErrors.add(errorText);
}
@@ -144,20 +101,27 @@
/**
* Current implementation ignores the exception and continues.
*/
- public void postHandler() {
+ public void postHandler(UiDevice device) {
// TODO: Add custom error logging here
- String formatedOutput = String.format("UI Exception Message: %-20s\n", UiDevice
- .getInstance().getCurrentPackageName());
+ String formatedOutput = String.format("UI Exception Message: %-20s\n",
+ device.getCurrentPackageName());
Log.e(LOG_TAG, formatedOutput);
- UiObject buttonOK = new UiObject(new UiSelector().text("OK").enabled(true));
+ UiObject2 buttonMute = device.findObject(By.res("android", "aerr_mute"));
+ if (buttonMute != null) {
+ buttonMute.click();
+ }
+
+ UiObject2 closeAppButton = device.findObject(By.res("android", "aerr_close"));
+ if (closeAppButton != null) {
+ closeAppButton.click();
+ }
+
// sometimes it takes a while for the OK button to become enabled
- buttonOK.waitForExists(5000);
- try {
+ UiObject2 buttonOK = device.findObject(By.text("OK").enabled(true));
+ if (buttonOK != null) {
buttonOK.click();
- } catch (UiObjectNotFoundException e) {
- Log.e(LOG_TAG, "Exception", e);
}
}
}
diff --git a/libraries/base-app-helpers/Android.mk b/libraries/base-app-helpers/Android.mk
new file mode 100644
index 0000000..c46cc84
--- /dev/null
+++ b/libraries/base-app-helpers/Android.mk
@@ -0,0 +1,23 @@
+#
+# Copyright (C) 2016 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT 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 := base-app-helpers
+LOCAL_JAVA_LIBRARIES := ub-uiautomator launcher-helper-lib
+LOCAL_SRC_FILES := $(call all-java-files-under, src)
+
+include $(BUILD_STATIC_JAVA_LIBRARY)
diff --git a/libraries/base-app-helpers/src/android/platform/test/helpers/AbstractChromeHelper.java b/libraries/base-app-helpers/src/android/platform/test/helpers/AbstractChromeHelper.java
new file mode 100644
index 0000000..050a1c9
--- /dev/null
+++ b/libraries/base-app-helpers/src/android/platform/test/helpers/AbstractChromeHelper.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.platform.test.helpers;
+
+import android.app.Instrumentation;
+import android.support.test.uiautomator.Direction;
+
+public abstract class AbstractChromeHelper extends AbstractStandardAppHelper {
+
+ public AbstractChromeHelper(Instrumentation instr) {
+ super(instr);
+ }
+
+ /**
+ * Setup expectations: Chrome is open and on a standard page, i.e. a tab is open.
+ *
+ * This method will open the URL supplied and block until the page is open.
+ */
+ public abstract void openUrl(String url);
+
+ /**
+ * Setup expectations: Chrome is open on a page.
+ *
+ * This method will scroll the page as directed and block until idle.
+ */
+ public abstract void flingPage(Direction dir);
+
+ /**
+ * Setup expectations: Chrome is open on a page.
+ *
+ * This method will open the overload menu, indicated by three dots and block until open.
+ */
+ public abstract void openMenu();
+
+ /**
+ * Setup expectations: Chrome is open on a page and the tabs are treated as apps.
+ *
+ * This method will change the settings to treat tabs inside of Chrome and block until Chrome is
+ * open on the original tab.
+ */
+ public abstract void mergeTabs();
+
+ /**
+ * Setup expectations: Chrome is open on a page and the tabs are merged.
+ *
+ * This method will change the settings to treat tabs outside of Chrome and block until Chrome
+ * is open on the original tab.
+ */
+ public abstract void unmergeTabs();
+}
diff --git a/libraries/base-app-helpers/src/android/platform/test/helpers/AbstractFacebookHelper.java b/libraries/base-app-helpers/src/android/platform/test/helpers/AbstractFacebookHelper.java
new file mode 100644
index 0000000..a532263
--- /dev/null
+++ b/libraries/base-app-helpers/src/android/platform/test/helpers/AbstractFacebookHelper.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.platform.test.helpers;
+
+import android.app.Instrumentation;
+import android.support.test.uiautomator.Direction;
+
+public abstract class AbstractFacebookHelper extends AbstractStandardAppHelper {
+
+ public AbstractFacebookHelper(Instrumentation instr) {
+ super(instr);
+ }
+
+ /**
+ * Setup expectations: Facebook is on the home page.
+ *
+ * This method scrolls the home page.
+ *
+ * @param dir the direction to scroll
+ */
+ public abstract void scrollHomePage(Direction dir);
+
+ /**
+ * Setup expectations: Facebook app is open.
+ *
+ * This method keeps pressing the back button until Facebook is on the homepage.
+ */
+ public abstract void goToHomePage();
+
+ /**
+ * Setup expectations: Facebook app is on the home page.
+ *
+ * This method moves the Facebook app to the News Feed tab of the home page.
+ */
+ public abstract void goToNewsFeed();
+
+ /**
+ * Setup expectations: Facebook is on the News Feed tab of the home page.
+ *
+ * This method moves the Facebook app to the status update page.
+ */
+ public abstract void goToStatusUpdate();
+
+ /**
+ * Setup expectations: Facebook is on the status update page.
+ *
+ * This method clicks on the status update text field to move the keyboard cursor there
+ */
+ public abstract void clickStatusUpdateTextField();
+
+ /**
+ * Setup expections: Facebook is on the status update page.
+ *
+ * This method sets the status update text.
+ *
+ * @param statusText text for status update
+ */
+ public abstract void setStatusText(String statusText);
+
+ /**
+ * Setup expectations: Facebook is on the status update page.
+ *
+ * This method posts the status update.
+ */
+ public abstract void postStatusUpdate();
+
+ /**
+ * Setup expectations: Facebook app is on the login page.
+ *
+ * This method attempts to log in using the specified username and password.
+ *
+ * @param username username of Facebook account
+ * @param password password of Facebook account
+ */
+ public abstract void login(String username, String password);
+}
diff --git a/libraries/base-app-helpers/src/android/platform/test/helpers/AbstractFlightDemoHelper.java b/libraries/base-app-helpers/src/android/platform/test/helpers/AbstractFlightDemoHelper.java
new file mode 100644
index 0000000..84c31d9
--- /dev/null
+++ b/libraries/base-app-helpers/src/android/platform/test/helpers/AbstractFlightDemoHelper.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.platform.test.helpers;
+
+import android.app.Instrumentation;
+
+public abstract class AbstractFlightDemoHelper extends AbstractStandardAppHelper {
+
+ public AbstractFlightDemoHelper(Instrumentation instr) {
+ super(instr);
+ }
+
+ /**
+ * Setup expectation: On the opening screen.
+ *
+ * Best effort attempt to start the flight simulator demo
+ */
+ public abstract void startDemo();
+
+ /**
+ * Setup expectation: Currently running the flight simulator demo
+ *
+ * Best effort attempt to stop the flight simulator demo
+ */
+ public abstract void stopDemo();
+}
diff --git a/libraries/base-app-helpers/src/android/platform/test/helpers/AbstractGmailHelper.java b/libraries/base-app-helpers/src/android/platform/test/helpers/AbstractGmailHelper.java
new file mode 100644
index 0000000..ce36818
--- /dev/null
+++ b/libraries/base-app-helpers/src/android/platform/test/helpers/AbstractGmailHelper.java
@@ -0,0 +1,196 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.platform.test.helpers;
+
+import android.app.Instrumentation;
+import android.support.test.uiautomator.Direction;
+
+public abstract class AbstractGmailHelper extends AbstractStandardAppHelper {
+
+ public AbstractGmailHelper(Instrumentation instr) {
+ super(instr);
+ }
+
+ /**
+ * Setup expectations: Gmail is open and the navigation bar is visible.
+ *
+ * This method will navigate to the Inbox or Primary, depending on the name.
+ */
+ public abstract void goToInbox();
+
+ /**
+ * Alias method for AbstractGmailHelper#goToInbox
+ */
+ public void goToPrimary() {
+ goToInbox();
+ }
+
+ /**
+ * Setup expectations: Gmail is open on the Inbox or Primary page.
+ *
+ * This method will open a new e-mail to compose and block until complete.
+ */
+ public abstract void goToComposeEmail();
+
+ /**
+ * Checks if the current view is the compose email view.
+ *
+ * @return true if the current view is the compose email view, false otherwise.
+ */
+ public abstract boolean isInComposeEmail();
+
+ /**
+ * Checks if the app is open on the Inbox or Primary page.
+ *
+ * @return true if the current view is the Inbox or Primary page, false otherwise.
+ */
+ public abstract boolean isInPrimaryOrInbox();
+
+ /**
+ * Setup expectations: Gmail is open and on the Inbox or Primary page.
+ *
+ * This method will open the (index)'th visible e-mail in the list and block until the e-mail is
+ * visible in the foreground. The top-most visible e-mail will always be labeled 0. To get the
+ * number of visible e-mails, consult the getVisibleEmailCount() function.
+ */
+ public abstract void openEmailByIndex(int index);
+
+ /**
+ * Setup expectations: Gmail is open and on the Inbox or Primary page.
+ *
+ * This method will return the number of visible e-mails for use with the #openEmailByIndex
+ * method.
+ */
+ public abstract int getVisibleEmailCount();
+
+ /**
+ * Setup expectations: Gmail is open and an e-mail is open in the foreground.
+ *
+ * This method will press reply, send a reply e-mail with the given parameters, and block until
+ * the original message is in the foreground again.
+ */
+ public abstract void sendReplyEmail(String address, String body);
+
+ /**
+ * Setup expectations: Gmail is open and composing an e-mail.
+ *
+ * This method will set the e-mail's To address and block until complete.
+ */
+ public abstract void setEmailToAddress(String address);
+
+ /**
+ * Setup expectations: Gmail is open and composing an e-mail.
+ *
+ * This method will set the e-mail's subject and block until complete.
+ */
+ public abstract void setEmailSubject(String subject);
+
+ /**
+ * Setup expectations: Gmail is open and composing an e-mail.
+ *
+ * This method will set the e-mail's Body and block until complete. Focus will remain on the
+ * e-mail body after completion.
+ */
+ public abstract void setEmailBody(String body);
+
+ /**
+ * Setup expectations: Gmail is open and composing an e-mail.
+ *
+ * This method will press send and block until the device is idle on the original e-mail.
+ */
+ public abstract void clickSendButton();
+
+ /**
+ * Setup expectations: Gmail is open and composing an e-mail.
+ *
+ * This method will get the e-mail's composition's body and block until complete.
+ *
+ * @return {String} the text contained in the email composition's body.
+ */
+ public abstract String getComposeEmailBody();
+
+ /**
+ * Setup expectations: Gmail is open and the navigation drawer is visible.
+ *
+ * This method will open the navigation drawer and block until complete.
+ */
+ public abstract void openNavigationDrawer();
+
+ /**
+ * Setup expectations: Gmail is open and the navigation drawer is open.
+ *
+ * This method will close the navigation drawer and returns true otherwise false
+ */
+ public abstract boolean closeNavigationDrawer();
+
+ /**
+ * Setup expectations: Gmail is open and the navigation drawer is open.
+ *
+ * This method will scroll the navigation drawer and block until idle. Only accepts UP and DOWN.
+ */
+ public abstract void scrollNavigationDrawer(Direction dir);
+
+ /**
+ * Setup expectations: Gmail is open and a mailbox is open.
+ *
+ * This method will scroll the mailbox view.
+ *
+ * @param direction The direction to scroll, only accepts UP and DOWN.
+ * @param amount The amount to scroll
+ * @param scrollToEnd Whether or not to scroll to the end
+ */
+ public abstract void scrollMailbox(Direction direction, float amount, boolean scrollToEnd);
+
+ /**
+ * Setup expectations: Gmail is open and an email is open.
+ *
+ * This method will scroll the current email.
+ *
+ * @param direction The direction to scroll, only accepts UP and DOWN.
+ * @param amount The amount to scroll
+ * @param scrollToEnd Whether or not to scroll to the end
+ */
+ public abstract void scrollEmail(Direction direction, float amount, boolean scrollToEnd);
+
+ /**
+ * Setup expectations: Gmail is open and the navigation drawer is open.
+ *
+ * This method will open the mailbox with the given name and block until emails in
+ * that mailbox have loaded.
+ *
+ * @param mailboxName The case insensitive name of the mailbox to open
+ */
+ public abstract void openMailbox(String mailboxName);
+
+ /**
+ * Setup expectations: Gmail is open and an email is open.
+ *
+ * This method will return to the mailbox the current email was opened from.
+ */
+ public abstract void returnToMailbox();
+
+ /**
+ * Setup expectations: Gmail is open and an email is open.
+ *
+ * This method starts downloading the attachment at the specified index in the current email.
+ * The download happens in the background. This method returns immediately after starting
+ * the download and does not wait for the download to complete.
+ *
+ * @param index The index of the attachment to download
+ */
+ public abstract void downloadAttachment(int index);
+}
diff --git a/libraries/base-app-helpers/src/android/platform/test/helpers/AbstractGoogleCameraHelper.java b/libraries/base-app-helpers/src/android/platform/test/helpers/AbstractGoogleCameraHelper.java
new file mode 100644
index 0000000..dd7efe0
--- /dev/null
+++ b/libraries/base-app-helpers/src/android/platform/test/helpers/AbstractGoogleCameraHelper.java
@@ -0,0 +1,139 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.platform.test.helpers;
+
+import android.app.Instrumentation;
+import android.support.test.uiautomator.Direction;
+
+public abstract class AbstractGoogleCameraHelper extends AbstractStandardAppHelper {
+
+ public AbstractGoogleCameraHelper(Instrumentation instr) {
+ super(instr);
+ }
+
+ /**
+ * Setup expectations: GoogleCamera is open and idle in video mode.
+ *
+ * This method will change to camera mode and block until the transition is complete.
+ */
+ public abstract void goToCameraMode();
+
+ /**
+ * Setup expectations: GoogleCamera is open and idle in camera mode.
+ *
+ * This method will change to video mode and block until the transition is complete.
+ */
+ public abstract void goToVideoMode();
+
+ /**
+ * Setup expectations: GoogleCamera is open and idle in either camera/video mode.
+ *
+ * This method will change to back camera and block until the transition is complete.
+ */
+ public abstract void goToBackCamera();
+
+ /**
+ * Setup expectations: GoogleCamera is open and idle in either camera/video mode.
+ *
+ * This method will change to front camera and block until the transition is complete.
+ */
+ public abstract void goToFrontCamera();
+
+ /**
+ * Setup expectation: in Camera mode with the capture button present.
+ *
+ * This method will capture a photo and block until the transaction is complete.
+ */
+ public abstract void capturePhoto();
+
+ /**
+ * Setup expectation: in Video mode with the capture button present.
+ *
+ * This method will capture a video of length timeInMs and block until the transaction is
+ * complete.
+ * @param time duration of video in milliseconds
+ */
+ public abstract void captureVideo(long time);
+
+ /**
+ * Setup expectation:
+ * 1. in Video mode with the capture button present.
+ * 2. videoTime > snapshotStartTime
+ *
+ * This method will capture a video of length videoTime, and take a picture at snapshotStartTime.
+ * It will block until the the video is captured and the device is again idle in video mode.
+ * @param time duration of video in milliseconds
+ */
+ public abstract void snapshotVideo(long videoTime, long snapshotStartTime);
+
+ /**
+ * Setup expectation: GoogleCamera is open and idle in camera mode.
+ *
+ * This method will set HDR to on(1), auto(-1), or off(0).
+ * @param mode the integer value of the mode denoted above.
+ */
+ public abstract void setHdrMode(int mode);
+
+ /**
+ * Setup expectation: GoogleCamera is open and idle in video mode.
+ *
+ * This method will set 4K mode to on(1), or off(0).
+ * @param mode the integer value of the mode denoted above.
+ */
+ public abstract void set4KMode(int mode);
+
+ /**
+ * Setup expectation: GoogleCamera is open and idle in video mode.
+ *
+ * This method will set HFR mode to 240 fps (2), 120 fps (1), or off(0).
+ * @param mode the integer value of the mode denoted above.
+ */
+ public abstract void setHFRMode(int mode);
+
+ /**
+ * Setup expectation: in Camera mode with the capture button present.
+ *
+ * This method will block until the capture button is enabled for pressing.
+ */
+ public abstract void waitForCameraShutterEnabled();
+
+ /**
+ * Setup expectation: in Video mode with the capture button present.
+ *
+ * This method will block until the capture button is enabled for pressing.
+ */
+ public abstract void waitForVideoShutterEnabled();
+
+ /**
+ * Temporary function.
+ */
+ public abstract String openWithShutterTimeString();
+
+ /**
+ * Setup expectations: in Camera mode or in Video mode
+ */
+ public abstract void goToAlbum();
+
+ /**
+ * Setup expectations:
+ * 1. in album view
+ * 2. scroll direction is either LEFT or RIGHT
+ *
+ * @param direction scroll direction, either LEFT or RIGHT
+ */
+ public abstract void scrollAlbum(Direction direction);
+}
diff --git a/libraries/base-app-helpers/src/android/platform/test/helpers/AbstractGoogleDocsHelper.java b/libraries/base-app-helpers/src/android/platform/test/helpers/AbstractGoogleDocsHelper.java
new file mode 100644
index 0000000..b041489
--- /dev/null
+++ b/libraries/base-app-helpers/src/android/platform/test/helpers/AbstractGoogleDocsHelper.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.platform.test.helpers;
+
+import android.app.Instrumentation;
+
+public abstract class AbstractGoogleDocsHelper extends AbstractStandardAppHelper {
+
+ public AbstractGoogleDocsHelper(Instrumentation instr) {
+ super(instr);
+ }
+
+ /**
+ * Setup expectation: Google Docs is open and the Recent Docs Tab can be reached by
+ * pressing back button multiple times, i.e. the test procedure has been on the Recent
+ * Docs tab.
+ *
+ * Returns to the Recent Docs Tab.
+ */
+ public abstract void goToRecentDocsTab();
+
+ /**
+ * Setup expectation: Google Docs is on the Recent Docs tab.
+ *
+ * Opens the document.
+ *
+ * @param title The title (case sensitive) of the document as is displayed in the app.
+ */
+ public abstract void openDoc(String title);
+
+ /**
+ * Setup expectation: Google Docs is on a document page.
+ *
+ * Scrolls down the document.
+ */
+ public abstract void scrollDownDocument();
+}
diff --git a/libraries/base-app-helpers/src/android/platform/test/helpers/AbstractGoogleKeyboardHelper.java b/libraries/base-app-helpers/src/android/platform/test/helpers/AbstractGoogleKeyboardHelper.java
new file mode 100644
index 0000000..bf072f6
--- /dev/null
+++ b/libraries/base-app-helpers/src/android/platform/test/helpers/AbstractGoogleKeyboardHelper.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.platform.test.helpers;
+
+import android.app.Instrumentation;
+
+public abstract class AbstractGoogleKeyboardHelper extends AbstractStandardAppHelper {
+
+ public AbstractGoogleKeyboardHelper(Instrumentation instr) {
+ super(instr);
+ }
+
+ /*
+ * Setup expectations: Recently performed action that will open Google Keyboard
+ *
+ * @param timeout wait timeout in milliseconds
+ */
+ public abstract boolean waitForKeyboard(long timeout);
+
+ /*
+ * Setup expectations: Google Keyboard is open and visible
+ *
+ * @param text text to type
+ * @param delayBetweenKeyPresses delay between key presses in milliseconds
+ */
+ public abstract void typeText(String text, long delayBetweenKeyPresses);
+}
diff --git a/libraries/base-app-helpers/src/android/platform/test/helpers/AbstractGoogleMessengerHelper.java b/libraries/base-app-helpers/src/android/platform/test/helpers/AbstractGoogleMessengerHelper.java
new file mode 100644
index 0000000..f051c39
--- /dev/null
+++ b/libraries/base-app-helpers/src/android/platform/test/helpers/AbstractGoogleMessengerHelper.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 android.platform.test.helpers;
+
+import android.app.Instrumentation;
+import android.support.test.uiautomator.Direction;
+
+public abstract class AbstractGoogleMessengerHelper extends AbstractStandardAppHelper {
+
+ public AbstractGoogleMessengerHelper(Instrumentation instr) {
+ super(instr);
+ }
+
+ /*
+ * Setup expectations: Google Messenger app is open.
+ *
+ * This method brings the Messenger app to the home page.
+ */
+ public abstract void goToHomePage();
+
+ /*
+ * Setup expectation: Google Messenger app is on the home page.
+ *
+ * This method brings up the new conversation page.
+ */
+ public abstract void goToNewConversationPage();
+
+ /*
+ * Setup expectations: Google Messenger app is on the new conversation page.
+ *
+ * This method moves the Google Messenger app to messages page with the specified contacts.
+ */
+ public abstract void goToMessagesPage();
+
+ /**
+ * Setup expectations: Google Messenger app is on the messages page.
+ *
+ * This method scrolls through the messages on the messages page.
+ *
+ * @param direction Direction to scroll, must be UP or DOWN.
+ */
+ public abstract void scrollMessages(Direction direction);
+
+ /**
+ * Setup expectations: Google Messenger app is on the messages page.
+ *
+ * This method clicks the "send message" textbox on the messages page.
+ */
+ public abstract void clickComposeMessageText();
+
+ /**
+ * Setup expectations:
+ * 1. Google Messenger app is on the messages page
+ * 2. New message textbox is not empty.
+ */
+ public abstract void clickSendMessageButton();
+
+ /**
+ * Setup expectations: Google Messenger app is on the messages page.
+ *
+ * This method clicks the "attach media" button and attaches the media file with the given
+ * index in the device media gallery view.
+ */
+ public abstract void attachMediaFromDevice(int index);
+}
diff --git a/libraries/base-app-helpers/src/android/platform/test/helpers/AbstractLeanbackAppHelper.java b/libraries/base-app-helpers/src/android/platform/test/helpers/AbstractLeanbackAppHelper.java
new file mode 100644
index 0000000..e81bcfe
--- /dev/null
+++ b/libraries/base-app-helpers/src/android/platform/test/helpers/AbstractLeanbackAppHelper.java
@@ -0,0 +1,192 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package android.platform.test.helpers;
+
+import android.app.Instrumentation;
+import android.platform.test.helpers.exceptions.UnknownUiException;
+import android.support.test.launcherhelper.ILeanbackLauncherStrategy;
+import android.support.test.launcherhelper.LauncherStrategyFactory;
+import android.support.test.uiautomator.By;
+import android.support.test.uiautomator.BySelector;
+import android.support.test.uiautomator.Direction;
+import android.support.test.uiautomator.UiObject2;
+import android.support.test.uiautomator.Until;
+import android.util.Log;
+
+/**
+ * This app helper handles the following important widgets for TV apps:
+ * BrowseFragment, DetailsFragment, SearchFragment and PlaybackOverlayFragment
+ */
+public abstract class AbstractLeanbackAppHelper extends AbstractStandardAppHelper {
+
+ private static final String TAG = AbstractLeanbackAppHelper.class.getSimpleName();
+ private static final long OPEN_SECTION_WAIT_TIME_MS = 5000;
+ private static final long OPEN_SIDE_PANEL_WAIT_TIME_MS = 5000;
+ private static final int OPEN_SIDE_PANEL_MAX_ATTEMPTS = 5;
+ private static final long MAIN_ACTIVITY_WAIT_TIME_MS = 250;
+
+ protected DPadHelper mDPadHelper;
+ public ILeanbackLauncherStrategy mLauncherStrategy;
+
+
+ public AbstractLeanbackAppHelper(Instrumentation instr) {
+ super(instr);
+ mDPadHelper = DPadHelper.getInstance(instr);
+ mLauncherStrategy = LauncherStrategyFactory.getInstance(
+ mDevice).getLeanbackLauncherStrategy();
+ }
+
+ protected abstract BySelector getAppSelector();
+
+ protected abstract BySelector getSidePanelSelector();
+
+ protected abstract BySelector getSidePanelResultSelector(String sectionName);
+
+ /**
+ * Selector to identify main activity for getMainActivitySelector().
+ * Not every application has its main activity, so the override is optional.
+ */
+ protected BySelector getMainActivitySelector() {
+ return null;
+ }
+
+ /**
+ * Setup expectation: Side panel is selected on browse fragment
+ *
+ * Best effort attempt to go to the side panel, and open the selected section.
+ */
+ public void openSection(String sectionName) {
+ openSidePanel();
+ // Section header is focused; it should not be after pressing the DPad
+ selectSection(sectionName);
+ mDevice.pressDPadCenter();
+
+ // Test for focus change and selection result
+ BySelector sectionResult = getSidePanelResultSelector(sectionName);
+ if (!mDevice.wait(Until.hasObject(sectionResult), OPEN_SECTION_WAIT_TIME_MS)) {
+ throw new UnknownUiException(
+ String.format("Failed to find result opening section %s", sectionName));
+ }
+ Log.v(TAG, "Successfully opened section");
+ }
+
+ /**
+ * Setup expectation: On navigation screen on browse fragment
+ *
+ * Best effort attempt to open the side panel.
+ * @param onMainActivity True if it opens the side panel on app's main activity.
+ */
+ public void openSidePanel(boolean onMainActivity) {
+ if (onMainActivity) {
+ returnToMainActivity();
+ }
+ int attempts = 0;
+ while (!isSidePanelSelected(OPEN_SIDE_PANEL_WAIT_TIME_MS)
+ && attempts++ < OPEN_SIDE_PANEL_MAX_ATTEMPTS) {
+ mDevice.pressDPadLeft();
+ }
+ if (attempts == OPEN_SIDE_PANEL_MAX_ATTEMPTS) {
+ throw new UnknownUiException("Failed to open side panel");
+ }
+ }
+
+ public void openSidePanel() {
+ openSidePanel(false);
+ }
+
+ /**
+ * Select target item through the container in the given direction.
+ * @param container
+ * @param target
+ * @param direction
+ * @return the focused object
+ */
+ public UiObject2 select(UiObject2 container, BySelector target, Direction direction) {
+ if (container == null) {
+ throw new IllegalArgumentException("The container should not be null.");
+ }
+ UiObject2 focus = container.findObject(By.focused(true));
+ if (focus == null) {
+ throw new UnknownUiException("The container should have a focus.");
+ }
+ while (!focus.hasObject(target)) {
+ UiObject2 prev = focus;
+ mDPadHelper.pressDPad(direction);
+ focus = container.findObject(By.focused(true));
+ if (focus == null) {
+ mDPadHelper.pressDPad(Direction.reverse(direction));
+ focus = container.findObject(By.focused(true));
+ }
+ if (focus.equals(prev)) {
+ // It reached at the end, but no target is found.
+ return null;
+ }
+ }
+ return focus;
+ }
+
+ /**
+ * Attempts to return to main activity with getMainActivitySelector()
+ * by pressing the back button repeatedly and sleeping briefly to allow for UI slowness.
+ */
+ public void returnToMainActivity() {
+ int maxBackAttempts = 10;
+ BySelector selector = getMainActivitySelector();
+ if (selector == null) {
+ throw new IllegalStateException("getMainActivitySelector() should be overridden.");
+ }
+ while (!mDevice.wait(Until.hasObject(selector), MAIN_ACTIVITY_WAIT_TIME_MS)
+ && maxBackAttempts-- > 0) {
+ mDevice.pressBack();
+ }
+ }
+
+ @Override
+ public void dismissInitialDialogs() {
+ return;
+ }
+
+ protected boolean isSidePanelSelected(long timeout) {
+ UiObject2 sidePanel = mDevice.wait(Until.findObject(getSidePanelSelector()), timeout);
+ if (sidePanel == null) {
+ return false;
+ }
+ return sidePanel.hasObject(By.focused(true).minDepth(1));
+ }
+
+ protected UiObject2 selectSection(String sectionName) {
+ UiObject2 container = mDevice.wait(
+ Until.findObject(getSidePanelSelector()), OPEN_SIDE_PANEL_WAIT_TIME_MS);
+ BySelector section = By.clazz(".TextView").text(sectionName);
+
+ // Wait until the section text appears at runtime. This needs to be long enough to run under
+ // low bandwidth environments in the test lab.
+ mDevice.wait(Until.findObject(section), 60 * 1000);
+
+ // Search up, then down
+ UiObject2 focused = select(container, section, Direction.UP);
+ if (focused != null) {
+ return focused;
+ }
+ focused = select(container, section, Direction.DOWN);
+ if (focused != null) {
+ return focused;
+ }
+ throw new UnknownUiException("Failed to select section");
+ }
+
+}
diff --git a/libraries/base-app-helpers/src/android/platform/test/helpers/AbstractMapsHelper.java b/libraries/base-app-helpers/src/android/platform/test/helpers/AbstractMapsHelper.java
new file mode 100644
index 0000000..6c9f72f
--- /dev/null
+++ b/libraries/base-app-helpers/src/android/platform/test/helpers/AbstractMapsHelper.java
@@ -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.
+ */
+
+package android.platform.test.helpers;
+
+import android.app.Instrumentation;
+
+public abstract class AbstractMapsHelper extends AbstractStandardAppHelper {
+
+ public AbstractMapsHelper(Instrumentation instr) {
+ super(instr);
+ }
+
+ /**
+ * Setup expectation: On the standard Map screen in any setup.
+ *
+ * Best effort attempt to go to the query screen (if not currently there),
+ * does a search, and selects the results.
+ */
+ public abstract void doSearch(String query);
+
+ /**
+ * Setup expectation: Destination is selected.
+ *
+ * Best effort attempt to go to the directions screen for the selected destination.
+ */
+ public abstract void getDirections();
+
+ /**
+ * Setup expectation: On directions screen.
+ *
+ * Best effort attempt to start navigation for the selected destination.
+ */
+ public abstract void startNavigation();
+
+ /**
+ * Setup expectation: On navigation screen.
+ *
+ * Best effort attempt to stop navigation, and go back to the directions screen.
+ */
+ public abstract void stopNavigation();
+}
diff --git a/libraries/base-app-helpers/src/android/platform/test/helpers/AbstractPhotosHelper.java b/libraries/base-app-helpers/src/android/platform/test/helpers/AbstractPhotosHelper.java
new file mode 100644
index 0000000..b94ea5e
--- /dev/null
+++ b/libraries/base-app-helpers/src/android/platform/test/helpers/AbstractPhotosHelper.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 android.platform.test.helpers;
+
+import android.app.Instrumentation;
+import android.support.test.uiautomator.Direction;
+
+public abstract class AbstractPhotosHelper extends AbstractStandardAppHelper {
+
+ public AbstractPhotosHelper(Instrumentation instr) {
+ super(instr);
+ }
+
+ /**
+ * Setup expectations: Photos is open and on the main or a device folder's screen.
+ *
+ * This method will check if there are videos that can be opened on current screen
+ * by looking for any view objects with content-desc that starts with "Video"
+ *
+ * @return true if video clips are found, false otherwise
+ */
+ public abstract boolean searchForVideoClip();
+
+ /**
+ * Setup expectations: Photos is open and on the main or a device folder's screen.
+ *
+ * This method will select the first clip to open and play. This will block until the clip
+ * begins to plays.
+ */
+ public abstract void openFirstClip();
+
+ /**
+ * Setup expectations: Photos is open and a clip is currently playing.
+ *
+ * This method will pause the current clip and block until paused.
+ */
+ public abstract void pauseClip();
+
+ /**
+ * Setup expectations: Photos is open and a clip is currently paused in the foreground.
+ *
+ * This method will play the current clip and block until it is playing.
+ */
+ public abstract void playClip();
+
+ /**
+ * Setup expectations: Photos is open.
+ *
+ * This method will go to the main screen.
+ */
+ public abstract void goToMainScreen();
+
+ /**
+ * Setup expectations: Photos is open.
+ *
+ * This method will go to device folder screen.
+ */
+ public abstract void goToDeviceFolderScreen();
+
+ /**
+ * Setup expectations:
+ * 1. Photos is open
+ * 2. on device folder screen
+ * 3. the first device folder is shown on the screen
+ *
+ * This method will search for user-specified device folder in device folders.
+ * If the device folder is found, the function will return with the device
+ * folder on current screen.
+ *
+ * @param folderName User-specified device folder name
+ * @return true if device folder is found, false otherwise
+ */
+ public abstract boolean searchForDeviceFolder(String folderName);
+
+ /**
+ * Setup expectations:
+ * 1. Photos is open
+ * 2. on device folder screen
+ * 3. user-specified device folder is currently on screen
+ *
+ * This method will open the user-specified device folder.
+ *
+ * @param folderName User-specified device folder name
+ */
+ public abstract void openDeviceFolder(String folderName);
+
+ /**
+ * Setup expectations: Photos is open and on the main or a device folder's screen.
+ *
+ * This method will check if there are pictures that can be opened on current screen
+ * by looking for any view objects with content-desc that starts with "Photo"
+ *
+ * @return true if pictures are found
+ */
+ public abstract boolean searchForPicture();
+
+ /**
+ * Setup expectations: Photos is open and on the main or a device folder's screen.
+ *
+ * This method will open the picture at the specified index.
+ *
+ * @param index The index of the picture to open
+ */
+ public abstract void openPicture(int index);
+
+ /**
+ * Setup expectations: Photos is open and a picture album is open.
+ *
+ * This method will scroll the picture album in the specified direction.
+ *
+ * @param direction The direction to scroll, must be LEFT or RIGHT.
+ */
+ public abstract void scrollAlbum(Direction direction);
+}
diff --git a/libraries/base-app-helpers/src/android/platform/test/helpers/AbstractPlayBooksHelper.java b/libraries/base-app-helpers/src/android/platform/test/helpers/AbstractPlayBooksHelper.java
new file mode 100644
index 0000000..d0b0c28
--- /dev/null
+++ b/libraries/base-app-helpers/src/android/platform/test/helpers/AbstractPlayBooksHelper.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.platform.test.helpers;
+
+import android.app.Instrumentation;
+
+public abstract class AbstractPlayBooksHelper extends AbstractStandardAppHelper {
+
+ public AbstractPlayBooksHelper(Instrumentation instr) {
+ super(instr);
+ }
+
+ /**
+ * Setup expectations: PlayBooks is open on any screen.
+ *
+ * Navigates to "My Library" and selects the "ALL BOOKS" tab.
+ */
+ public abstract void goToAllBooksTab();
+
+ /**
+ * Setup expectations: PlayBooks is open on "My Library - ALL BOOKS" screen.
+ *
+ * Selects the first Book and start reading.
+ */
+ public abstract void openBook();
+
+ /**
+ * Setup expectations: PlayBooks is on a page of a book.
+ *
+ * Exits reading mode.
+ */
+ public abstract void exitReadingMode();
+
+ /**
+ * Setup expectations: PlayBooks is on a full-screen page of a book.
+ *
+ * Goes to the next page by clicking the right side of the page.
+ */
+ public abstract void goToNextPage();
+
+ /**
+ * Setup expectations: PlayBooks is on a full-screen page of a book.
+ *
+ * Goes to the previous page by clicking the left side of the page.
+ */
+ public abstract void goToPreviousPage();
+
+ /**
+ * Setup expectations: PlayBooks is on a full-screen page of a book.
+ *
+ * Goes to the next page by scrolling leftwards.
+ */
+ public abstract void scrollToNextPage();
+
+ /**
+ * Setup expectations: PlayBooks is on a full-screen page of a book.
+ *
+ * Goes to the previous page by scrolling rightwards.
+ */
+ public abstract void scrollToPreviousPage();
+}
diff --git a/libraries/base-app-helpers/src/android/platform/test/helpers/AbstractPlayMoviesHelper.java b/libraries/base-app-helpers/src/android/platform/test/helpers/AbstractPlayMoviesHelper.java
new file mode 100644
index 0000000..56ce8e6
--- /dev/null
+++ b/libraries/base-app-helpers/src/android/platform/test/helpers/AbstractPlayMoviesHelper.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.platform.test.helpers;
+
+import android.app.Instrumentation;
+
+public abstract class AbstractPlayMoviesHelper extends AbstractStandardAppHelper {
+
+ public AbstractPlayMoviesHelper(Instrumentation instr) {
+ super(instr);
+ }
+
+ /**
+ * Setup expectations: PlayMovies is open on any screen with access to the navigation bar.
+ *
+ * This method will navigate to "My Library" and select the "My Movies" tab. This will block
+ * until the method is complete.
+ */
+ public abstract void openMoviesTab();
+
+ /**
+ * Setup expectations: PlayMovies is open on any screen.
+ *
+ * PlayMovies will select the movie card and subsequently press the play button.
+ */
+ public abstract void playMovie(String name);
+}
diff --git a/libraries/base-app-helpers/src/android/platform/test/helpers/AbstractPlayMusicHelper.java b/libraries/base-app-helpers/src/android/platform/test/helpers/AbstractPlayMusicHelper.java
new file mode 100644
index 0000000..352e8ea
--- /dev/null
+++ b/libraries/base-app-helpers/src/android/platform/test/helpers/AbstractPlayMusicHelper.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 android.platform.test.helpers;
+
+import android.app.Instrumentation;
+
+public abstract class AbstractPlayMusicHelper extends AbstractStandardAppHelper {
+
+ public AbstractPlayMusicHelper(Instrumentation instr) {
+ super(instr);
+ }
+
+ /**
+ * Setup expectations: PlayMusic is open and the navigation bar is visible.
+ *
+ * This method will open the navigation bar, press "My Library," and navigate to the songs tab.
+ * This method blocks until the process is complete.
+ */
+ public abstract void goToTab(String tabTitle);
+
+ /**
+ * Setup expectations: PlayMusic is open and the navigation bar is visible.
+ *
+ * This method will navigate to the Albums tab, select the album, and then select the song. The
+ * method will block until the song is playing.
+ */
+ public abstract void selectSong(String album, String song);
+
+ /**
+ * Setup expectations: PlayMusic is open with a song playing.
+ *
+ * This method will pause the song and block until the song is paused.
+ */
+ public abstract void pauseSong();
+
+ /**
+ * Setup expectations: PlayMusic is open with a song paused.
+ *
+ * This method will play the song and block until the song is playing.
+ */
+ public abstract void playSong();
+
+ /**
+ * Setup expectations: PlayMusic is open with a song playing the controls minimized.
+ *
+ * This method will press the header and block until the song is expanded.
+ */
+ public abstract void expandMediaControls();
+
+ /**
+ * Setup expectations: PlayMusic is open and on the Songs library tab
+ *
+ * This method will press the "Shuffle All" button and block until the song is playing.
+ */
+ public abstract void pressShuffleAll();
+
+ /**
+ * Setup expectations: PlayMusic is open with a song open and expanded.
+ *
+ * This method will press the repeat button and cycle to the next state. Unfortunately, the
+ * limitations of the Accessibility for Play Music means that we cannot tell what state it
+ * currently is in.
+ */
+ public abstract void pressRepeat();
+}
diff --git a/libraries/base-app-helpers/src/android/platform/test/helpers/AbstractPlayStoreHelper.java b/libraries/base-app-helpers/src/android/platform/test/helpers/AbstractPlayStoreHelper.java
new file mode 100644
index 0000000..723565a
--- /dev/null
+++ b/libraries/base-app-helpers/src/android/platform/test/helpers/AbstractPlayStoreHelper.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.platform.test.helpers;
+
+import android.app.Instrumentation;
+
+public abstract class AbstractPlayStoreHelper extends AbstractStandardAppHelper {
+
+ public AbstractPlayStoreHelper(Instrumentation instr) {
+ super(instr);
+ }
+
+ /**
+ * Setup expectations: The search bar is visible.
+ *
+ * Selects the search bar, enters a query, and displays the results. Blocks until the results
+ * are selectable.
+ */
+ public abstract void doSearch(String query);
+
+ /**
+ * Setup expectations: There are visible search results.
+ *
+ * Opens the necessary categories and enters the first search result. Blocks until the process
+ * is complete.
+ */
+ public abstract void selectFirstResult();
+}
diff --git a/libraries/base-app-helpers/src/android/platform/test/helpers/AbstractRecentsHelper.java b/libraries/base-app-helpers/src/android/platform/test/helpers/AbstractRecentsHelper.java
new file mode 100644
index 0000000..7a1a85b
--- /dev/null
+++ b/libraries/base-app-helpers/src/android/platform/test/helpers/AbstractRecentsHelper.java
@@ -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.
+ */
+
+package android.platform.test.helpers;
+
+import android.app.Instrumentation;
+import android.support.test.uiautomator.Direction;
+
+public abstract class AbstractRecentsHelper extends AbstractStandardAppHelper {
+
+ public AbstractRecentsHelper(Instrumentation instr) {
+ super(instr);
+ }
+
+ /**
+ * Setup expectations: "Recents" is open.
+ *
+ * Flings the recent apps in the specified direction.
+ * @param dir the direction for the apps to move
+ */
+ public abstract void flingRecents(Direction dir);
+}
diff --git a/libraries/base-app-helpers/src/android/platform/test/helpers/AbstractRedditHelper.java b/libraries/base-app-helpers/src/android/platform/test/helpers/AbstractRedditHelper.java
new file mode 100644
index 0000000..7cf83a0
--- /dev/null
+++ b/libraries/base-app-helpers/src/android/platform/test/helpers/AbstractRedditHelper.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.platform.test.helpers;
+
+import android.app.Instrumentation;
+import android.support.test.uiautomator.Direction;
+
+public abstract class AbstractRedditHelper extends AbstractStandardAppHelper {
+
+ public AbstractRedditHelper(Instrumentation instr) {
+ super(instr);
+ }
+
+ /*
+ * Setup expectations: Reddit app is open.
+ *
+ * This method moves the Reddit app to the front page.
+ */
+ public abstract void goToFrontPage();
+
+ /*
+ * Setup expectations: Reddit app is on the front pages.
+ *
+ * This method moves the Reddit app to the first visible article's comment page.
+ */
+ public abstract void goToFirstArticleComments();
+
+ /*
+ * Setup expectations: Reddit app is on the front page.
+ *
+ * This method scrolls the front page.
+ *
+ * @param direction Direction in which to scroll, must be UP or DOWN
+ * @param percent Percent of page to scroll
+ * @return boolean Whether the page can still scroll in the given direction
+ */
+ public abstract boolean scrollFrontPage(Direction direction, float percent);
+
+ /*
+ * Setup expectations: Reddit app is on an article's comment page.
+ *
+ * This method scrolls the comment page.
+ *
+ * @param direction Direction in which to scroll, must be UP or DOWN
+ * @param percent Percent of page to scroll
+ * @return boolean Whether the page can still scroll in the given direction
+ */
+ public abstract boolean scrollCommentPage(Direction direction, float percent);
+}
diff --git a/libraries/base-app-helpers/src/android/platform/test/helpers/AbstractSettingsHelper.java b/libraries/base-app-helpers/src/android/platform/test/helpers/AbstractSettingsHelper.java
new file mode 100644
index 0000000..eab8188
--- /dev/null
+++ b/libraries/base-app-helpers/src/android/platform/test/helpers/AbstractSettingsHelper.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.platform.test.helpers;
+
+import android.app.Instrumentation;
+
+public abstract class AbstractSettingsHelper extends AbstractStandardAppHelper {
+
+ public AbstractSettingsHelper(Instrumentation instr) {
+ super(instr);
+ }
+
+ /**
+ * Setup expectation: Settings is open.
+ *
+ * This method will fling through the settings list numberOfFlings
+ * number of times or until the bottom of Settings is reached,
+ * whichever happens first.
+ * @param numberOfFlings number of flings needed
+ */
+ public abstract void scrollThroughSettings(int numberOfFlings) throws Exception;
+
+ /**
+ * Setup expectation: Settings is open.
+ *
+ * This method will fling through the settings list until it
+ * reaches the top of the list.
+ */
+ public abstract void flingSettingsToStart() throws Exception;
+}
diff --git a/libraries/base-app-helpers/src/android/platform/test/helpers/AbstractStandardAppHelper.java b/libraries/base-app-helpers/src/android/platform/test/helpers/AbstractStandardAppHelper.java
new file mode 100644
index 0000000..93fd1fe
--- /dev/null
+++ b/libraries/base-app-helpers/src/android/platform/test/helpers/AbstractStandardAppHelper.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 android.platform.test.helpers;
+
+import android.app.Instrumentation;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.support.test.launcherhelper.ILauncherStrategy;
+import android.support.test.launcherhelper.LauncherStrategyFactory;
+import android.support.test.uiautomator.By;
+import android.support.test.uiautomator.UiDevice;
+
+public abstract class AbstractStandardAppHelper implements IStandardAppHelper {
+ public UiDevice mDevice;
+ public Instrumentation mInstrumentation;
+ public ILauncherStrategy mLauncherStrategy;
+
+ public AbstractStandardAppHelper(Instrumentation instr) {
+ mInstrumentation = instr;
+ mDevice = UiDevice.getInstance(instr);
+ mLauncherStrategy = LauncherStrategyFactory.getInstance(mDevice).getLauncherStrategy();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void open() {
+ String pkg = getPackage();
+ String id = getLauncherName();
+ if (!mDevice.hasObject(By.pkg(pkg).depth(0))) {
+ mLauncherStrategy.launch(id, pkg);
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void exit() {
+ int maxBacks = 4;
+ while (!mDevice.hasObject(mLauncherStrategy.getWorkspaceSelector()) && maxBacks > 0) {
+ mDevice.pressBack();
+ mDevice.waitForIdle();
+ maxBacks--;
+ }
+
+ if (maxBacks == 0) {
+ mDevice.pressHome();
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public String getVersion() throws NameNotFoundException {
+ String pkg = getPackage();
+
+ if (null == pkg || pkg.isEmpty()) {
+ throw new RuntimeException("Cannot find version of empty package");
+ }
+ PackageManager pm = mInstrumentation.getContext().getPackageManager();
+ PackageInfo pInfo = pm.getPackageInfo(pkg, 0);
+ String version = pInfo.versionName;
+ if (null == version || version.isEmpty()) {
+ throw new RuntimeException(String.format("Version isn't found for package, %s", pkg));
+ }
+
+ return version;
+ }
+
+ protected int getOrientation() {
+ return mInstrumentation.getContext().getResources().getConfiguration().orientation;
+ }
+}
diff --git a/libraries/base-app-helpers/src/android/platform/test/helpers/AbstractTuneInHelper.java b/libraries/base-app-helpers/src/android/platform/test/helpers/AbstractTuneInHelper.java
new file mode 100644
index 0000000..1e5ec95
--- /dev/null
+++ b/libraries/base-app-helpers/src/android/platform/test/helpers/AbstractTuneInHelper.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.platform.test.helpers;
+
+import android.app.Instrumentation;
+
+public abstract class AbstractTuneInHelper extends AbstractStandardAppHelper {
+
+ public AbstractTuneInHelper(Instrumentation instr) {
+ super(instr);
+ }
+
+ /**
+ * Setup expectation: TuneIn app is open, originally on Browse Page
+ *
+ * This method attempts a few times until go back to Browse Page
+ * and assert fails if it doesn't end up on Browse Page
+ */
+ public abstract void goToBrowsePage();
+
+ /**
+ * Setup expectation: TuneIn is on Browse page
+ *
+ * This method blocks until on local radio page
+ */
+ public abstract void goToLocalRadio();
+
+ /**
+ * Setup expectation: TuneIn is on Local Radio page
+ *
+ * This method selects the ith FM from the radio list
+ * and goes to radio profile page
+ * @param i ith FM
+ */
+ public abstract void selectFM(int i);
+
+ /**
+ * Setup expectation: TuneIn is on radio profile page
+ *
+ * This method starts playing the radio channel
+ */
+ public abstract void startChannel();
+
+ /**
+ * Setup expectation: TuneIn is on channel page
+ *
+ * This method stops the channel and stays on the page
+ */
+ public abstract void stopChannel();
+}
diff --git a/libraries/base-app-helpers/src/android/platform/test/helpers/AbstractYouTubeHelper.java b/libraries/base-app-helpers/src/android/platform/test/helpers/AbstractYouTubeHelper.java
new file mode 100644
index 0000000..aa0c641
--- /dev/null
+++ b/libraries/base-app-helpers/src/android/platform/test/helpers/AbstractYouTubeHelper.java
@@ -0,0 +1,119 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.platform.test.helpers;
+
+import android.app.Instrumentation;
+
+public abstract class AbstractYouTubeHelper extends AbstractStandardAppHelper {
+
+ public enum VideoQuality {
+ QUALITY_AUTO ("Auto"),
+ QUALITY_144p ("144p"),
+ QUALITY_240p ("240p"),
+ QUALITY_360p ("360p"),
+ QUALITY_480p ("480p"),
+ QUALITY_720p ("720p"),
+ QUALITY_1080p("1080p");
+
+ private final String text;
+
+ VideoQuality(String text) {
+ this.text = text;
+ }
+
+ public String getText() {
+ return text;
+ }
+ };
+
+ public AbstractYouTubeHelper(Instrumentation instr) {
+ super(instr);
+ }
+
+ /**
+ * Setup expectations: YouTube app is open.
+ *
+ * This method keeps pressing the back button until YouTube is on the home page.
+ */
+ public abstract void goToHomePage();
+
+ /**
+ * Setup expectations: YouTube is on the home page.
+ *
+ * This method scrolls to the top of the home page and clicks the search button.
+ */
+ public abstract void goToSearchPage();
+
+ /**
+ * Setup expectations: YouTube is on the non-fullscreen video player page.
+ *
+ * This method changes the video player to fullscreen mode. Has no effect if the video player
+ * is already in fullscreen mode.
+ */
+ public abstract void goToFullscreenMode();
+
+ /**
+ * Setup expectations: YouTube is on the home page.
+ *
+ * This method selects a video on the home page and blocks until the video is playing.
+ */
+ public abstract void playHomePageVideo();
+
+ /**
+ * Setup expectations: YouTube is on the search results page.
+ *
+ * This method selects a search result video and blocks until the video is playing.
+ */
+ public abstract void playSearchResultPageVideo();
+
+ /**
+ * Setup expectations: Recently opened a video in the YouTube app.
+ *
+ * This method blocks until the video has loaded.
+ *
+ * @param timeout wait timeout in milliseconds
+ * @return true if video loaded within the timeout, false otherwise
+ */
+ public abstract boolean waitForVideoToLoad(long timeout);
+
+ /**
+ * Setup expectations: Recently initiated a search query in the YouTube app.
+ *
+ * This method blocks until search results appear.
+ *
+ * @param timeout wait timeout in milliseconds
+ * @return true if search results appeared within timeout, false otherwise
+ */
+ public abstract boolean waitForSearchResults(long timeout);
+
+ /**
+ * Setup expectations: YouTube is on the video player page.
+ *
+ * This method changes the video quality of the current video.
+ *
+ * @param quality the desired video quality
+ * @see AbstractYouTubeHelper.VideoQuality
+ */
+ public abstract void setVideoQuality(VideoQuality quality);
+
+ /**
+ * Setup expectations: YouTube is on the video player page.
+ *
+ * This method resumes the video if it is paused.
+ */
+ public abstract void resumeVideo();
+}
diff --git a/libraries/base-app-helpers/src/android/platform/test/helpers/DPadHelper.java b/libraries/base-app-helpers/src/android/platform/test/helpers/DPadHelper.java
new file mode 100644
index 0000000..51dd12b
--- /dev/null
+++ b/libraries/base-app-helpers/src/android/platform/test/helpers/DPadHelper.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 android.platform.test.helpers;
+
+import android.app.Instrumentation;
+import android.os.SystemClock;
+import android.support.test.uiautomator.Direction;
+import android.support.test.uiautomator.UiDevice;
+
+public class DPadHelper {
+
+ private static final String TAG = DPadHelper.class.getSimpleName();
+ private static final long DPAD_DEFAULT_WAIT_TIME_MS = 1000; // 1 sec
+ private static DPadHelper mInstance;
+ private UiDevice mDevice;
+
+
+ private DPadHelper(Instrumentation instrumentation) {
+ mDevice = UiDevice.getInstance(instrumentation);
+ }
+
+ public static DPadHelper getInstance(Instrumentation instrumentation) {
+ if (mInstance == null) {
+ mInstance = new DPadHelper(instrumentation);
+ }
+ return mInstance;
+ }
+
+ public void pressDPad(Direction direction) {
+ pressDPad(direction, 1, DPAD_DEFAULT_WAIT_TIME_MS);
+ }
+
+ public void pressDPad(Direction direction, long repeat) {
+ pressDPad(direction, repeat, DPAD_DEFAULT_WAIT_TIME_MS);
+ }
+
+ /**
+ * Presses DPad button of the same direction for the count times.
+ * It sleeps between each press for DPAD_DEFAULT_WAIT_TIME_MS.
+ *
+ * @param direction the direction of the button to press.
+ * @param repeat the number of times to press the button.
+ * @param timeout the timeout for the wait.
+ */
+ public void pressDPad(Direction direction, long repeat, long timeout) {
+ int iteration = 0;
+ while (iteration++ < repeat) {
+ switch (direction) {
+ case LEFT:
+ mDevice.pressDPadLeft();
+ break;
+ case RIGHT:
+ mDevice.pressDPadRight();
+ break;
+ case UP:
+ mDevice.pressDPadUp();
+ break;
+ case DOWN:
+ mDevice.pressDPadDown();
+ break;
+ }
+ SystemClock.sleep(timeout);
+ }
+ }
+}
diff --git a/libraries/base-app-helpers/src/android/platform/test/helpers/IStandardAppHelper.java b/libraries/base-app-helpers/src/android/platform/test/helpers/IStandardAppHelper.java
new file mode 100644
index 0000000..3c5cde1
--- /dev/null
+++ b/libraries/base-app-helpers/src/android/platform/test/helpers/IStandardAppHelper.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.platform.test.helpers;
+
+import android.content.pm.PackageManager.NameNotFoundException;
+
+public interface IStandardAppHelper {
+
+ /**
+ * Setup expectation: On the launcher home screen.
+ *
+ * Launches the desired application.
+ */
+ abstract void open();
+
+ /**
+ * Setup expectation: None
+ *
+ * Presses back until the launcher package is visible, i.e. the home screen. This can be
+ * overriden for custom functionality, however consider and document the exit state if doing so.
+ */
+ abstract void exit();
+
+ /**
+ * Setup expectations: This application is on the initial launch screen.
+ *
+ * This method will dismiss all visible relevant dialogs and block until this process is
+ * complete.
+ */
+ abstract void dismissInitialDialogs();
+
+ /**
+ * Setup expectations: None
+ *
+ * @return the package name for this helper's application.
+ */
+ abstract String getPackage();
+
+ /**
+ * Setup expectations: None.
+ *
+ * @return the name for this application in the launcher.
+ */
+ abstract String getLauncherName();
+
+ /**
+ * Setup expectations: None
+ *
+ * This method will return the version String from PackageManager.
+ * @param pkgName the application package
+ * @throws NameNotFoundException if the package is not found in PM
+ * @return the version as a String
+ */
+ abstract String getVersion() throws NameNotFoundException;
+}
diff --git a/libraries/base-app-helpers/src/android/platform/test/helpers/exceptions/UiTimeoutException.java b/libraries/base-app-helpers/src/android/platform/test/helpers/exceptions/UiTimeoutException.java
new file mode 100644
index 0000000..3aee637
--- /dev/null
+++ b/libraries/base-app-helpers/src/android/platform/test/helpers/exceptions/UiTimeoutException.java
@@ -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.
+ */
+
+package android.platform.test.helpers.exceptions;
+
+/**
+ * A UiTimeoutException is an exception specific to UI-driven app helpers. This should be thrown
+ * when a specific UI condition is not met due to a timeout that has been exceeded.
+ * <p>
+ * Examples include (but are not limited to): waiting for the shutter button to be enabled in GCA
+ * or long loading times for Gmail. The reason or symptom may be clarified by the included message,
+ * but should not speculate if there is any reasonable doubt.
+ */
+public class UiTimeoutException extends RuntimeException {
+ public UiTimeoutException(String msg) {
+ super(msg);
+ }
+
+ public UiTimeoutException(String msg, Throwable cause) {
+ super(msg, cause);
+ }
+}
diff --git a/libraries/base-app-helpers/src/android/platform/test/helpers/exceptions/UnknownUiException.java b/libraries/base-app-helpers/src/android/platform/test/helpers/exceptions/UnknownUiException.java
new file mode 100644
index 0000000..b06df06
--- /dev/null
+++ b/libraries/base-app-helpers/src/android/platform/test/helpers/exceptions/UnknownUiException.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.platform.test.helpers.exceptions;
+
+/**
+ * An UnknownUiException is an exception specific to UI-driven app helpers. This should be thrown
+ * when specific UI conditions, generally post-conditions, are not met for some unknown reason.
+ * <p>
+ * Examples include (but are not limited to): opening an e-mail and not finding any open message,
+ * loading a website and not seeing any content, being on GCA in camera mode without a flash button.
+ * <p>
+ * These exceptions are likely a manifestation of unhandled conditions or UI updates, but cannot
+ * explicitly say so without further diagnosis.
+ */
+public class UnknownUiException extends RuntimeException {
+ public UnknownUiException(String msg) {
+ super(msg);
+ }
+
+ public UnknownUiException(String msg, Throwable cause) {
+ super(msg, cause);
+ }
+}
diff --git a/libraries/chrome-app-helper/Android.mk b/libraries/chrome-app-helper/Android.mk
new file mode 100644
index 0000000..7f0c1d0
--- /dev/null
+++ b/libraries/chrome-app-helper/Android.mk
@@ -0,0 +1,23 @@
+#
+# Copyright (C) 2016 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT 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 := chrome-app-helper
+LOCAL_JAVA_LIBRARIES := ub-uiautomator base-app-helpers
+LOCAL_SRC_FILES := $(call all-java-files-under, src)
+
+include $(BUILD_STATIC_JAVA_LIBRARY)
diff --git a/libraries/chrome-app-helper/src/android/platform/test/helpers/ChromeHelperImpl.java b/libraries/chrome-app-helper/src/android/platform/test/helpers/ChromeHelperImpl.java
new file mode 100644
index 0000000..25d8755
--- /dev/null
+++ b/libraries/chrome-app-helper/src/android/platform/test/helpers/ChromeHelperImpl.java
@@ -0,0 +1,307 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.platform.test.helpers;
+
+import android.app.Instrumentation;
+import android.os.SystemClock;
+import android.support.test.uiautomator.By;
+import android.support.test.uiautomator.Direction;
+import android.support.test.uiautomator.UiObject2;
+import android.support.test.uiautomator.Until;
+import android.util.Log;
+import android.webkit.WebView;
+import android.widget.ListView;
+
+import java.io.IOException;
+
+public class ChromeHelperImpl extends AbstractChromeHelper {
+ private static final String LOG_TAG = ChromeHelperImpl.class.getSimpleName();
+
+ private static final String UI_MENU_BUTTON_ID = "menu_button";
+ private static final String UI_SEARCH_BOX_ID = "search_box_text";
+ private static final String UI_URL_BAR_ID = "url_bar";
+ private static final String UI_VIEW_HOLDER_ID = "compositor_view_holder";
+ private static final String UI_POSITIVE_BUTTON_ID = "positive_button";
+ private static final String UI_NEGATIVE_BUTTON_ID = "negative_button";
+
+ private static final long APP_INIT_WAIT = 10000;
+ private static final long MAX_DIALOG_TRANSITION = 5000;
+ private static final long PAGE_LOAD_TIMEOUT = 30 * 1000;
+ private static final long ANIMATION_TIMEOUT = 3000;
+
+ private String mPackageName;
+ private String mLauncherName;
+
+ public ChromeHelperImpl(Instrumentation instr) {
+ super(instr);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public String getPackage() {
+ if (mPackageName == null) {
+ String prop = null;
+ try {
+ mDevice.executeShellCommand("getprop dev.chrome.package");
+ } catch (IOException ioe) {
+ // log but ignore
+ Log.e(LOG_TAG, "IOException while getprop", ioe);
+ }
+ if (prop == null || prop.isEmpty()) {
+ prop = "com.android.chrome";
+ }
+ mPackageName = prop;
+ }
+ return mPackageName;
+ }
+
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public String getLauncherName() {
+ if (mLauncherName == null) {
+ String prop = null;
+ try {
+ mDevice.executeShellCommand("getprop dev.chrome.name");
+ } catch (IOException ioe) {
+ // log but ignore
+ Log.e(LOG_TAG, "IOException while getprop", ioe);
+ }
+ if (prop == null || prop.isEmpty()) {
+ prop = "Chrome";
+ }
+ mLauncherName = prop;
+ }
+ return mLauncherName;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void dismissInitialDialogs() {
+ // Terms of Service
+ UiObject2 tos = mDevice.wait(Until.findObject(By.res(getPackage(), "terms_accept")),
+ APP_INIT_WAIT);
+ if (tos != null) {
+ tos.click();
+ }
+
+ if (!hasAccountRegistered()) {
+ // Device has no accounts registered that Chrome recognizes
+ // Select negative button to skip setup wizard sign in
+ UiObject2 negative = mDevice.wait(Until.findObject(
+ By.res(getPackage(), UI_NEGATIVE_BUTTON_ID)), MAX_DIALOG_TRANSITION);
+
+ if (negative != null) {
+ negative.click();
+ }
+ } else {
+ // Device has an account registered that Chrome recognizes
+ // Press positive buttons until through setup wizard
+ for (int i = 0; i < 4; i++) {
+ if (!isInSetupWizard()) {
+ break;
+ }
+
+ UiObject2 positive = mDevice.wait(Until.findObject(
+ By.res(getPackage(), UI_POSITIVE_BUTTON_ID)), MAX_DIALOG_TRANSITION);
+ if (positive != null) {
+ positive.click();
+ }
+ }
+ }
+
+ mDevice.wait(Until.findObject(By.res(getPackage(), UI_SEARCH_BOX_ID)),
+ MAX_DIALOG_TRANSITION);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void openUrl(String url) {
+ UiObject2 urlBar = getUrlBar();
+ if (urlBar == null) {
+ throw new IllegalStateException("Failed to detect a URL bar");
+ }
+
+ mDevice.waitForIdle();
+ urlBar.setText(url);
+ mDevice.pressEnter();
+ waitForPageLoad();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void flingPage(Direction dir) {
+ UiObject2 page = getWebPage();
+ if (page != null) {
+ int minDim = Math.min(
+ page.getVisibleBounds().width(), page.getVisibleBounds().height());
+ page.setGestureMargin((int)Math.floor(minDim * 0.25));
+ page.fling(dir);
+ } else {
+ Log.e(LOG_TAG, String.format("Failed to fling page %s", dir.toString()));
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void openMenu() {
+ UiObject2 menuButton = null;
+ for (int retries = 2; retries > 0; retries--) {
+ menuButton = mDevice.findObject(By.desc("More options"));
+ if (menuButton == null) {
+ flingPage(Direction.UP);
+ } else {
+ break;
+ }
+ }
+
+ if (menuButton == null) {
+ throw new IllegalStateException("Unable to find menu button.");
+ }
+ menuButton.clickAndWait(Until.newWindow(), 5000);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void mergeTabs() {
+ openSettings();
+ mDevice.findObject(By.text("Merge tabs and apps")).click();
+ if (mDevice.findObject(By.text("On")) != null) {
+ // Merge tabs is already on
+ mDevice.pressBack();
+ mDevice.pressBack();
+ } else {
+ mDevice.findObject(By.res(getPackage(), "switch_widget")).click();
+ mDevice.findObject(By.text("OK")).click();
+ }
+ SystemClock.sleep(5000);
+ waitForPageLoad();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void unmergeTabs() {
+ openSettings();
+ mDevice.findObject(By.text("Merge tabs and apps")).click();
+ if (mDevice.findObject(By.text("Off")) != null) {
+ // Merge tabs is already off
+ mDevice.pressBack();
+ mDevice.pressBack();
+ } else {
+ mDevice.findObject(By.res(getPackage(), "switch_widget")).click();
+ mDevice.findObject(By.text("OK")).click();
+ }
+ SystemClock.sleep(5000);
+ waitForPageLoad();
+ }
+
+ private void openSettings() {
+ openMenu();
+ UiObject2 menu = getMenu();
+ // TODO: Change this to be non-constant
+ menu.setGestureMargin(500);
+ menu.scroll(Direction.DOWN, 1.0f);
+ // Open the Settings menu
+ mDevice.findObject(By.desc("Settings")).clickAndWait(Until.newWindow(), 3000);
+ }
+
+ private UiObject2 getWebPage() {
+ mDevice.waitForIdle();
+
+ UiObject2 webView = mDevice.findObject(By.clazz(WebView.class));
+ if (webView != null) {
+ return webView;
+ }
+
+ UiObject2 viewHolder = mDevice.findObject(
+ By.res(getPackage(), UI_VIEW_HOLDER_ID));
+ return viewHolder;
+ }
+
+ private UiObject2 getUrlBar() {
+ // First time, URL bar is has id SEARCH_BOX_ID
+ UiObject2 urlLoc = mDevice.findObject(By.res(getPackage(), UI_SEARCH_BOX_ID));
+ if (urlLoc != null) {
+ urlLoc.click();
+ // Waits for the animation to complete.
+ mDevice.wait(Until.findObject(By.res(getPackage(), UI_URL_BAR_ID)), ANIMATION_TIMEOUT);
+ }
+
+ // Afterwards, URL bar has id URL_BAR_ID; must re-select
+ for (int retries = 2; retries > 0; retries--) {
+ urlLoc = mDevice.findObject(By.res(getPackage(), UI_URL_BAR_ID));
+ if (urlLoc == null) {
+ flingPage(Direction.UP);
+ } else {
+ break;
+ }
+ }
+
+ if (urlLoc != null) {
+ urlLoc.click();
+ } else {
+ throw new IllegalStateException("Failed to find a URL bar.");
+ }
+
+ return urlLoc;
+ }
+
+ private UiObject2 getMenu() {
+ return mDevice.findObject(By.clazz(ListView.class).pkg(getPackage()));
+ }
+
+ private void waitForPageLoad() {
+ mDevice.waitForIdle();
+ if (mDevice.hasObject(By.desc("Stop page loading"))) {
+ mDevice.wait(Until.gone(By.desc("Stop page loading")), PAGE_LOAD_TIMEOUT);
+ } else if (mDevice.hasObject(By.res(getPackage(), "progress"))) {
+ mDevice.wait(Until.gone(By.res(getPackage(), "progress")), PAGE_LOAD_TIMEOUT);
+ }
+ }
+
+ private boolean isInSetupWizard() {
+ return mDevice.hasObject(By.res(getPackage(), "fre_pager"));
+ }
+
+ private boolean hasAccountRegistered() {
+ boolean addAcountTextPresent = mDevice.wait(Until.hasObject(By.textStartsWith("Add an " +
+ "account")), MAX_DIALOG_TRANSITION);
+
+ UiObject2 next = mDevice.wait(Until.findObject(
+ By.res(getPackage(), UI_POSITIVE_BUTTON_ID)), MAX_DIALOG_TRANSITION);
+ boolean signInButtonPresent = next != null && "SIGN IN".equals(next.getText());
+
+ // If any of theese elements is present, then there is no account registered.
+ return !addAcountTextPresent && !signInButtonPresent;
+ }
+}
diff --git a/libraries/facebook-app-helper/Android.mk b/libraries/facebook-app-helper/Android.mk
new file mode 100644
index 0000000..b997aea
--- /dev/null
+++ b/libraries/facebook-app-helper/Android.mk
@@ -0,0 +1,23 @@
+#
+# Copyright (C) 2016 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT 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 := facebook-app-helper
+LOCAL_JAVA_LIBRARIES := ub-uiautomator base-app-helpers
+LOCAL_SRC_FILES := $(call all-java-files-under, src)
+
+include $(BUILD_STATIC_JAVA_LIBRARY)
diff --git a/libraries/facebook-app-helper/src/android/platform/test/helpers/FacebookHelperImpl.java b/libraries/facebook-app-helper/src/android/platform/test/helpers/FacebookHelperImpl.java
new file mode 100644
index 0000000..b7f39f6
--- /dev/null
+++ b/libraries/facebook-app-helper/src/android/platform/test/helpers/FacebookHelperImpl.java
@@ -0,0 +1,285 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.platform.test.helpers;
+
+import android.app.Instrumentation;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.os.SystemClock;
+import android.platform.test.helpers.exceptions.UnknownUiException;
+import android.support.test.uiautomator.By;
+import android.support.test.uiautomator.BySelector;
+import android.support.test.uiautomator.Direction;
+import android.support.test.uiautomator.Until;
+import android.support.test.uiautomator.UiDevice;
+import android.support.test.uiautomator.UiObject2;
+import android.util.Log;
+
+public class FacebookHelperImpl extends AbstractFacebookHelper {
+ private static final String TAG = "android.platform.test.helpers.FacebookHelperImpl";
+
+ private static final String UI_HOME_PAGE_CONTAINER_ID = "cs7";
+ private static final String UI_LOADING_VIEW_ID = "loading_view";
+ private static final String UI_LOGIN_BUTTON_ID = "bjb";
+ private static final String UI_LOGIN_PASSWORD_ID = "bj_";
+ private static final String UI_LOGIN_ROOT_ID = "bj6";
+ private static final String UI_LOGIN_USERNAME_ID = "bj8";
+ private static final String UI_NEWS_FEED_TAB_ID = "a0";
+ private static final String UI_NEWS_FEED_TAB_SELECTED_DESC = "News";
+ private static final String UI_PACKAGE_NAME = "com.facebook.katana";
+ private static final String UI_POST_BUTTON_ID = "rk";
+ private static final String UI_STATUS_TEXT_ID = "cmk";
+ private static final String UI_STATUS_UPDATE_BUTTON_ID = "bmp";
+ private static final String UI_LOGIN_ONE_TAP = "sc";
+
+ private static final long UI_LOGIN_WAIT = 30000;
+ private static final long UI_NAVIGATION_WAIT = 10000;
+
+ public FacebookHelperImpl(Instrumentation instr) {
+ super(instr);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void open() {
+ super.open();
+ mDevice.wait(Until.findObject(
+ By.res(UI_PACKAGE_NAME, UI_HOME_PAGE_CONTAINER_ID)), UI_NAVIGATION_WAIT);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public String getPackage() {
+ return UI_PACKAGE_NAME;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public String getLauncherName() {
+ return "Facebook";
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void dismissInitialDialogs() {
+
+ }
+
+ private UiObject2 getHomePageContainer() {
+ return mDevice.findObject(By.res(UI_PACKAGE_NAME, UI_HOME_PAGE_CONTAINER_ID));
+ }
+
+ private boolean isOnHomePage() {
+ return (getHomePageContainer() != null);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void scrollHomePage(Direction dir) {
+ UiObject2 scrollContainer = getHomePageContainer();
+ if (scrollContainer == null) {
+ throw new IllegalStateException("No valid scrolling mechanism found.");
+ }
+
+ scrollContainer.scroll(dir, 5.f);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void goToHomePage() {
+ // Try to go to the home page by repeatedly pressing the back button
+ for (int retriesRemaining = 5; retriesRemaining > 0 && !isOnHomePage();
+ --retriesRemaining) {
+ mDevice.pressBack();
+ mDevice.waitForIdle();
+ }
+ }
+
+ private UiObject2 getNewsFeedTab() {
+ return mDevice.findObject(By.res(UI_PACKAGE_NAME, UI_NEWS_FEED_TAB_ID));
+ }
+
+ private boolean isOnNewsFeed() {
+ UiObject2 newsFeedTab = getNewsFeedTab();
+ if (newsFeedTab == null) {
+ return false;
+ }
+
+ return newsFeedTab.getContentDescription().contains(UI_NEWS_FEED_TAB_SELECTED_DESC);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void goToNewsFeed() {
+ if (!isOnHomePage()) {
+ throw new IllegalStateException("Not on home page");
+ }
+
+ UiObject2 newsFeedTab = getNewsFeedTab();
+ if (newsFeedTab == null) {
+ throw new UnknownUiException("Could not find news feed tab");
+ }
+
+ newsFeedTab.click();
+ mDevice.wait(Until.findObject(By.res(UI_PACKAGE_NAME, UI_NEWS_FEED_TAB_ID).descContains(
+ UI_NEWS_FEED_TAB_SELECTED_DESC)), UI_NAVIGATION_WAIT);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void goToStatusUpdate() {
+ if (!isOnNewsFeed()) {
+ throw new IllegalStateException("Not on News Feed");
+ }
+
+ UiObject2 statusUpdateButton = null;
+ for (int retriesRemaining = 50; retriesRemaining > 0 && statusUpdateButton == null;
+ --retriesRemaining) {
+ scrollHomePage(Direction.UP);
+ statusUpdateButton = mDevice.findObject(
+ By.res(UI_PACKAGE_NAME, UI_STATUS_UPDATE_BUTTON_ID));
+ }
+ if (statusUpdateButton == null) {
+ throw new UnknownUiException("Could not find status update button");
+ }
+
+ statusUpdateButton.click();
+ mDevice.wait(Until.findObject(
+ By.res(UI_PACKAGE_NAME, UI_STATUS_TEXT_ID)), UI_NAVIGATION_WAIT);
+
+ getStatusTextField().click();
+ }
+
+ private UiObject2 getStatusTextField() {
+ return mDevice.findObject(By.res(UI_PACKAGE_NAME, UI_STATUS_TEXT_ID));
+ }
+
+ private boolean isOnStatusUpdatePage() {
+ return (mDevice.hasObject(By.text("Post to Facebook")) &&
+ mDevice.hasObject(By.text("What's on your mind?")));
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void clickStatusUpdateTextField() {
+ if (!isOnStatusUpdatePage()) {
+ throw new IllegalStateException("Not on status update page");
+ }
+
+ UiObject2 statusTextField = getStatusTextField();
+
+ if (statusTextField == null) {
+ throw new UnknownUiException("Cannot find status update text field");
+ }
+
+ statusTextField.click();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void setStatusText(String statusText) {
+ UiObject2 statusTextField = getStatusTextField();
+ if (statusTextField == null) {
+ throw new UnknownUiException("Could not find status text field");
+ }
+
+ statusTextField.setText(statusText);
+ }
+
+ private UiObject2 getPostButton() {
+ return mDevice.findObject(By.res(UI_PACKAGE_NAME, UI_POST_BUTTON_ID));
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void postStatusUpdate() {
+ UiObject2 postButton = getPostButton();
+ if (postButton == null) {
+ throw new UnknownUiException("Could not find post status button");
+ }
+
+ postButton.click();
+ mDevice.wait(Until.findObject(
+ By.res(UI_PACKAGE_NAME, UI_HOME_PAGE_CONTAINER_ID)), UI_NAVIGATION_WAIT);
+ }
+
+ private boolean isOnLoginPage() {
+ return (mDevice.findObject(By.res(UI_PACKAGE_NAME, UI_LOGIN_ROOT_ID)) != null);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void login(String username, String password) {
+ if (!isOnLoginPage()) {
+ return;
+ }
+
+ UiObject2 usernameTextField = mDevice.findObject(
+ By.res(UI_PACKAGE_NAME, UI_LOGIN_USERNAME_ID));
+ UiObject2 passwordTextField = mDevice.findObject(
+ By.res(UI_PACKAGE_NAME, UI_LOGIN_PASSWORD_ID));
+ UiObject2 loginButton = mDevice.findObject(By.res(UI_PACKAGE_NAME, UI_LOGIN_BUTTON_ID));
+ if (usernameTextField == null) {
+ throw new UnknownUiException("Could not find username text field");
+ }
+ if (passwordTextField == null) {
+ throw new UnknownUiException("Could not find password text field");
+ }
+ if (loginButton == null) {
+ throw new UnknownUiException("Could not find login button");
+ }
+
+ usernameTextField.setText(username);
+ passwordTextField.setText(password);
+ loginButton.click();
+
+ // Check if one tap login screen is prompted and click on it
+ UiObject2 oneTapLogin = mDevice.wait(Until.findObject(
+ By.res(UI_PACKAGE_NAME, UI_LOGIN_ONE_TAP)), UI_NAVIGATION_WAIT);
+ if (oneTapLogin != null) {
+ oneTapLogin.click();
+ }
+
+ mDevice.wait(Until.findObject(
+ By.res(UI_PACKAGE_NAME, UI_HOME_PAGE_CONTAINER_ID)), UI_NAVIGATION_WAIT);
+ // Wait for user content to load after logging in
+ mDevice.wait(Until.gone(By.res(UI_PACKAGE_NAME, UI_LOADING_VIEW_ID)), UI_LOGIN_WAIT);
+ }
+}
diff --git a/libraries/flightdemo-app-helper/Android.mk b/libraries/flightdemo-app-helper/Android.mk
new file mode 100644
index 0000000..c6c6b84
--- /dev/null
+++ b/libraries/flightdemo-app-helper/Android.mk
@@ -0,0 +1,23 @@
+#
+# Copyright (C) 2016 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT 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 := flightdemo-app-helper
+LOCAL_JAVA_LIBRARIES := ub-uiautomator base-app-helpers
+LOCAL_SRC_FILES := $(call all-java-files-under, src)
+
+include $(BUILD_STATIC_JAVA_LIBRARY)
diff --git a/libraries/flightdemo-app-helper/src/android/platform/test/helpers/FlightDemoHelperImpl.java b/libraries/flightdemo-app-helper/src/android/platform/test/helpers/FlightDemoHelperImpl.java
new file mode 100644
index 0000000..a8e069d
--- /dev/null
+++ b/libraries/flightdemo-app-helper/src/android/platform/test/helpers/FlightDemoHelperImpl.java
@@ -0,0 +1,123 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.platform.test.helpers;
+
+import android.app.Instrumentation;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.os.RemoteException;
+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.Until;
+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.UiSelector;
+import android.util.Log;
+
+import java.util.regex.Pattern;
+
+public class FlightDemoHelperImpl extends AbstractFlightDemoHelper {
+ private static final String LOG_TAG = FlightDemoHelperImpl.class.getCanonicalName();
+ private static final String UI_PACKAGE_NAME = "leofs.android.free";
+ private static final String UI_ACTIVITY_NAME = "leofs.android.free.LeofsActivity";
+
+ private static final int UI_RESPONSE_WAIT = 2000; // 2 secs
+ private static final int MAX_MENU_SCROLL_DOWN_COUNT = 10;
+
+ public FlightDemoHelperImpl(Instrumentation instr) {
+ super(instr);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public String getPackage() {
+ return UI_PACKAGE_NAME;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public String getLauncherName() {
+ return "Leo´s RC Simulator";
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void dismissInitialDialogs() {
+ // Nothing to do here. There is no initial dialog in this app.
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void startDemo() {
+ Log.v(LOG_TAG, "Starting flight simulator demo");
+ selectMenuItem("Demo");
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void stopDemo() {
+ Log.v(LOG_TAG, "Stopping flight simulator demo");
+ selectMenuItem("Reset");
+ mDevice.pressBack();
+ }
+
+ private void selectMenuItem(String item) {
+ mDevice.pressMenu();
+ UiObject2 container = mDevice.wait(Until.findObject(By.res("android", "list")),
+ UI_RESPONSE_WAIT);
+ if (container == null) {
+ throw new IllegalStateException("Cannot find scrollable menu");
+ }
+
+ String err_msg = String.format("Cannot find menu item %s", item);
+ int scroll_counter = 0;
+ UiObject2 button = null;
+ boolean reachedEnd = false;
+ while (!reachedEnd) {
+ final Pattern word = Pattern.compile(item, Pattern.CASE_INSENSITIVE);
+ button = mDevice.wait(Until.findObject(By.text(word)), UI_RESPONSE_WAIT);
+ if (button != null) {
+ button.click();
+ break;
+ }
+
+ if (!container.scroll(Direction.DOWN, 1.0f) &&
+ scroll_counter >= MAX_MENU_SCROLL_DOWN_COUNT) {
+ reachedEnd = true;
+ }
+ scroll_counter++;
+ }
+ if (button != null) {
+ button.click();
+ }
+ else {
+ throw new IllegalStateException(err_msg);
+ }
+ }
+}
diff --git a/libraries/gmail-app-helper/Android.mk b/libraries/gmail-app-helper/Android.mk
new file mode 100644
index 0000000..d9db92c
--- /dev/null
+++ b/libraries/gmail-app-helper/Android.mk
@@ -0,0 +1,23 @@
+#
+# Copyright (C) 2016 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT 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 := gmail-app-helper
+LOCAL_JAVA_LIBRARIES := ub-uiautomator base-app-helpers
+LOCAL_SRC_FILES := $(call all-java-files-under, src)
+
+include $(BUILD_STATIC_JAVA_LIBRARY)
diff --git a/libraries/gmail-app-helper/src/android/platform/test/helpers/GmailHelperImpl.java b/libraries/gmail-app-helper/src/android/platform/test/helpers/GmailHelperImpl.java
new file mode 100644
index 0000000..98482c2
--- /dev/null
+++ b/libraries/gmail-app-helper/src/android/platform/test/helpers/GmailHelperImpl.java
@@ -0,0 +1,654 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.platform.test.helpers;
+
+import android.app.Instrumentation;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.content.res.Configuration;
+import android.os.SystemClock;
+import android.platform.test.helpers.exceptions.UnknownUiException;
+import android.support.test.uiautomator.By;
+import android.support.test.uiautomator.BySelector;
+import android.support.test.uiautomator.Direction;
+import android.support.test.uiautomator.Until;
+import android.support.test.uiautomator.UiDevice;
+import android.support.test.uiautomator.UiObject2;
+import android.util.Log;
+import android.webkit.WebView;
+import android.widget.ListView;
+import android.widget.ImageButton;
+
+import java.io.IOException;
+import java.util.List;
+import java.util.regex.Pattern;
+
+public class GmailHelperImpl extends AbstractGmailHelper {
+ private static final String LOG_TAG = GmailHelperImpl.class.getSimpleName();
+
+ private static final long APP_INIT_WAIT = 10000;
+ private static final long DIALOG_TIMEOUT = 5000;
+ private static final long POPUP_TIMEOUT = 7500;
+ private static final long COMPOSE_TIMEOUT = 10000;
+ private static final long SEND_TIMEOUT = 10000;
+ private static final long LOADING_TIMEOUT = 25000;
+ private static final long LOAD_EMAIL_TIMEOUT = 20000;
+ private static final long WIFI_TIMEOUT = 60 * 1000;
+ private static final long RELOAD_INBOX_TIMEOUT = 10 * 1000;
+ private static final long COMPOSE_EMAIL_TIMEOUT = 10 * 1000;
+
+ private static final String UI_ATTACHMENT_TILE_SAVE_ID = "attachment_tile_save";
+ private static final String UI_NAME_ID = "name";
+ private static final String UI_PACKAGE_NAME = "com.google.android.gm";
+ private static final String UI_PROMO_ACTION_NEG_RES = "promo_action_negative_single_line";
+ private static final String UI_CONVERSATIONS_LIST_ID = "conversation_list_view";
+ private static final String UI_CONVERSATION_LIST_LOADING_VIEW_ID =
+ "conversation_list_loading_view";
+ private static final String UI_CONVERSATION_PAGER = "conversation_pager";
+ private static final String UI_MULTI_PANE_CONTAINER_ID = "two_pane_activity";
+ private static final BySelector PRIMARY_SELECTOR =
+ By.res(UI_PACKAGE_NAME, "name").text("Primary");
+ private static final BySelector INBOX_SELECTOR =
+ By.res(UI_PACKAGE_NAME, "name").text("Inbox");
+ private static final BySelector NAV_DRAWER_SELECTOR = By.res("android", "list").focused(true);
+
+ public GmailHelperImpl(Instrumentation instr) {
+ super(instr);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public String getPackage() {
+ return "com.google.android.gm";
+ }
+
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public String getLauncherName() {
+ return "Gmail";
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void dismissInitialDialogs() {
+ // Check for the first, option dialog dismissal screen
+ if (mDevice.wait(Until.hasObject(By.res(UI_PACKAGE_NAME, "welcome_tour_pager")),
+ APP_INIT_WAIT)) {
+ // Dismiss "New in Gmail" with GOT IT button or "Wecome to Gmail" with > button
+ BySelector gotItSelector = By.res(UI_PACKAGE_NAME, "welcome_tour_got_it");
+ BySelector skipSelector = By.res(UI_PACKAGE_NAME, "welcome_tour_skip");
+ if (mDevice.hasObject(gotItSelector)) {
+ mDevice.findObject(gotItSelector).clickAndWait(Until.newWindow(), DIALOG_TIMEOUT);
+ } else if (mDevice.hasObject(skipSelector)) {
+ mDevice.findObject(skipSelector).clickAndWait(Until.newWindow(), DIALOG_TIMEOUT);
+ }
+ } else {
+ Log.e(LOG_TAG, "Unable to find initial screen. Continuing anyway.");
+ }
+ // Dismiss "Add another email address" with TAKE ME TO GMAIL button
+ UiObject2 tutorialDone = mDevice.wait(Until.findObject(
+ By.res(UI_PACKAGE_NAME, "action_done")), DIALOG_TIMEOUT);
+ if (tutorialDone != null) {
+ tutorialDone.clickAndWait(Until.newWindow(), DIALOG_TIMEOUT);
+ }
+ // Dismiss dogfood confidentiality dialog with OK, GOT IT button
+ Pattern gotItWord = Pattern.compile("OK, GOT IT", Pattern.CASE_INSENSITIVE);
+ UiObject2 splash = mDevice.wait(Until.findObject(By.text(gotItWord)), DIALOG_TIMEOUT);
+ if (splash != null) {
+ splash.clickAndWait(Until.newWindow(), DIALOG_TIMEOUT);
+ }
+ // Wait for "Getting your messages" to disappear
+ if (mDevice.findObject(By.textContains("Getting your messages")) != null) {
+ if (!mDevice.wait(Until.gone(By.text("Getting your messages")), WIFI_TIMEOUT)) {
+ throw new UnknownUiException(
+ "Timed out waiting for 'Getting your messages' to disappear");
+ }
+ }
+ if (!mDevice.wait(Until.hasObject(
+ By.res(UI_PACKAGE_NAME, UI_CONVERSATIONS_LIST_ID)), WIFI_TIMEOUT)) {
+ throw new UnknownUiException("Timed out waiting for conversation list to appear");
+ }
+ // Dismiss "Tap a sender image" dialog
+ UiObject2 senderImageDismissButton =
+ mDevice.findObject(By.res(UI_PACKAGE_NAME, "dismiss_icon"));
+ if (senderImageDismissButton != null) {
+ senderImageDismissButton.click();
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void goToInbox() {
+ // Check if already in Inbox or Primary
+ if (isInPrimaryOrInbox()) {
+ return;
+ }
+
+ if (isMultiPaneActivity()) {
+ // Select for the closed Primary icon
+ UiObject2 primaryClosed = mDevice.findObject(
+ By.res(UI_PACKAGE_NAME, "image_view").text("Primary"));
+ if (primaryClosed != null) {
+ primaryClosed.click();
+ mDevice.waitForIdle();
+ return;
+ }
+
+ // Select for the closed Inbox icon
+ UiObject2 inboxClosed = mDevice.findObject(
+ By.res(UI_PACKAGE_NAME, "image_view").text("Inbox"));
+ if (inboxClosed != null) {
+ inboxClosed.click();
+ mDevice.waitForIdle();
+ return;
+ }
+
+ scrollNavigationDrawer(Direction.UP);
+
+ // Select for the open Primary icon
+ UiObject2 primaryOpen = mDevice.findObject(
+ By.res(UI_PACKAGE_NAME, "name").text("Primary"));
+ if (primaryOpen != null) {
+ primaryOpen.click();
+ mDevice.waitForIdle();
+ return;
+ }
+
+ // Select for the open Inbox icon
+ UiObject2 inboxOpen = mDevice.findObject(
+ By.res(UI_PACKAGE_NAME, "name").text("Inbox"));
+ if (inboxOpen != null) {
+ inboxOpen.click();
+ mDevice.waitForIdle();
+ return;
+ }
+
+ // Currently unhandled case; throw Exception.
+ throw new RuntimeException("Unable to find method to get to Primary/Inbox");
+ } else {
+ // Simply press back if in a conversation
+ if (isInConversation()) {
+ mDevice.pressBack();
+ waitForConversationsList();
+ }
+
+ // If in another e-mail sub-folder, go to Primary or Inbox
+ if (!isInPrimaryOrInbox()) {
+ // Search with the navigation drawer
+ openNavigationDrawer();
+
+ // Select for "Primary" and for "Inbox"
+ UiObject2 primaryInboxSelector = mDevice.findObject(PRIMARY_SELECTOR);
+ if (primaryInboxSelector == null) {
+ primaryInboxSelector = mDevice.findObject(INBOX_SELECTOR);
+ }
+
+ primaryInboxSelector.click();
+ waitForConversationsList();
+ }
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void goToComposeEmail() {
+ if (!isInPrimaryOrInbox()) {
+ throw new IllegalStateException("Gmail is not on the Inbox or Primary page");
+ }
+ UiObject2 compose = mDevice.findObject(By.desc("Compose"));
+ if (compose == null) {
+ throw new UnknownUiException("Compose button not found");
+ }
+ compose.clickAndWait(Until.newWindow(), COMPOSE_TIMEOUT);
+ waitForCompose();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void openEmailByIndex(int index) {
+ if (!isInMailbox()) {
+ throw new IllegalStateException("Must be in a mailbox to open an email by index");
+ }
+
+ if (index >= getVisibleEmailCount()) {
+ throw new IllegalArgumentException(String.format("Cannot select %s'th message of %s",
+ (index + 1), getVisibleEmailCount()));
+ }
+
+ // Select an e-mail by index
+ UiObject2 conversationList = getConversationList();
+ List<UiObject2> emails = conversationList.findObjects(
+ By.clazz(android.widget.FrameLayout.class));
+ if (conversationList == null) {
+ throw new UnknownUiException("No e-mails found.");
+ }
+ emails.get(index).click();
+
+ // Wait until the e-mail is open
+ UiObject2 loadMsg = mDevice.findObject(By.res(UI_PACKAGE_NAME, "loading_progress"));
+ if (loadMsg != null) {
+ if (!mDevice.wait(Until.gone(
+ By.res(UI_PACKAGE_NAME, "loading_progress")), LOADING_TIMEOUT)) {
+ throw new RuntimeException("Loading message timed out after 20s");
+ }
+ }
+
+ waitForConversation();
+ }
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public int getVisibleEmailCount() {
+ if (!isInMailbox()) {
+ throw new IllegalStateException("Must be in a mailbox to open an email by index");
+ }
+
+ return getConversationList().getChildCount();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void sendReplyEmail(String address, String body) {
+ if (!isInConversation()) {
+ throw new IllegalStateException("Must have an e-mail open to send a reply.");
+ }
+
+ UiObject2 convScroll = getConversationPager();
+ while(convScroll.scroll(Direction.DOWN, 1.0f));
+
+ UiObject2 replyButton = mDevice.findObject(By.text("Reply"));
+ if (replyButton != null) {
+ replyButton.clickAndWait(Until.newWindow(), COMPOSE_TIMEOUT);
+ waitForCompose();
+ } else {
+ throw new UnknownUiException("Failed to find a 'Reply' button.");
+ }
+
+ // Set the necessary fields (address and body)
+ setEmailToAddress(address);
+ setEmailBody(body);
+
+ // Send the reply e-mail and wait for original e-mail
+ clickSendButton();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void setEmailToAddress(String address) {
+ UiObject2 convScroll = getComposeScrollContainer();
+ while (convScroll.scroll(Direction.UP, 1.0f));
+
+ UiObject2 toField = getToField();
+ for (int retries = 5; retries > 0 && toField == null; retries--) {
+ convScroll.scroll(Direction.DOWN, 1.0f);
+ toField = getToField();
+ }
+
+ if (toField != null) {
+ toField.setText(address);
+ } else {
+ throw new UnknownUiException("Failed to find a 'To' field.");
+ }
+ }
+
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void setEmailSubject(String subject) {
+ UiObject2 convScroll = getComposeScrollContainer();
+ while (convScroll.scroll(Direction.UP, 1.0f));
+
+ UiObject2 subjectField = getSubjectField();
+ for (int retries = 5; retries > 0 && subjectField == null; retries--) {
+ convScroll.scroll(Direction.DOWN, 1.0f);
+ subjectField = getSubjectField();
+ }
+
+ if (subjectField != null) {
+ subjectField.setText(subject);
+ } else {
+ throw new UnknownUiException("Failed to find a 'Subject' field.");
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void setEmailBody(String body) {
+ UiObject2 convScroll = getComposeScrollContainer();
+ while (convScroll.scroll(Direction.UP, 1.0f));
+
+ UiObject2 bodyField = getBodyField();
+ for (int retries = 5; retries > 0 && bodyField == null; retries--) {
+ convScroll.scroll(Direction.DOWN, 1.0f);
+ bodyField = getBodyField();
+ }
+
+ if (bodyField != null) {
+ // Ensure the focus is left in the body field.
+ bodyField.click();
+ bodyField.setText(body);
+ } else {
+ throw new UnknownUiException("Failed to find a 'Body' field.");
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void clickSendButton() {
+ UiObject2 convScroll = getComposeScrollContainer();
+ while (convScroll.scroll(Direction.UP, 1.0f));
+
+ UiObject2 sendButton = getSendButton();
+ if (sendButton != null) {
+ sendButton.clickAndWait(Until.newWindow(), SEND_TIMEOUT);
+ waitForConversation();
+ } else {
+ throw new UnknownUiException("Failed to find a 'Send' button.");
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public String getComposeEmailBody(){
+ UiObject2 bodyField = getBodyField();
+ return bodyField.getText();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void openNavigationDrawer() {
+ for (int retries = 3; retries > 0; retries--) {
+ if (isNavDrawerOpen()) {
+ return;
+ }
+
+ UiObject2 nav = mDevice.findObject(By.desc(Pattern.compile(
+ "(Open navigation drawer)|(Navigate up)")));
+
+ if (nav == null) {
+ throw new IllegalStateException("Could not find navigation drawer");
+ }
+ nav.click();
+ mDevice.waitForIdle();
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void scrollNavigationDrawer(Direction dir) {
+ if (dir == Direction.RIGHT || dir == Direction.LEFT) {
+ throw new IllegalArgumentException("Can only scroll navigation drawer up and down.");
+ }
+
+ UiObject2 scroll = getNavDrawerContainer();
+ if (scroll == null) {
+ throw new UnknownUiException("No navigation drawer found to scroll");
+ }
+ scroll.scroll(dir, 1.0f);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public boolean closeNavigationDrawer() {
+ UiObject2 navDrawer = mDevice.wait(Until.findObject(
+ By.clazz(ImageButton.class).desc("Close navigation drawer")), 1000);
+ if (navDrawer != null) {
+ navDrawer.click();
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public boolean isInComposeEmail(){
+ return mDevice.findObject(By.res(UI_PACKAGE_NAME, "compose")) != null;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public boolean isInPrimaryOrInbox() {
+ if (isMultiPaneActivity()) {
+ return (mDevice.hasObject(By.res(UI_PACKAGE_NAME, "actionbar_title").text("Primary")) ||
+ mDevice.hasObject(By.res(UI_PACKAGE_NAME, "actionbar_title").text("Inbox")));
+ } else {
+ return getConversationList() != null &&
+ (mDevice.hasObject(By.text("Primary")) ||
+ mDevice.hasObject(By.text("Inbox")));
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void scrollMailbox(Direction direction, float amount, boolean scrollToEnd) {
+ if (!isInMailbox()) {
+ throw new IllegalStateException("Not in mailbox");
+ }
+
+ if (!(Direction.UP.equals(direction) || Direction.DOWN.equals(direction))) {
+ throw new IllegalArgumentException("Scroll direction must be UP or DOWN");
+ }
+
+ UiObject2 scrollContainer = getConversationList();
+ if (scrollContainer == null) {
+ throw new IllegalStateException("Could not find scroll container");
+ }
+
+ scroll(scrollContainer, direction, amount, scrollToEnd);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void scrollEmail(Direction direction, float amount, boolean scrollToEnd) {
+ if (!(Direction.UP.equals(direction) || Direction.DOWN.equals(direction))) {
+ throw new IllegalArgumentException("Scroll direction must be UP or DOWN");
+ }
+
+ UiObject2 scrollContainer = getConversationPager();
+ if (scrollContainer == null) {
+ throw new IllegalStateException("Could not find email scroll container");
+ }
+
+ scroll(scrollContainer, direction, amount, scrollToEnd);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void openMailbox(String mailboxName) {
+ if (!isNavDrawerOpen()) {
+ throw new IllegalStateException("Navigation drawer is not open");
+ }
+
+ UiObject2 mailbox = null;
+ for (int scrollsRemaining = 5; scrollsRemaining > 0; --scrollsRemaining) {
+ mailbox = mDevice.findObject(By.res(UI_PACKAGE_NAME, UI_NAME_ID).text(
+ Pattern.compile(mailboxName, Pattern.CASE_INSENSITIVE)));
+ if (mailbox != null) {
+ break;
+ } else {
+ scrollNavigationDrawer(Direction.DOWN);
+ }
+ }
+ if (mailbox == null) {
+ throw new IllegalArgumentException(
+ String.format("Could not find mailbox '%s'", mailboxName));
+ }
+ mailbox.click();
+ mDevice.waitForIdle();
+ mDevice.wait(Until.gone(
+ By.res(UI_PACKAGE_NAME, UI_CONVERSATION_LIST_LOADING_VIEW_ID)), WIFI_TIMEOUT);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void returnToMailbox() {
+ for (int retriesRemaining = 5; retriesRemaining > 0; --retriesRemaining) {
+ if (isInMailbox()) {
+ break;
+ } else {
+ mDevice.pressBack();
+ mDevice.waitForIdle();
+ }
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void downloadAttachment(int index) {
+ if (!isInConversation()) {
+ throw new IllegalStateException("Email is not open");
+ }
+
+ List<UiObject2> downloadButtons =
+ mDevice.findObjects(By.res(UI_PACKAGE_NAME, UI_ATTACHMENT_TILE_SAVE_ID));
+ if (downloadButtons != null && index >= 0 && index < downloadButtons.size()) {
+ downloadButtons.get(index).click();
+ } else {
+ throw new IndexOutOfBoundsException("attachment index out of bounds");
+ }
+ }
+
+ private UiObject2 getToField() {
+ return mDevice.findObject(By.res(UI_PACKAGE_NAME, "to"));
+ }
+
+ private UiObject2 getSubjectField() {
+ return mDevice.findObject(By.res(UI_PACKAGE_NAME, "subject"));
+ }
+
+ private UiObject2 getBodyField() {
+ return mDevice.findObject(By.res(UI_PACKAGE_NAME, "body"));
+ }
+
+ private UiObject2 getSendButton() {
+ return mDevice.findObject(By.desc("Send"));
+ }
+
+ private UiObject2 getComposeScrollContainer() {
+ return mDevice.findObject(By.res(UI_PACKAGE_NAME, "compose"));
+ }
+
+ private UiObject2 getNavDrawerContainer() {
+ return mDevice.findObject(NAV_DRAWER_SELECTOR);
+ }
+
+ private UiObject2 getConversationList() {
+ return mDevice.findObject(By.res(UI_PACKAGE_NAME, UI_CONVERSATIONS_LIST_ID));
+ }
+
+ private UiObject2 getConversationPager() {
+ return mDevice.findObject(By.res(UI_PACKAGE_NAME, UI_CONVERSATION_PAGER));
+ }
+
+ private boolean isInConversation() {
+ return mDevice.hasObject(By.res(UI_PACKAGE_NAME, UI_CONVERSATION_PAGER));
+ }
+
+ private boolean isNavDrawerOpen() {
+ if (isMultiPaneActivity()) {
+ return mDevice.hasObject(By.res("android", "list"));
+ } else {
+ return mDevice.hasObject(NAV_DRAWER_SELECTOR);
+ }
+ }
+
+ private void waitForConversationsList() {
+ mDevice.wait(Until.hasObject(
+ By.res(UI_PACKAGE_NAME, UI_CONVERSATIONS_LIST_ID)), RELOAD_INBOX_TIMEOUT);
+ }
+
+ private void waitForConversation() {
+ mDevice.wait(Until.hasObject(
+ By.res(UI_PACKAGE_NAME, UI_CONVERSATION_PAGER)), LOAD_EMAIL_TIMEOUT);
+ }
+
+ private void waitForCompose() {
+ mDevice.wait(Until.findObject(By.res(UI_PACKAGE_NAME, "compose")), COMPOSE_EMAIL_TIMEOUT);
+ }
+
+ private boolean isMultiPaneActivity() {
+ return mDevice.hasObject(By.res(UI_PACKAGE_NAME, UI_MULTI_PANE_CONTAINER_ID));
+ }
+
+ private boolean isInMailbox() {
+ if (isMultiPaneActivity()) {
+ return mDevice.hasObject(By.desc("Search"));
+ } else {
+ return mDevice.hasObject(By.desc("Search")) && getNavDrawerContainer() == null;
+ }
+ }
+
+ private void scroll(UiObject2 scrollContainer, Direction direction,
+ float amount, boolean scrollToEnd) {
+ if (amount < 0.0f) {
+ throw new IllegalArgumentException("Scroll amount cannot be negative");
+ }
+
+ if (scrollToEnd) {
+ while (scrollContainer.scroll(direction, 1.0f)) {
+ // empty
+ }
+ } else {
+ scrollContainer.scroll(direction, (float) amount);
+ }
+ }
+}
diff --git a/libraries/google-app-camera-helper/Android.mk b/libraries/google-app-camera-helper/Android.mk
new file mode 100644
index 0000000..2aa862e
--- /dev/null
+++ b/libraries/google-app-camera-helper/Android.mk
@@ -0,0 +1,23 @@
+#
+# Copyright (C) 2016 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT 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 := google-camera-app-helper
+LOCAL_JAVA_LIBRARIES := ub-uiautomator base-app-helpers launcher-helper-lib
+LOCAL_SRC_FILES := $(call all-java-files-under, src)
+
+include $(BUILD_STATIC_JAVA_LIBRARY)
diff --git a/libraries/google-app-camera-helper/src/android/platform/test/helpers/GoogleCameraHelperImpl.java b/libraries/google-app-camera-helper/src/android/platform/test/helpers/GoogleCameraHelperImpl.java
new file mode 100644
index 0000000..3a559ae
--- /dev/null
+++ b/libraries/google-app-camera-helper/src/android/platform/test/helpers/GoogleCameraHelperImpl.java
@@ -0,0 +1,1201 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.platform.test.helpers;
+
+import android.app.Instrumentation;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.os.SystemClock;
+import android.platform.test.helpers.exceptions.UnknownUiException;
+import android.support.test.launcherhelper.ILauncherStrategy;
+import android.support.test.uiautomator.By;
+import android.support.test.uiautomator.BySelector;
+import android.support.test.uiautomator.Configurator;
+import android.support.test.uiautomator.Direction;
+import android.support.test.uiautomator.Until;
+import android.support.test.uiautomator.UiDevice;
+import android.support.test.uiautomator.UiObject2;
+import android.support.test.uiautomator.UiWatcher;
+import android.util.Log;
+
+import java.text.SimpleDateFormat;
+import java.text.DateFormat;
+import java.util.Date;
+import java.util.regex.Pattern;
+
+public class GoogleCameraHelperImpl extends AbstractGoogleCameraHelper {
+ private static final String LOG_TAG = GoogleCameraHelperImpl.class.getSimpleName();
+ private static final String UI_ACTIVITY_VIEW_ID = "activity_root_view";
+ private static final String UI_ALBUM_FILMSTRIP_VIEW_ID = "filmstrip_view";
+ private static final String UI_PACKAGE_NAME = "com.android.camera2";
+ private static final String UI_RECORDING_TIME_ID = "recording_time";
+ private static final String UI_SHUTTER_DESC_CAM_3X = "Capture photo";
+ private static final String UI_SHUTTER_DESC_CAM_2X = "Shutter";
+ private static final String UI_SHUTTER_DESC_VID_3X = "Capture video";
+ private static final String UI_SHUTTER_DESC_VID_2X = "Shutter";
+ private static final String UI_THUMBNAIL_ALBUM_BUTTON_ID = "rounded_thumbnail_view";
+ private static final String UI_TOGGLE_BUTTON_ID = "photo_video_paginator";
+ private static final String UI_BACK_FRONT_TOGGLE_BUTTON_ID = "camera_toggle_button";
+ private static final String UI_MODE_OPTION_TOGGLE_BUTTON_ID = "mode_options_toggle";
+ private static final String UI_SHUTTER_BUTTON_ID_3X = "photo_video_button";
+ private static final String UI_SHUTTER_BUTTON_ID_2X = "shutter_button";
+ private static final String UI_SETTINGS_BUTTON_ID = "settings_button";
+ private static final String UI_MENU_BUTTON_ID_3X = "menuButton";
+ private static final String UI_MENU_BUTTON_ID_4X = "toybox_menu_button";
+ private static final String UI_SPECIAL_MODE_CLOSE = "closeButton";
+ private static final String UI_HDR_BUTTON_ID_2X = "hdr_plus_toggle_button";
+ private static final String UI_HDR_BUTTON_ID_3X = "hdr_plus_toggle_button";
+ private static final String UI_HDR_BUTTON_ID_4X = "hdr_button";
+ private static final String UI_HDR_AUTO_ID_4X = "hdr_auto";
+ private static final String UI_HDR_ON_ID_4X = "hdr_on";
+ private static final String UI_HDR_OFF_ID_4X = "hdr_off";
+ private static final String UI_SELECTED_OPTION_ID = "selected_option_label";
+ private static final String UI_HFR_TOGGLE_ID_J = "hfr_button";
+ private static final String UI_HFR_TOGGLE_ID_I = "hfr_mode_toggle_button";
+
+ private static final String DESC_HDR_AUTO = "HDR Plus auto";
+ private static final String DESC_HDR_OFF_3X = "HDR Plus off";
+ private static final String DESC_HDR_ON_3X = "HDR Plus on";
+
+ private static final String DESC_HDR_OFF_2X = "HDR off";
+ private static final String DESC_HDR_ON_2X = "HDR on";
+
+ private static final String DESC_HFR_OFF = "Slow motion is off";
+ private static final String DESC_HFR_120_FPS = "Slow motion is set to 120 fps";
+ private static final String DESC_HFR_240_FPS = "Slow motion is set to 240 fps";
+
+ private static final String TEXT_4K_ON = "UHD 4K";
+ private static final String TEXT_HD_1080 = "HD 1080p";
+ private static final String TEXT_HD_720 = "HD 720p";
+ private static final String TEXT_HDR_AUTO = "HDR off";
+ private static final String TEXT_HDR_ON = "HDR+ Auto";
+ private static final String TEXT_HDR_OFF = "HDR on";
+ private static final String TEXT_BACK_VIDEO_RESOLUTION_4X = "Back camera video resolution";
+ private static final String TEXT_BACK_VIDEO_RESOLUTION_3X = "Back camera video";
+
+ public static final int HDR_MODE_AUTO = -1;
+ public static final int HDR_MODE_OFF = 0;
+ public static final int HDR_MODE_ON = 1;
+
+ public static final int VIDEO_4K_MODE_ON = 1;
+ public static final int VIDEO_HD_1080 = 0;
+ public static final int VIDEO_HD_720 = -1;
+
+ public static final int HFR_MODE_OFF = 0;
+ public static final int HFR_MODE_120_FPS = 1;
+ public static final int HFR_MODE_240_FPS = 2;
+
+ private static final long APP_INIT_WAIT = 20000;
+ private static final long DIALOG_TRANSITION_WAIT = 5000;
+ private static final long SHUTTER_WAIT_TIME = 20000;
+ private static final long SWITCH_WAIT_TIME = 5000;
+ private static final long MENU_WAIT_TIME = 5000;
+
+ private boolean mIsVersionH = false;
+ private boolean mIsVersionI = false;
+ private boolean mIsVersionJ = false;
+ private boolean mIsVersionK = false;
+
+ public GoogleCameraHelperImpl(Instrumentation instr) {
+ super(instr);
+
+ try {
+ mIsVersionH = getVersion().startsWith("2.");
+ mIsVersionI = getVersion().startsWith("3.0") || getVersion().startsWith("3.1");
+ mIsVersionJ = getVersion().startsWith("3.2");
+ mIsVersionK = getVersion().startsWith("4");
+ } catch (NameNotFoundException e) {
+ Log.e(LOG_TAG, String.format("Unable to find package by name, %s", getPackage()));
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public String getPackage() {
+ return "com.google.android.GoogleCamera";
+ }
+
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public String getLauncherName() {
+ return "Camera";
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void dismissInitialDialogs() {
+ if (mIsVersionK) {
+ // Dismiss dogfood confidentiality dialog
+ Pattern okText = Pattern.compile("OK, GOT IT", Pattern.CASE_INSENSITIVE);
+ UiObject2 dogfoodMessage = mDevice.wait(
+ Until.findObject(By.text(okText)), APP_INIT_WAIT);
+ if (dogfoodMessage != null) {
+ dogfoodMessage.click();
+ }
+ } else if (mIsVersionI || mIsVersionJ) {
+ // Dismiss dogfood confidentiality dialog
+ Pattern okText = Pattern.compile("OK, GOT IT", Pattern.CASE_INSENSITIVE);
+ UiObject2 dogfoodMessage = mDevice.wait(
+ Until.findObject(By.text(okText)), APP_INIT_WAIT);
+ if (dogfoodMessage != null) {
+ dogfoodMessage.click();
+ }
+ // Swipe left to dismiss 'how to open video message'
+ UiObject2 activityView = mDevice.wait(Until.findObject(
+ By.res(UI_PACKAGE_NAME, "activity_root_view")), DIALOG_TRANSITION_WAIT);
+ if (activityView != null) {
+ activityView.swipe(Direction.LEFT, 1.0f);
+ }
+ // Confirm 'GOT IT' for action above
+ UiObject2 thanks = mDevice.wait(Until.findObject(By.text("GOT IT")),
+ DIALOG_TRANSITION_WAIT);
+ if (thanks != null) {
+ thanks.click();
+ }
+ } else {
+ BySelector confirm = By.res(UI_PACKAGE_NAME, "confirm_button");
+ UiObject2 location = mDevice.wait(Until.findObject(
+ By.copy(confirm).text("NEXT")), APP_INIT_WAIT);
+ if (location != null) {
+ location.click();
+ }
+ // Choose sensor size. It's okay to timeout. These dialog screens might not exist..
+ UiObject2 sensor = mDevice.wait(Until.findObject(
+ By.copy(confirm).text("OK, GOT IT")), DIALOG_TRANSITION_WAIT);
+ if (sensor != null) {
+ sensor.click();
+ }
+ // Dismiss dogfood dialog
+ if (mDevice.wait(Until.hasObject(
+ By.res(UI_PACKAGE_NAME, "internal_release_dialog_title")), 5000)) {
+ mDevice.findObject(By.res(UI_PACKAGE_NAME, "ok_button")).click();
+ }
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void capturePhoto() {
+ if (!isCameraMode()) {
+ throw new IllegalStateException(
+ "GoogleCamera must be in Camera mode to capture photos.");
+ }
+
+ getCameraShutter().click();
+ waitForCameraShutterEnabled();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void captureVideo(long timeInMs) {
+ if (!isVideoMode()) {
+ throw new IllegalStateException("GoogleCamera must be in Video mode to record videos.");
+ }
+
+ if (isRecording()) {
+ return;
+ }
+
+ // Temporary hack #1: Make UI code responsive by shortening the UiAutomator idle timeout.
+ // The pulsing record button broadcasts unnecessary events of TYPE_WINDOW_CONTENT_CHANGED,
+ // but we intend to have a fix and remove this hack with Kenai (GC 3.0).
+ long original = Configurator.getInstance().getWaitForIdleTimeout();
+ Configurator.getInstance().setWaitForIdleTimeout(1000);
+
+ try {
+ getVideoShutter().click();
+ SystemClock.sleep(timeInMs);
+ getVideoShutter().click();
+ waitForVideoShutterEnabled();
+ } finally {
+ Configurator.getInstance().setWaitForIdleTimeout(original);
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void snapshotVideo(long videoTimeInMs, long snapshotStartTimeInMs) {
+ if (!isVideoMode()) {
+ throw new IllegalStateException("GoogleCamera must be in Video mode to record videos.");
+ } else if (videoTimeInMs <= snapshotStartTimeInMs) {
+ throw new IllegalArgumentException(
+ "video recording time length must be larger than snapshot start time");
+ }
+
+ // Temporary hack #2: Make UI code responsive by shortening the UiAutomator idle timeout.
+ // The pulsing record button broadcasts unnecessary events of TYPE_WINDOW_CONTENT_CHANGED,
+ // but we intend to have a fix and remove this hack with Kenai (GC 3.0).
+ long original = Configurator.getInstance().getWaitForIdleTimeout();
+ Configurator.getInstance().setWaitForIdleTimeout(1000);
+
+ if (isRecording()) {
+ return;
+ }
+
+ try {
+ getVideoShutter().click();
+ SystemClock.sleep(snapshotStartTimeInMs);
+
+ boolean snapshot_success = false;
+
+ // Take a snapshot
+ if (mIsVersionJ || mIsVersionK) {
+ UiObject2 snapshotButton = mDevice.findObject(By.res(UI_PACKAGE_NAME, "snapshot_button"));
+ if (snapshotButton != null) {
+ snapshotButton.click();
+ snapshot_success = true;
+ }
+ } else if (mIsVersionI) {
+ // Ivvavik Version of GCA doesn't support snapshot
+ snapshot_success = false;
+ } else {
+ UiObject2 snapshotButton = mDevice.findObject(By.res(UI_PACKAGE_NAME, "recording_time"));
+ if (snapshotButton != null) {
+ snapshotButton.click();
+ snapshot_success = true;
+ }
+ }
+
+ if (!snapshot_success) {
+ getVideoShutter().click();
+ waitForVideoShutterEnabled();
+ throw new UnknownUiException("snapshot button not found!");
+ }
+
+ SystemClock.sleep(videoTimeInMs - snapshotStartTimeInMs);
+ getVideoShutter().click();
+ waitForVideoShutterEnabled();
+ } finally {
+ Configurator.getInstance().setWaitForIdleTimeout(original);
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void goToCameraMode() {
+ if (isCameraMode()) {
+ return;
+ }
+
+ if (mIsVersionI || mIsVersionJ || mIsVersionK) {
+ UiObject2 toggle = getCameraVideoToggleButton();
+ if (toggle != null) {
+ toggle.click();
+ }
+ } else {
+ openMenu();
+ selectMenuItem("Camera");
+ }
+
+ mDevice.waitForIdle();
+ waitForCameraShutterEnabled();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void goToVideoMode() {
+ if (isVideoMode()) {
+ return;
+ }
+
+ if (mIsVersionI || mIsVersionJ || mIsVersionK) {
+ UiObject2 toggle = getCameraVideoToggleButton();
+ if (toggle != null) {
+ toggle.click();
+ }
+ } else {
+ openMenu();
+ selectMenuItem("Video");
+ }
+
+ mDevice.waitForIdle();
+ waitForVideoShutterEnabled();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void goToBackCamera() {
+ if (isBackCamera()) {
+ return;
+ }
+
+ // Close menu if open
+ closeMenu();
+
+ if (mIsVersionI || mIsVersionJ || mIsVersionK) {
+ pressBackFrontToggleButton();
+ } else {
+ // Open mode options if not open.
+ // Note: the mode option button only appear if mode option menu not open
+ UiObject2 modeoptions = getModeOptionsMenuButton();
+ if (modeoptions != null) {
+ modeoptions.click();
+ }
+ pressBackFrontToggleButton();
+ }
+
+ // Wait for ensuring back camera button enabled
+ waitForBackEnabled();
+
+ // Wait for ensuring shutter button enabled
+ waitForCurrentShutterEnabled();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void goToFrontCamera() {
+ if (isFrontCamera()) {
+ return;
+ }
+
+ // Close menu if open
+ closeMenu();
+
+ if (mIsVersionI || mIsVersionJ || mIsVersionK) {
+ pressBackFrontToggleButton();
+ } else {
+ // Open mode options if not open.
+ // Note: the mode option button only appear if mode option menu not open
+ UiObject2 modeoptions = getModeOptionsMenuButton();
+ if (modeoptions != null) {
+ modeoptions.click();
+ }
+ pressBackFrontToggleButton();
+ }
+
+ // Wait for ensuring front camera button enabled
+ waitForFrontEnabled();
+
+ // Wait for ensuring shutter button enabled
+ waitForCurrentShutterEnabled();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public void setHdrMode(int mode) {
+ if (!isCameraMode()) {
+ throw new IllegalStateException("Cannot set HDR unless in camera mode.");
+ }
+
+ if (mIsVersionK) {
+ if (getHdrToggleButton() == null) {
+ if (mode == HDR_MODE_OFF) {
+ return;
+ } else {
+ throw new UnsupportedOperationException(
+ "Cannot set HDR on this device as requested.");
+ }
+ }
+
+ getHdrToggleButton().click();
+ // After clicking the HDR auto button should be visible.
+ mDevice.wait(Until.findObject(By.res(UI_PACKAGE_NAME, UI_HDR_AUTO_ID_4X)),
+ DIALOG_TRANSITION_WAIT);
+
+ switch (mode) {
+ case HDR_MODE_AUTO:
+ mDevice.findObject(By.res(UI_PACKAGE_NAME, UI_HDR_AUTO_ID_4X)).click();
+ break;
+ case HDR_MODE_ON:
+ mDevice.findObject(By.res(UI_PACKAGE_NAME, UI_HDR_ON_ID_4X)).click();
+ break;
+ case HDR_MODE_OFF:
+ mDevice.findObject(By.res(UI_PACKAGE_NAME, UI_HDR_OFF_ID_4X)).click();
+ break;
+ default:
+ throw new UnknownUiException("Failing setting HDR+ mode!");
+ }
+ mDevice.waitForIdle();
+ } else if (mIsVersionI || mIsVersionJ) {
+ if (getHdrToggleButton() == null) {
+ if (mode == HDR_MODE_OFF) {
+ return;
+ } else {
+ throw new UnsupportedOperationException(
+ "Cannot set HDR on this device as requested.");
+ }
+ }
+
+ for (int retries = 0; retries < 3; retries++) {
+ if (!isHdrMode(mode)) {
+ getHdrToggleButton().click();
+ mDevice.waitForIdle();
+ } else {
+ Log.e(LOG_TAG, "Successfully set HDR mode!");
+ mDevice.waitForIdle();
+ return;
+ }
+ }
+ } else {
+ // Open mode options before checking Hdr status
+ openModeOptions2X();
+ if (getHdrToggleButton() == null) {
+ if (mode == HDR_MODE_OFF) {
+ return;
+ } else {
+ throw new UnsupportedOperationException(
+ "Cannot set HDR on this device as requested.");
+ }
+ }
+
+ for (int retries = 0; retries < 3; retries++) {
+ if (!isHdrMode(mode)) {
+ getHdrToggleButton().click();
+ mDevice.waitForIdle();
+ } else {
+ Log.e(LOG_TAG, "Successfully set HDR mode!");
+ mDevice.waitForIdle();
+ return;
+ }
+ }
+ }
+ }
+
+ private boolean isHdrMode(int mode) {
+ if (mIsVersionK) {
+ getHdrToggleButton().click();
+ mDevice.waitForIdle();
+ UiObject2 selectedOption = mDevice.wait(Until.findObject(
+ By.res(UI_PACKAGE_NAME, UI_SELECTED_OPTION_ID)), MENU_WAIT_TIME);
+ String currentHdrModeText = selectedOption.getText();
+ int currentMode = 0;
+ switch (currentHdrModeText) {
+ case TEXT_HDR_AUTO:
+ currentMode = HDR_MODE_AUTO;
+ break;
+ case TEXT_HDR_ON:
+ currentMode = HDR_MODE_ON;
+ break;
+ case TEXT_HDR_OFF:
+ currentMode = HDR_MODE_OFF;
+ break;
+ default:
+ throw new UnknownUiException("Failed to identify the HDR+ settings!");
+ }
+ selectedOption.click();
+ mDevice.wait(Until.findObject(
+ By.res(UI_PACKAGE_NAME, UI_HDR_BUTTON_ID_4X)), MENU_WAIT_TIME);
+ return mode == currentMode;
+ } else if (mIsVersionI || mIsVersionJ) {
+ String modeDesc = getHdrToggleButton().getContentDescription();
+ if (DESC_HDR_AUTO.equals(modeDesc)) {
+ return HDR_MODE_AUTO == mode;
+ } else if (DESC_HDR_OFF_3X.equals(modeDesc)) {
+ return HDR_MODE_OFF == mode;
+ } else if (DESC_HDR_ON_3X.equals(modeDesc)) {
+ return HDR_MODE_ON == mode;
+ } else {
+ throw new UnknownUiException("Unexpected failure.");
+ }
+ } else {
+ // Open mode options before checking Hdr status
+ openModeOptions2X();
+ // Check the HDR mode
+ String modeDesc = getHdrToggleButton().getContentDescription();
+ if (DESC_HDR_OFF_2X.equals(modeDesc)) {
+ return HDR_MODE_OFF == mode;
+ } else if (DESC_HDR_ON_2X.equals(modeDesc)) {
+ return HDR_MODE_ON == mode;
+ } else {
+ throw new UnknownUiException("Unexpected failure.");
+ }
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public void set4KMode(int mode) {
+ // If the menu is not open, open it
+ if (!isMenuOpen()) {
+ openMenu();
+ }
+
+ if (mIsVersionI || mIsVersionJ || mIsVersionK) {
+ // Select Menu Item "Settings"
+ selectMenuItem("Settings");
+ } else {
+ // Select Menu Item "Settings"
+ selectSetting2X();
+ }
+
+ if (mIsVersionI || mIsVersionJ) {
+ // Select Item "Resolution & Quality"
+ selectSettingItem("Resolution & quality");
+ }
+
+ // Select Item "Back camera video", which is the only mode supports 4k
+ selectVideoResolution(mode);
+
+ if (mIsVersionI || mIsVersionJ) {
+ // Quit Menu "Resolution & Quality"
+ closeSettingItem();
+ }
+
+ // Close Main Menu
+ closeMenuItem();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public void setHFRMode(int mode) {
+ if (!isVideoMode()) {
+ throw new IllegalStateException("Must be in video mode to set HFR mode.");
+ }
+
+ // Haleakala doesn't support slow motion, so throw exception
+ if (mIsVersionH) {
+ throw new UnsupportedOperationException(
+ "HFR not supported on this version of Google Camera.");
+ } else if (mIsVersionI) {
+ waitForHFRToggleEnabled();
+ for (int retries = 0; retries < 3; retries++) {
+ if (!isHfrMode(mode)) {
+ getHfrToggleButton().click();
+ mDevice.waitForIdle();
+ } else {
+ Log.e(LOG_TAG, "Successfully set HFR mode!");
+ mDevice.waitForIdle();
+ waitForVideoShutterEnabled();
+ return;
+ }
+ }
+ //If none of the 3 options match expected option, throw an exception
+ if (mode == HFR_MODE_OFF) {
+ throw new UnknownUiException("Failed to turn off the HFR mode");
+ } else {
+ throw new UnknownUiException(String.format("Failed to select HFR mode to FPS %d",
+ (int) Math.floor(mode * 120)));
+ }
+ } else if (mIsVersionJ || mIsVersionK) {
+ String uiMenuButton = (mIsVersionK)? UI_MENU_BUTTON_ID_4X:UI_MENU_BUTTON_ID_3X;
+ if (mode == HFR_MODE_OFF) {
+ // This close button ui only appeared in hfr mode
+ UiObject2 hfrmodeclose = mDevice.findObject(By.res(UI_PACKAGE_NAME,
+ UI_SPECIAL_MODE_CLOSE));
+ if (hfrmodeclose != null) {
+ hfrmodeclose.click();
+ mDevice.wait(Until.hasObject(By.res(UI_PACKAGE_NAME, uiMenuButton)),
+ MENU_WAIT_TIME);
+ } else {
+ throw new UnknownUiException(
+ "Fail to find hfr mode close button when trying to turn off HFR mode");
+ }
+ return;
+ }
+
+ // When not in HFR interface, select menu to open HFR interface
+ if (mDevice.hasObject(By.res(UI_PACKAGE_NAME, UI_SPECIAL_MODE_CLOSE))
+ && !isVideoMode()) {
+ UiObject2 specialmodeclose = mDevice.findObject(By.res(UI_PACKAGE_NAME,
+ UI_SPECIAL_MODE_CLOSE));
+ if (specialmodeclose != null) {
+ specialmodeclose.click();
+ mDevice.wait(Until.hasObject(By.res(UI_PACKAGE_NAME, uiMenuButton)),
+ MENU_WAIT_TIME);
+ } else {
+ throw new UnknownUiException(
+ "Fail to close other special mode before setting hfr mode");
+ }
+ }
+
+ if (!mDevice.hasObject(By.res(UI_PACKAGE_NAME, UI_SPECIAL_MODE_CLOSE))) {
+ // If the menu is not open, open it
+ if (!isMenuOpen()) {
+ openMenu();
+ }
+ // Select Item "Slow Motion"
+ selectSettingItem("Slow Motion");
+ // Change Slow Motion mode to 120FPS or 240FPS
+ }
+
+ mDevice.waitForIdle();
+ // Detect if hfr toggle exists in the interface
+ if (!mDevice.hasObject(By.res(UI_PACKAGE_NAME, UI_HFR_TOGGLE_ID_J))) {
+ if (mode == HFR_MODE_240_FPS) {
+ throw new UnknownUiException(
+ "The 240 fps HFR mode is not supported on the device.");
+ }
+ return;
+ }
+
+ for (int retries = 0; retries < 2; retries++) {
+ if (!isHfrMode(mode)) {
+ getHfrToggleButton().click();
+ mDevice.waitForIdle();
+ } else {
+ Log.e(LOG_TAG, "Successfully set HFR mode!");
+ mDevice.waitForIdle();
+ waitForVideoShutterEnabled();
+ return;
+ }
+ }
+ //If neither of the 2 options match expected option, throw an exception
+ throw new UnknownUiException(String.format("Failed to select HFR mode to FPS %d",
+ (int) Math.floor(mode * 120)));
+ } else {
+ throw new UnknownUiException("The Google Camera version is not supported.");
+ }
+ }
+
+ private boolean isHfrMode(int mode) {
+ if (mIsVersionI) {
+ String modeDesc = getHfrToggleButton().getContentDescription();
+ if (DESC_HFR_120_FPS.equals(modeDesc)) {
+ return HFR_MODE_120_FPS == mode;
+ } else if (DESC_HFR_240_FPS.equals(modeDesc)) {
+ return HFR_MODE_240_FPS == mode;
+ } else if (DESC_HFR_OFF.equals(modeDesc)) {
+ return HFR_MODE_OFF == mode;
+ } else {
+ throw new UnknownUiException("Fail to identify HFR toggle description.");
+ }
+ } else if (mIsVersionJ || mIsVersionK) {
+ if (getHfrToggleButton() == null) {
+ return HFR_MODE_OFF == mode;
+ }
+ String modeDesc = getHfrToggleButton().getContentDescription();
+ if (DESC_HFR_120_FPS.equals(modeDesc)) {
+ return HFR_MODE_120_FPS == mode;
+ } else if (DESC_HFR_240_FPS.equals(modeDesc)) {
+ return HFR_MODE_240_FPS == mode;
+ } else {
+ throw new UnknownUiException("Fail to identify HFR toggle description.");
+ }
+ }
+ return HFR_MODE_OFF == mode;
+ }
+
+ private void openModeOptions2X() {
+ // If the mode option is already open, return as it is
+ if (mDevice.hasObject(By.res(UI_PACKAGE_NAME, "mode_options_buttons"))) {
+ return;
+ }
+ // Before openning the mode option, close the menu if the menu is open
+ closeMenu();
+ waitForVideoShutterEnabled();
+ // Open the mode options to check HDR mode
+ UiObject2 modeoptions = getModeOptionsMenuButton();
+ if (modeoptions != null) {
+ modeoptions.click();
+ // If succeeded, the hdr toggle button should be visible.
+ mDevice.wait(Until.hasObject(By.res(UI_PACKAGE_NAME, "hdr_plus_toggle_button")),
+ DIALOG_TRANSITION_WAIT);
+ } else {
+ throw new UnknownUiException(
+ "Fail to find modeoption button when trying to check HDR mode");
+ }
+ }
+
+ private void openMenu() {
+ if (mIsVersionI || mIsVersionJ || mIsVersionK) {
+ String uiMenuButton = (mIsVersionK)? UI_MENU_BUTTON_ID_4X:UI_MENU_BUTTON_ID_3X;
+ UiObject2 menu = mDevice.findObject(By.res(UI_PACKAGE_NAME, uiMenuButton));
+ menu.click();
+ } else {
+ UiObject2 activityView = mDevice.wait(Until.findObject(
+ By.res(UI_PACKAGE_NAME, UI_ACTIVITY_VIEW_ID)), MENU_WAIT_TIME);
+ activityView.swipe(Direction.RIGHT, 1.0f);
+ }
+
+ mDevice.wait(Until.hasObject(By.text("Photo Sphere")), MENU_WAIT_TIME);
+
+ mDevice.waitForIdle();
+ }
+
+ private void selectMenuItem(String mode) {
+ UiObject2 menuItem = mDevice.findObject(By.text(mode));
+ if (menuItem != null) {
+ menuItem.click();
+ } else {
+ throw new UnknownUiException(
+ String.format("Menu item button was not enabled with %d seconds",
+ (int)Math.floor(MENU_WAIT_TIME / 1000)));
+ }
+ mDevice.wait(Until.gone(By.text("Photo Sphere")), MENU_WAIT_TIME);
+
+ mDevice.waitForIdle();
+ }
+
+ private void closeMenuItem() {
+ UiObject2 navUp = mDevice.findObject(By.desc("Navigate up"));
+ if (navUp != null) {
+ navUp.click();
+ } else {
+ throw new UnknownUiException(String.format(
+ "Navigation up button was not enabled with %d seconds",
+ (int)Math.floor(MENU_WAIT_TIME / 1000)));
+ }
+ mDevice.wait(Until.gone(By.text("Help & feedback")), MENU_WAIT_TIME);
+
+ mDevice.waitForIdle();
+ }
+
+ private void selectSettingItem(String mode) {
+ UiObject2 settingItem = mDevice.findObject(By.text(mode));
+ if (settingItem != null) {
+ settingItem.click();
+ } else {
+ throw new UnknownUiException(
+ String.format("Setting item button was not enabled with %d seconds",
+ (int)Math.floor(MENU_WAIT_TIME / 1000)));
+ }
+ mDevice.wait(Until.gone(By.text("Help & feedback")), MENU_WAIT_TIME);
+
+ mDevice.waitForIdle();
+ }
+
+ private void selectSetting2X() {
+ UiObject2 settingItem = mDevice.findObject(By.desc("Settings"));
+ if (settingItem != null) {
+ settingItem.click();
+ } else {
+ throw new UnknownUiException(
+ String.format("Setting item button was not enabled with %d seconds",
+ (int)Math.floor(MENU_WAIT_TIME / 1000)));
+ }
+ mDevice.wait(Until.gone(By.text("Help & feedback")), MENU_WAIT_TIME);
+
+ mDevice.waitForIdle();
+ }
+
+ private void closeSettingItem() {
+ UiObject2 navUp = mDevice.findObject(By.desc("Navigate up"));
+ if (navUp != null) {
+ navUp.click();
+ } else {
+ throw new UnknownUiException(
+ String.format("Navigation up button was not enabled with %d seconds",
+ (int)Math.floor(MENU_WAIT_TIME / 1000)));
+ }
+ mDevice.wait(Until.findObject(By.text("Help & feedback")), MENU_WAIT_TIME);
+
+ mDevice.waitForIdle();
+ }
+
+ private void selectVideoResolution(int mode) {
+ String textBackVideoResolution =
+ (mIsVersionK)? TEXT_BACK_VIDEO_RESOLUTION_4X:TEXT_BACK_VIDEO_RESOLUTION_3X;
+ UiObject2 backCamera = mDevice.findObject(By.text(textBackVideoResolution));
+ if (backCamera != null) {
+ backCamera.click();
+ } else {
+ throw new UnknownUiException(
+ String.format("Back camera button was not enabled with %d seconds",
+ (int)Math.floor(MENU_WAIT_TIME / 1000)));
+ }
+ mDevice.wait(Until.findObject(By.text("CANCEL")), MENU_WAIT_TIME);
+ mDevice.waitForIdle();
+
+ if (mode == VIDEO_4K_MODE_ON) {
+ mDevice.wait(Until.findObject(By.text(TEXT_4K_ON)), MENU_WAIT_TIME).click();
+ } else if (mode == VIDEO_HD_1080) {
+ mDevice.wait(Until.findObject(By.text(TEXT_HD_1080)), MENU_WAIT_TIME).click();
+ } else if (mode == VIDEO_HD_720){
+ mDevice.wait(Until.findObject(By.text(TEXT_HD_720)), MENU_WAIT_TIME).click();
+ } else {
+ throw new UnknownUiException("Failed to set video resolution");
+ }
+
+ mDevice.wait(Until.gone(By.text("CANCEL")), MENU_WAIT_TIME);
+
+ mDevice.waitForIdle();
+ }
+
+ private void closeMenu() {
+ // Should only call this function when menu is open, do nothing if menu is not open
+ if (!isMenuOpen()) {
+ return;
+ }
+
+ if (mIsVersionI || mIsVersionJ || mIsVersionK) {
+ // Click menu button to close menu (this is NOT for taking pictures)
+ String uiMenuButton = (mIsVersionK)? UI_MENU_BUTTON_ID_4X:UI_MENU_BUTTON_ID_3X;
+ UiObject2 backButton = mDevice.findObject(By.res(UI_PACKAGE_NAME, uiMenuButton));
+ if (backButton != null) {
+ backButton.click();
+ }
+ } else {
+ // Click shutter button to close menu (this is NOT for taking pictures)
+ UiObject2 shutter = mDevice.findObject(By.res(UI_PACKAGE_NAME, UI_SHUTTER_BUTTON_ID_2X));
+ if (shutter != null) {
+ shutter.click();
+ }
+ }
+ }
+
+ private boolean isCameraMode() {
+ if (mIsVersionI || mIsVersionJ || mIsVersionK) {
+ return (mDevice.hasObject(By.desc(UI_SHUTTER_DESC_CAM_3X)));
+ } else {
+ // TODO: identify a Haleakala UiObject2 unique Camera mode
+ return !isVideoMode();
+ }
+ }
+
+ private boolean isVideoMode() {
+ if (mIsVersionI || mIsVersionJ || mIsVersionK) {
+ return (mDevice.hasObject(By.desc(UI_SHUTTER_DESC_VID_3X)));
+ } else {
+ return (mDevice.hasObject(By.res(UI_PACKAGE_NAME, "recording_time_rect")));
+ }
+ }
+
+ private boolean isRecording() {
+ return mDevice.hasObject(By.res(UI_PACKAGE_NAME, UI_RECORDING_TIME_ID));
+ }
+
+ private boolean isFrontCamera() {
+ // Close menu if open
+ closeMenu();
+
+ if (mIsVersionJ || mIsVersionK) {
+ return (mDevice.hasObject(By.desc("Switch to back camera")));
+ } else if (mIsVersionI) {
+ return (mDevice.hasObject(By.desc("Front camera")));
+ } else {
+ // Open mode options if not open
+ UiObject2 modeoptions = getModeOptionsMenuButton();
+ if (modeoptions != null) {
+ modeoptions.click();
+ }
+ return (mDevice.hasObject(By.desc("Front camera")));
+ }
+ }
+
+ private boolean isBackCamera() {
+ // Close menu if open
+ closeMenu();
+
+ if (mIsVersionJ || mIsVersionK) {
+ return (mDevice.hasObject(By.desc("Switch to front camera")));
+ } else if (mIsVersionI) {
+ return (mDevice.hasObject(By.desc("Back camera")));
+ } else {
+ // Open mode options if not open
+ UiObject2 modeoptions = getModeOptionsMenuButton();
+ if (modeoptions != null) {
+ modeoptions.click();
+ }
+ return (mDevice.hasObject(By.desc("Back camera")));
+ }
+ }
+
+ private boolean isMenuOpen() {
+ if (mIsVersionI || mIsVersionJ || mIsVersionK) {
+ if (mDevice.hasObject(By.desc("Open settings"))) {
+ return true;
+ }
+ } else {
+ if (mDevice.hasObject(By.res(UI_PACKAGE_NAME, UI_SETTINGS_BUTTON_ID))) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private void pressBackFrontToggleButton() {
+ UiObject2 toggle = getBackFrontToggleButton();
+ if (toggle != null) {
+ toggle.click();
+ } else {
+ throw new UnknownUiException("Failed to detect a back-front toggle button");
+ }
+ }
+
+ private UiObject2 getCameraVideoToggleButton() {
+ return mDevice.findObject(By.res(UI_PACKAGE_NAME, UI_TOGGLE_BUTTON_ID));
+ }
+
+ private UiObject2 getBackFrontToggleButton() {
+ return mDevice.findObject(By.res(UI_PACKAGE_NAME, UI_BACK_FRONT_TOGGLE_BUTTON_ID));
+ }
+
+ private UiObject2 getHdrToggleButton() {
+ if (mIsVersionK) {
+ return mDevice.findObject(By.res(UI_PACKAGE_NAME, UI_HDR_BUTTON_ID_4X));
+ } else if (mIsVersionI || mIsVersionJ) {
+ return mDevice.findObject(By.res(UI_PACKAGE_NAME, UI_HDR_BUTTON_ID_3X));
+ } else {
+ return mDevice.findObject(By.res(UI_PACKAGE_NAME, UI_HDR_BUTTON_ID_2X));
+ }
+ }
+
+ private UiObject2 getHfrToggleButton() {
+ if (mIsVersionI) {
+ return mDevice.findObject(By.res(UI_PACKAGE_NAME, UI_HFR_TOGGLE_ID_I));
+ } else if (mIsVersionJ || mIsVersionK) {
+ return mDevice.findObject(By.res(UI_PACKAGE_NAME, UI_HFR_TOGGLE_ID_J));
+ } else {
+ throw new UnsupportedOperationException(
+ "HFR not supported on this version of Google Camera.");
+ }
+ }
+
+ private UiObject2 getModeOptionsMenuButton() {
+ return mDevice.findObject(By.res(UI_PACKAGE_NAME, UI_MODE_OPTION_TOGGLE_BUTTON_ID));
+ }
+
+ private UiObject2 getCameraShutter() {
+ if (mIsVersionI || mIsVersionJ || mIsVersionK) {
+ return mDevice.findObject(By.desc(UI_SHUTTER_DESC_CAM_3X).enabled(true));
+ } else {
+ return mDevice.findObject(By.desc(UI_SHUTTER_DESC_CAM_2X).enabled(true));
+ }
+ }
+
+ private UiObject2 getVideoShutter() {
+ if (mIsVersionI || mIsVersionJ || mIsVersionK) {
+ return mDevice.findObject(By.desc(UI_SHUTTER_DESC_VID_3X).enabled(true));
+ } else {
+ return mDevice.findObject(By.desc(UI_SHUTTER_DESC_VID_2X).enabled(true));
+ }
+ }
+
+ private UiObject2 getThumbnailAlbumButton() {
+ return mDevice.findObject(By.res(UI_PACKAGE_NAME, UI_THUMBNAIL_ALBUM_BUTTON_ID));
+ }
+
+ private UiObject2 getAlbumFilmstripView() {
+ return mDevice.findObject(By.res(UI_PACKAGE_NAME, UI_ALBUM_FILMSTRIP_VIEW_ID));
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public void waitForCameraShutterEnabled() {
+ boolean uiSuccess = false;
+
+ if (mIsVersionI || mIsVersionJ || mIsVersionK) {
+ uiSuccess = mDevice.wait(Until.hasObject(
+ By.desc(UI_SHUTTER_DESC_CAM_3X).enabled(true)), SHUTTER_WAIT_TIME);
+ } else {
+ uiSuccess = mDevice.wait(Until.hasObject(
+ By.desc(UI_SHUTTER_DESC_CAM_2X).enabled(true)), SHUTTER_WAIT_TIME);
+ }
+
+ if (!uiSuccess) {
+ throw new UnknownUiException(
+ String.format("Camera shutter was not enabled with %d seconds",
+ (int)Math.floor(SHUTTER_WAIT_TIME / 1000)));
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public void waitForVideoShutterEnabled() {
+ boolean uiSuccess = false;
+
+ if (mIsVersionI || mIsVersionJ || mIsVersionK) {
+ uiSuccess = mDevice.wait(Until.hasObject(
+ By.desc(UI_SHUTTER_DESC_VID_3X).enabled(true)), SHUTTER_WAIT_TIME);
+ } else {
+ uiSuccess = mDevice.wait(Until.hasObject(
+ By.desc(UI_SHUTTER_DESC_VID_2X).enabled(true)), SHUTTER_WAIT_TIME);
+ }
+
+ if (!uiSuccess) {
+ throw new UnknownUiException(
+ String.format("Video shutter was not enabled with %d seconds",
+ (int)Math.floor(SHUTTER_WAIT_TIME / 1000)));
+ }
+ }
+
+ private void waitForCurrentShutterEnabled() {
+ // This function is called to wait for shutter button enabled in either camera or video mode
+ if (mIsVersionI || mIsVersionJ || mIsVersionK) {
+ mDevice.wait(Until.hasObject(By.res(UI_PACKAGE_NAME, UI_SHUTTER_BUTTON_ID_3X).enabled(true)),
+ SHUTTER_WAIT_TIME);
+ } else {
+ mDevice.wait(Until.hasObject(By.res(UI_PACKAGE_NAME, UI_SHUTTER_BUTTON_ID_2X).enabled(true)),
+ SHUTTER_WAIT_TIME);
+ }
+ }
+
+ private void waitForBackEnabled() {
+ if (mIsVersionI || mIsVersionJ || mIsVersionK) {
+ mDevice.wait(Until.hasObject(By.desc("Switch to front camera").enabled(true)),
+ SWITCH_WAIT_TIME);
+ } else {
+ mDevice.wait(Until.hasObject(By.desc("Back camera").enabled(true)),
+ SWITCH_WAIT_TIME);
+ }
+ }
+
+ private void waitForFrontEnabled() {
+ if (mIsVersionI || mIsVersionJ || mIsVersionK) {
+ mDevice.wait(Until.hasObject(By.desc("Switch to back camera").enabled(true)),
+ SWITCH_WAIT_TIME);
+ } else {
+ mDevice.wait(Until.hasObject(By.desc("Front camera").enabled(true)),
+ SWITCH_WAIT_TIME);
+ }
+ }
+
+ private void waitForHFRToggleEnabled() {
+ if (mIsVersionJ || mIsVersionK) {
+ mDevice.wait(Until.hasObject(By.res(UI_PACKAGE_NAME, UI_HFR_TOGGLE_ID_J).enabled(true)),
+ SWITCH_WAIT_TIME);
+ } else if (mIsVersionI) {
+ mDevice.wait(Until.hasObject(By.res(UI_PACKAGE_NAME, UI_HFR_TOGGLE_ID_I).enabled(true)),
+ SWITCH_WAIT_TIME);
+ } else {
+ throw new UnknownUiException("HFR is not supported on this version of Google Camera");
+ }
+ }
+
+ private void waitForAppInit() {
+ boolean initalized = false;
+ if (mIsVersionI || mIsVersionJ || mIsVersionK) {
+ String uiMenuButton = (mIsVersionK)? UI_MENU_BUTTON_ID_4X:UI_MENU_BUTTON_ID_3X;
+ initalized = mDevice.wait(Until.hasObject(By.res(UI_PACKAGE_NAME, uiMenuButton)),
+ APP_INIT_WAIT);
+ } else {
+ initalized = mDevice.wait(Until.hasObject(By.res(UI_PACKAGE_NAME, UI_MODE_OPTION_TOGGLE_BUTTON_ID)),
+ APP_INIT_WAIT);
+ }
+
+ waitForCurrentShutterEnabled();
+
+ mDevice.waitForIdle();
+
+ if (initalized) {
+ Log.e(LOG_TAG, "Successfully initialized.");
+ } else {
+ Log.e(LOG_TAG, "Failed to find initialization indicator.");
+ }
+ }
+
+ /**
+ * TODO: Temporary. Create long-term solution for registering watchers.
+ */
+ public void registerCrashWatcher() {
+ final UiDevice fDevice = mDevice;
+
+ mDevice.registerWatcher("GoogleCamera-crash-watcher", new UiWatcher() {
+ @Override
+ public boolean checkForCondition() {
+ Pattern dismissWords =
+ Pattern.compile("DISMISS", Pattern.CASE_INSENSITIVE);
+ UiObject2 buttonDismiss = fDevice.findObject(By.text(dismissWords).enabled(true));
+ if (buttonDismiss != null) {
+ buttonDismiss.click();
+ throw new UnknownUiException("Camera crash dialog encountered. Failing test.");
+ }
+
+ return false;
+ }
+ });
+ }
+
+ /**
+ * TODO: Temporary. Create long-term solution for registering watchers.
+ */
+ public void unregisterCrashWatcher() {
+ mDevice.removeWatcher("GoogleCamera-crash-watcher");
+ }
+
+ /**
+ * TODO: Should only be temporary
+ * {@inheritDoc}
+ */
+ public String openWithShutterTimeString() {
+ String pkg = getPackage();
+ String id = getLauncherName();
+
+ long launchStart = ILauncherStrategy.LAUNCH_FAILED_TIMESTAMP;
+ if (!mDevice.hasObject(By.pkg(pkg).depth(0))) {
+ launchStart = mLauncherStrategy.launch(id, pkg);
+ }
+
+ if (launchStart == ILauncherStrategy.LAUNCH_FAILED_TIMESTAMP) {
+ throw new UnknownUiException("Failed to launch GoogleCamera.");
+ }
+
+ waitForAppInit();
+ waitForCurrentShutterEnabled();
+ long launchDuration = SystemClock.uptimeMillis() - launchStart;
+
+ Date dateNow = new Date();
+ DateFormat dateFormat = new SimpleDateFormat("MM-dd HH:mm:ss.SSS");
+ String dateString = dateFormat.format(dateNow);
+
+ if (isCameraMode()) {
+ return String.format("%s %s %d\n", dateString, "camera", launchDuration);
+ } else if (isVideoMode()) {
+ return String.format("%s %s %d\n", dateString, "video", launchDuration);
+ } else {
+ return String.format("%s %s %d\n", dateString, "wtf", launchDuration);
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public void goToAlbum() {
+ UiObject2 thumbnailAlbumButton = getThumbnailAlbumButton();
+ if (thumbnailAlbumButton == null) {
+ throw new UnknownUiException("Could not find thumbnail album button");
+ }
+
+ thumbnailAlbumButton.click();
+ if (!mDevice.wait(Until.hasObject(
+ By.res(UI_PACKAGE_NAME, UI_ALBUM_FILMSTRIP_VIEW_ID)), DIALOG_TRANSITION_WAIT)) {
+ throw new UnknownUiException("Could not find album filmstrip");
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public void scrollAlbum(Direction direction) {
+ if (!(Direction.LEFT.equals(direction) || Direction.RIGHT.equals(direction))) {
+ throw new IllegalArgumentException("direction must be LEFT or RIGHT");
+ }
+
+ UiObject2 albumFilmstripView = getAlbumFilmstripView();
+ if (albumFilmstripView == null) {
+ throw new UnknownUiException("Could not find album filmstrip view");
+ }
+
+ albumFilmstripView.scroll(direction, 5.0f);
+ mDevice.waitForIdle();
+ }
+}
diff --git a/libraries/google-docs-app-helper/Android.mk b/libraries/google-docs-app-helper/Android.mk
new file mode 100644
index 0000000..44ee3c3
--- /dev/null
+++ b/libraries/google-docs-app-helper/Android.mk
@@ -0,0 +1,23 @@
+#
+# Copyright (C) 2016 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT 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 := google-docs-app-helper
+LOCAL_JAVA_LIBRARIES := ub-uiautomator base-app-helpers
+LOCAL_SRC_FILES := $(call all-java-files-under, src)
+
+include $(BUILD_STATIC_JAVA_LIBRARY)
diff --git a/libraries/google-docs-app-helper/src/android/platform/test/helpers/GoogleDocsHelperImpl.java b/libraries/google-docs-app-helper/src/android/platform/test/helpers/GoogleDocsHelperImpl.java
new file mode 100644
index 0000000..d9a31f8
--- /dev/null
+++ b/libraries/google-docs-app-helper/src/android/platform/test/helpers/GoogleDocsHelperImpl.java
@@ -0,0 +1,178 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.platform.test.helpers;
+
+import android.app.Instrumentation;
+import android.os.SystemClock;
+import android.support.test.uiautomator.By;
+import android.support.test.uiautomator.Direction;
+import android.support.test.uiautomator.Until;
+import android.support.test.uiautomator.UiObject2;
+
+import junit.framework.Assert;
+
+import java.lang.IllegalArgumentException;
+import java.lang.IllegalStateException;
+
+/**
+ * UI test helper for Google Docs (package: com.google.android.apps.docs.editors.docs).
+ * Implementation based on app version: 1.6.152
+ */
+
+public class GoogleDocsHelperImpl extends AbstractGoogleDocsHelper {
+
+ private static final String LOG_TAG = GoogleDocsHelperImpl.class.getSimpleName();
+
+ private static final String UI_PACKAGE_NAME = "com.google.android.apps.docs.editors.docs";
+ private static final String UI_DOCS_LIST_TITLE = "title";
+ private static final String UI_DOCS_LIST_VIEW = "doc_list_view";
+ private static final String UI_EDITOR_VIEW = "kix_editor_view";
+ private static final String UI_TOOLBAR = "toolbar";
+ private static final String UI_TEXT_DOCS = "Docs";
+ private static final String UI_TEXT_SKIP = "SKIP";
+
+ private static final long HACKY_DELAY = 1000; // 1 sec
+ private static final long LOAD_DOCUMENT_TIMEOUT = 60000; // 60 secs
+ private static final int BACK_TO_RECENT_DOCS_MAX_RETRY = 5;
+ private static final int SEARCHING_DOC_MAX_SCROLL_DOWN = 10;
+
+ public GoogleDocsHelperImpl(Instrumentation instr) {
+ super(instr);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public String getPackage() {
+ return "com.google.android.apps.docs.editors.docs";
+ }
+
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public String getLauncherName() {
+ return "Docs";
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void dismissInitialDialogs() {
+ UiObject2 skipButton = getSkipButton();
+ if (skipButton != null) {
+ skipButton.click();
+ mDevice.waitForIdle();
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void goToRecentDocsTab() {
+ for (int retryCnt = 0; retryCnt < BACK_TO_RECENT_DOCS_MAX_RETRY; retryCnt++) {
+ if (isOnRecentDocsTab()) {
+ return;
+ }
+ mDevice.pressBack();
+
+ // TODO Hacky workaround
+ // Bug: 28675538
+ // mDevice.waitForIdle() is insufficient when a short (unscrollable)
+ // document is scrolled by scrollDownDocument() before goToRecentDocsTab()
+ // is called. isOnRecentDocsTab() fails to recognize the Recent Docs tab
+ // even if the tab is indeed shown.
+ SystemClock.sleep(HACKY_DELAY);
+ }
+ Assert.assertTrue("Failed to go to Recent Docs Tab", isOnRecentDocsTab());
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void openDoc(String title) {
+ if (!isOnRecentDocsTab()) {
+ throw new IllegalStateException("Not on the Recent Docs tab");
+ }
+ UiObject2 recentDocsList = getRecentDocsList();
+
+ // TODO: Hacky workaround
+ // Bug: 28675621
+ // while (recentDocsList.scroll(Direction.UP, 1.0f));
+ // The above while loop doesn't work as scroll doesn't return true
+ // while there's more to scroll.
+ recentDocsList.fling(Direction.UP);
+
+ for (int cnt = 0; cnt < SEARCHING_DOC_MAX_SCROLL_DOWN; cnt++) {
+ UiObject2 documentTitle = recentDocsList.findObject(
+ By.res(UI_PACKAGE_NAME, UI_DOCS_LIST_TITLE).text(title));
+ if (documentTitle != null) {
+ // document is found, click to download
+ documentTitle.click();
+ boolean editorLoaded = mDevice.wait(
+ Until.hasObject(By.res(UI_PACKAGE_NAME, UI_EDITOR_VIEW)),
+ LOAD_DOCUMENT_TIMEOUT);
+ Assert.assertTrue(String.format("Failed to finish downloading %s", title),
+ editorLoaded);
+ return;
+ }
+ recentDocsList.scroll(Direction.DOWN, 0.5f);
+ }
+ Assert.fail(String.format("Can't find the document: %s", title));
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void scrollDownDocument() {
+ UiObject2 docsEditorPage = getDocsEditorPage();
+ if (docsEditorPage == null) {
+ throw new IllegalStateException("Not on a document page");
+ }
+ docsEditorPage.scroll(Direction.DOWN, 1.0f);
+ }
+
+ private boolean isOnRecentDocsTab() {
+ UiObject2 toolbar = getToolbar();
+ if (toolbar == null) {
+ return false;
+ }
+ return toolbar.hasObject(By.text(UI_TEXT_DOCS));
+ }
+
+ private UiObject2 getToolbar() {
+ return mDevice.findObject(By.res(UI_PACKAGE_NAME, UI_TOOLBAR));
+ }
+
+ private UiObject2 getRecentDocsList() {
+ return mDevice.findObject(By.res(UI_PACKAGE_NAME, UI_DOCS_LIST_VIEW));
+ }
+
+ private UiObject2 getDocsEditorPage() {
+ return mDevice.findObject(By.res(UI_PACKAGE_NAME, UI_EDITOR_VIEW));
+ }
+
+ private UiObject2 getSkipButton() {
+ return mDevice.findObject(By.text(UI_TEXT_SKIP));
+ }
+}
diff --git a/libraries/google-keyboard-app-helper/Android.mk b/libraries/google-keyboard-app-helper/Android.mk
new file mode 100644
index 0000000..26d952c
--- /dev/null
+++ b/libraries/google-keyboard-app-helper/Android.mk
@@ -0,0 +1,23 @@
+#
+# Copyright (C) 2016 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT 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 := google-keyboard-app-helper
+LOCAL_JAVA_LIBRARIES := ub-uiautomator base-app-helpers
+LOCAL_SRC_FILES := $(call all-java-files-under, src)
+
+include $(BUILD_STATIC_JAVA_LIBRARY)
diff --git a/libraries/google-keyboard-app-helper/src/android/platform/test/helpers/GoogleKeyboardHelperImpl.java b/libraries/google-keyboard-app-helper/src/android/platform/test/helpers/GoogleKeyboardHelperImpl.java
new file mode 100644
index 0000000..cb43c99
--- /dev/null
+++ b/libraries/google-keyboard-app-helper/src/android/platform/test/helpers/GoogleKeyboardHelperImpl.java
@@ -0,0 +1,335 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.platform.test.helpers;
+
+import android.app.Instrumentation;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.os.SystemClock;
+import android.support.test.uiautomator.By;
+import android.support.test.uiautomator.BySelector;
+import android.support.test.uiautomator.Until;
+import android.support.test.uiautomator.UiDevice;
+import android.support.test.uiautomator.UiObject2;
+import android.util.Log;
+
+import junit.framework.Assert;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.regex.Pattern;
+import java.util.Set;
+
+public class GoogleKeyboardHelperImpl extends AbstractGoogleKeyboardHelper {
+ private static final String TAG = GoogleKeyboardHelperImpl.class.getCanonicalName();
+
+ private static final Map<Character, String> SPECIAL_KEY_CONTENT_DESCRIPTIONS;
+
+ private static final Set<Character> ALWAYS_VISIBLE_CHARACTERS;
+ private static final Set<Character> KEYBOARD_NUMBER_SCREEN_SYMBOLS;
+ private static final Set<Character> KEYBOARD_OTHER_SYMBOLS;
+
+ private static final String UI_ANDROID_VIEW_CLASS = "android.view.View";
+ private static final String UI_DECLINE_BUTTON_ID = "decline_button";
+ private static final String UI_KEYBOARD_KEY_CLASS = "com.android.inputmethod.keyboard.Key";
+ private static final String UI_KEYBOARD_LETTER_KEY_DESC = "Letters";
+ private static final String UI_KEYBOARD_NUMBER_KEY_DESC = "Symbols";
+ private static final String UI_KEYBOARD_SHIFT_KEY_DESC = "Shift";
+ private static final String UI_KEYBOARD_SYMBOL_KEY_DESC = "More symbols";
+ private static final String UI_KEYBOARD_VIEW_ID = "keyboard_view";
+ private static final String UI_RESOURCE_NAME = "com.android.inputmethod.latin";
+ private static final String UI_PACKAGE_NAME = "com.google.android.inputmethod.latin";
+ private static final String UI_QUICK_SEARCH_BOX_PACKAGE_NAME =
+ "com.google.android.googlequicksearchbox";
+
+ private static final char KEYBOARD_TEST_LOWER_CASE_LETTER = 'a';
+ private static final char KEYBOARD_TEST_NUMBER = '1';
+ private static final char KEYBOARD_TEST_SYMBOL = '~';
+ private static final char KEYBOARD_TEST_UPPER_CASE_LETTER = 'A';
+
+ private static final long KEYBOARD_MODE_CHANGE_TIMEOUT = 5000; // 5 secs
+
+ static {
+ Map<Character, String> specialKeyContentDescriptions = new HashMap<>();
+ specialKeyContentDescriptions.put(' ', "Space");
+ specialKeyContentDescriptions.put('I', "Capital I");
+ specialKeyContentDescriptions.put('Δ', "Increment");
+ specialKeyContentDescriptions.put('©', "Copyright sign");
+ specialKeyContentDescriptions.put('®', "Registered sign");
+ specialKeyContentDescriptions.put('™', "Trade mark sign");
+ specialKeyContentDescriptions.put('â„…', "Care of");
+ SPECIAL_KEY_CONTENT_DESCRIPTIONS =
+ Collections.unmodifiableMap(specialKeyContentDescriptions);
+
+ String alwaysVisibleCharacters = ".,";
+ ALWAYS_VISIBLE_CHARACTERS = createImmutableSet(alwaysVisibleCharacters);
+
+ String keyboardNumberScreenSymbols = "@#$%&-+()*\"':;!?_/";
+ KEYBOARD_NUMBER_SCREEN_SYMBOLS = createImmutableSet(keyboardNumberScreenSymbols);
+
+ String keyboardOtherSymbols = "~`|•√π÷×¶Δ£¢€¥^°={}\\©®™â„…[]<>";
+ KEYBOARD_OTHER_SYMBOLS = createImmutableSet(keyboardOtherSymbols);
+ }
+
+ public GoogleKeyboardHelperImpl(Instrumentation instr) {
+ super(instr);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void open() {
+ Log.w(TAG, "No method defined to open Google Keyboard. (no-op)");
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public String getPackage() {
+ return UI_PACKAGE_NAME;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public String getLauncherName() {
+ return "Google Keyboard";
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void dismissInitialDialogs() {
+ mDevice.pressHome();
+ UiObject2 searchPlate = mDevice.findObject(
+ By.res(UI_QUICK_SEARCH_BOX_PACKAGE_NAME, "search_plate"));
+ if (searchPlate != null) {
+ searchPlate.click();
+ UiObject2 skipGoogleNowButton = mDevice.wait(Until.findObject(
+ By.res(UI_QUICK_SEARCH_BOX_PACKAGE_NAME, UI_DECLINE_BUTTON_ID)), 20000);
+ if (skipGoogleNowButton != null) {
+ skipGoogleNowButton.click();
+ }
+ BySelector closeSelector = By.text(Pattern.compile("CLOSE", Pattern.CASE_INSENSITIVE));
+ Assert.assertTrue("Could not find close button to dismiss Google Keyboard dialog",
+ mDevice.wait(Until.hasObject(closeSelector), 5000));
+ mDevice.findObject(closeSelector).click();
+ mDevice.wait(Until.gone(closeSelector), 5000);
+ }
+
+ }
+
+ private static Set<Character> createImmutableSet(String setCharacters) {
+ Assert.assertNotNull("setCharacters cannot be null", setCharacters);
+
+ Set<Character> tempSet = new HashSet<>();
+ for (int i = 0; i < setCharacters.length(); ++i) {
+ tempSet.add(setCharacters.charAt(i));
+ }
+ return Collections.unmodifiableSet(tempSet);
+ }
+
+ private UiObject2 getKeyboardView() {
+ return mDevice.findObject(By.clazz(UI_ANDROID_VIEW_CLASS).res(
+ UI_RESOURCE_NAME, UI_KEYBOARD_VIEW_ID));
+ }
+
+ private UiObject2 getShiftKey() {
+ return mDevice.findObject(
+ By.clazz(UI_KEYBOARD_KEY_CLASS).desc(UI_KEYBOARD_SHIFT_KEY_DESC));
+ }
+
+ private UiObject2 getNumberKey() {
+ return mDevice.findObject(
+ By.clazz(UI_KEYBOARD_KEY_CLASS).desc(UI_KEYBOARD_NUMBER_KEY_DESC));
+ }
+
+ private UiObject2 getLetterKey() {
+ return mDevice.findObject(
+ By.clazz(UI_KEYBOARD_KEY_CLASS).desc(UI_KEYBOARD_LETTER_KEY_DESC));
+ }
+
+ private UiObject2 getSymbolKey() {
+ return mDevice.findObject(
+ By.clazz(UI_KEYBOARD_KEY_CLASS).desc(UI_KEYBOARD_SYMBOL_KEY_DESC));
+ }
+
+ private String getKeyDesc(char key) {
+ String specialKeyDesc = SPECIAL_KEY_CONTENT_DESCRIPTIONS.get(key);
+ if (specialKeyDesc != null) {
+ return specialKeyDesc;
+ } else {
+ return String.valueOf(key);
+ }
+ }
+
+ private UiObject2 getKeyboardKey(char key) {
+ String keyDesc = getKeyDesc(key);
+
+ return mDevice.findObject(By.clazz(UI_KEYBOARD_KEY_CLASS).desc(keyDesc));
+ }
+
+ private boolean isLowerCaseLetter(char c) {
+ return (c >= 'a' && c <= 'z');
+ }
+
+ private boolean isUpperCaseLetter(char c) {
+ return (c >= 'A' && c <= 'Z');
+ }
+
+ private boolean isDigit(char c) {
+ return (c >= '0' && c <= '9');
+ }
+
+ private boolean isKeyboardOpen() {
+ return (getKeyboardView() != null);
+ }
+
+ private boolean isOnLowerCaseMode() {
+ return (getKeyboardKey(KEYBOARD_TEST_LOWER_CASE_LETTER) != null);
+ }
+
+ private boolean isOnUpperCaseMode() {
+ return (getKeyboardKey(KEYBOARD_TEST_UPPER_CASE_LETTER) != null);
+ }
+
+ private boolean isOnNumberMode() {
+ return (getKeyboardKey(KEYBOARD_TEST_NUMBER) != null);
+ }
+
+ private boolean isOnSymbolMode() {
+ return (getKeyboardKey(KEYBOARD_TEST_SYMBOL) != null);
+ }
+
+ private void toggleShiftMode() {
+ UiObject2 shiftKey = getShiftKey();
+ Assert.assertNotNull("Could not find Shift key", shiftKey);
+
+ shiftKey.click();
+ }
+
+ private void switchToLetterMode() {
+ UiObject2 letterKey = getLetterKey();
+ Assert.assertNotNull("Could not find Letter key", letterKey);
+
+ letterKey.click();
+ }
+
+ private void switchToLowerCaseMode() {
+ if (isOnNumberMode() || isOnSymbolMode()) {
+ switchToLetterMode();
+ }
+
+ if (isOnUpperCaseMode()) {
+ toggleShiftMode();
+ }
+
+ Assert.assertTrue("Could not switch to lower case letters mode on Google Keyboard",
+ mDevice.wait(Until.hasObject(By.clazz(UI_KEYBOARD_KEY_CLASS).desc(
+ String.valueOf(KEYBOARD_TEST_LOWER_CASE_LETTER))), KEYBOARD_MODE_CHANGE_TIMEOUT));
+ }
+
+ private void switchToUpperCaseMode() {
+ if (isOnNumberMode() || isOnSymbolMode()) {
+ switchToLetterMode();
+ }
+
+ if (isOnLowerCaseMode()) {
+ toggleShiftMode();
+ }
+
+ Assert.assertTrue("Could not switch to upper case letters mode on Google Keyboard",
+ mDevice.wait(Until.hasObject(By.clazz(UI_KEYBOARD_KEY_CLASS).desc(
+ String.valueOf(KEYBOARD_TEST_UPPER_CASE_LETTER))), KEYBOARD_MODE_CHANGE_TIMEOUT));
+ }
+
+ private void switchToNumberMode() {
+ if (!isOnNumberMode()) {
+ UiObject2 numberKey = getNumberKey();
+ Assert.assertNotNull("Could not find Number key", numberKey);
+
+ numberKey.click();
+ }
+
+ Assert.assertTrue("Could not switch to number mode on Google Keyboard",
+ mDevice.wait(Until.hasObject(By.clazz(UI_KEYBOARD_KEY_CLASS).desc(
+ String.valueOf(KEYBOARD_TEST_NUMBER))), KEYBOARD_MODE_CHANGE_TIMEOUT));
+ }
+
+ private void switchToSymbolMode() {
+ if (isOnLowerCaseMode() || isOnUpperCaseMode()) {
+ switchToNumberMode();
+ }
+
+ if (isOnNumberMode()) {
+ UiObject2 symbolKey = getSymbolKey();
+ Assert.assertNotNull("Could not find Symbol key", symbolKey);
+
+ symbolKey.click();
+ }
+
+ Assert.assertTrue("Could not switch to symbol mode on Google Keyboard",
+ mDevice.wait(Until.hasObject(By.clazz(UI_KEYBOARD_KEY_CLASS).desc(
+ String.valueOf(KEYBOARD_TEST_SYMBOL))), KEYBOARD_MODE_CHANGE_TIMEOUT));
+ }
+
+ /*
+ * {@inheritDoc}
+ */
+ @Override
+ public boolean waitForKeyboard(long timeout) {
+ return mDevice.wait(Until.hasObject(By.clazz(UI_ANDROID_VIEW_CLASS).res(
+ UI_RESOURCE_NAME, UI_KEYBOARD_VIEW_ID)), timeout);
+ }
+
+ /*
+ * {@inheritDoc}
+ */
+ @Override
+ public void typeText(String text, long delayBetweenKeyPresses) {
+ Assert.assertTrue("Google Keyboard is not open", isKeyboardOpen());
+ for (int i = 0; i < text.length(); ++i) {
+ char c = text.charAt(i);
+
+ if (ALWAYS_VISIBLE_CHARACTERS.contains(c)) {
+ // Period and comma are visible on all keyboard modes so no need to switch modes
+ } else if (isLowerCaseLetter(c)) {
+ switchToLowerCaseMode();
+ } else if (isUpperCaseLetter(c)) {
+ switchToUpperCaseMode();
+ } else if (isDigit(c) ||
+ KEYBOARD_NUMBER_SCREEN_SYMBOLS.contains(c)) {
+ switchToNumberMode();
+ } else if (KEYBOARD_OTHER_SYMBOLS.contains(c)) {
+ switchToSymbolMode();
+ } else {
+ Assert.fail(String.format("Unrecognized character '%c'", c));
+ }
+ UiObject2 keyboardKey = getKeyboardKey(c);
+ Assert.assertNotNull(String.format("Could not find key '%c' on Google Keyboard", c),
+ keyboardKey);
+
+ keyboardKey.click();
+ SystemClock.sleep(delayBetweenKeyPresses);
+ }
+ }
+}
diff --git a/libraries/google-messenger-app-helper/Android.mk b/libraries/google-messenger-app-helper/Android.mk
new file mode 100644
index 0000000..cf548b0
--- /dev/null
+++ b/libraries/google-messenger-app-helper/Android.mk
@@ -0,0 +1,23 @@
+#
+# Copyright (C) 2016 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT 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 := google-messenger-app-helper
+LOCAL_JAVA_LIBRARIES := ub-uiautomator base-app-helpers
+LOCAL_SRC_FILES := $(call all-java-files-under, src)
+
+include $(BUILD_STATIC_JAVA_LIBRARY)
diff --git a/libraries/google-messenger-app-helper/src/android/platform/test/helpers/GoogleMessengerHelperImpl.java b/libraries/google-messenger-app-helper/src/android/platform/test/helpers/GoogleMessengerHelperImpl.java
new file mode 100644
index 0000000..d7b08c0
--- /dev/null
+++ b/libraries/google-messenger-app-helper/src/android/platform/test/helpers/GoogleMessengerHelperImpl.java
@@ -0,0 +1,275 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.platform.test.helpers;
+
+import android.app.Instrumentation;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.os.SystemClock;
+import android.platform.test.helpers.exceptions.UnknownUiException;
+import android.support.test.uiautomator.By;
+import android.support.test.uiautomator.BySelector;
+import android.support.test.uiautomator.Direction;
+import android.support.test.uiautomator.Until;
+import android.support.test.uiautomator.UiDevice;
+import android.support.test.uiautomator.UiObject2;
+import android.util.Log;
+
+import java.util.List;
+
+public class GoogleMessengerHelperImpl extends AbstractGoogleMessengerHelper {
+ private static final String TAG = GoogleMessengerHelperImpl.class.getSimpleName();
+
+ private static final String UI_ATTACH_MEDIA_BUTTON_ID = "attach_media_button";
+ private static final String UI_CHOOSE_PHOTO_TEXT = "Choose photo";
+ private static final String UI_COMPOSE_MESSAGE_TEXT_ID = "compose_message_text";
+ private static final String UI_CONTACT_NAME_ID = "contact_name";
+ private static final String UI_MEDIA_FROM_DEVICE_DESC = "Choose images from this device";
+ private static final String UI_MEDIA_GALLERY_GRID_VIEW_ID = "gallery_grid_view";
+ private static final String UI_MEDIA_PICKER_TABSTRIP_ID = "mediapicker_tabstrip";
+ private static final String UI_PACKAGE_NAME = "com.google.android.apps.messaging";
+ private static final String UI_RECIPIENT_TEXT_VIEW_ID = "recipient_text_view";
+ private static final String UI_SEND_MESSAGE_BUTTON_ID = "send_message_button";
+ private static final String UI_START_NEW_CONVERSATION_BUTTON_ID =
+ "start_new_conversation_button";
+
+ private static final long UI_DIALOG_WAIT = 5000; // 5 sec
+
+ public GoogleMessengerHelperImpl(Instrumentation instr) {
+ super(instr);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public String getPackage() {
+ return UI_PACKAGE_NAME;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public String getLauncherName() {
+ return "Messenger";
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void dismissInitialDialogs() {
+
+ }
+
+ private UiObject2 getStartNewConversationButton() {
+ return mDevice.findObject(By.res(UI_PACKAGE_NAME, UI_START_NEW_CONVERSATION_BUTTON_ID));
+ }
+
+ private UiObject2 getRecipientTextView() {
+ return mDevice.findObject(By.res(UI_PACKAGE_NAME, UI_RECIPIENT_TEXT_VIEW_ID));
+ }
+
+ private UiObject2 getComposeMessageEditText() {
+ return mDevice.findObject(By.res(UI_PACKAGE_NAME, UI_COMPOSE_MESSAGE_TEXT_ID));
+ }
+
+ private UiObject2 getSendMessageButton() {
+ return mDevice.findObject(By.res(UI_PACKAGE_NAME, UI_SEND_MESSAGE_BUTTON_ID));
+ }
+
+ private UiObject2 getMessageRecyclerView() {
+ return mDevice.findObject(By.pkg(UI_PACKAGE_NAME)
+ .clazz("android.support.v7.widget.RecyclerView").res("android", "list"));
+ }
+
+ private UiObject2 getAttachMediaButton() {
+ return mDevice.findObject(By.res(UI_PACKAGE_NAME, UI_ATTACH_MEDIA_BUTTON_ID));
+ }
+
+ private UiObject2 getMediaFromDeviceTab() {
+ return mDevice.findObject(By.pkg(UI_PACKAGE_NAME).desc(UI_MEDIA_FROM_DEVICE_DESC));
+ }
+
+ private UiObject2 getMediaGalleryGridView() {
+ return mDevice.findObject(By.res(UI_PACKAGE_NAME, UI_MEDIA_GALLERY_GRID_VIEW_ID));
+ }
+
+ private boolean isOnHomePage() {
+ return (getStartNewConversationButton() != null);
+ }
+
+ /*
+ * {@inheritDoc}
+ */
+ @Override
+ public void goToHomePage() {
+ for (int retriesRemaining = 5; retriesRemaining > 0 && !isOnHomePage();
+ --retriesRemaining) {
+ mDevice.pressBack();
+ mDevice.waitForIdle();
+ }
+ }
+
+ /*
+ * {@inheritDoc}
+ */
+ @Override
+ public void goToNewConversationPage() {
+ UiObject2 startNewConversationButton = getStartNewConversationButton();
+ if (startNewConversationButton == null) {
+ throw new IllegalStateException("Could not find start new conversation button");
+ }
+
+ startNewConversationButton.click();
+ if (!mDevice.wait(Until.hasObject(
+ By.res(UI_PACKAGE_NAME, UI_RECIPIENT_TEXT_VIEW_ID)), UI_DIALOG_WAIT)) {
+ throw new UnknownUiException("Could not find recipient text view");
+ }
+ }
+
+ /*
+ * {@inheritDoc}
+ */
+ @Override
+ public void goToMessagesPage() {
+ UiObject2 contact = mDevice.findObject(By.res(UI_PACKAGE_NAME, UI_CONTACT_NAME_ID));
+ if (contact == null) {
+ throw new IllegalStateException("Could not find first contact drop down menu item");
+ }
+
+ contact.click();
+ if (!mDevice.wait(Until.hasObject(
+ By.res(UI_PACKAGE_NAME, UI_COMPOSE_MESSAGE_TEXT_ID)), UI_DIALOG_WAIT)) {
+ throw new UnknownUiException("Could not find compose message edit text");
+ }
+ }
+
+ private void goToFullscreenChooseMediaPage() {
+ UiObject2 mediaGalleryGridView = getMediaGalleryGridView();
+ if (mediaGalleryGridView == null) {
+ throw new IllegalStateException("Could not find media gallery grid view");
+ }
+
+ mediaGalleryGridView.scroll(Direction.DOWN, 5.0f);
+ if (!mDevice.wait(Until.hasObject(By.pkg(UI_PACKAGE_NAME).text(UI_CHOOSE_PHOTO_TEXT)),
+ UI_DIALOG_WAIT)) {
+ throw new UnknownUiException("Could not find full screen media gallery grid view");
+ }
+ }
+
+ /*
+ * {@inheritDoc}
+ */
+ @Override
+ public void scrollMessages(Direction direction) {
+ if (!(Direction.UP.equals(direction) || Direction.DOWN.equals(direction))) {
+ throw new IllegalArgumentException("Direction must be UP or DOWN");
+ }
+
+ UiObject2 messageRecyclerView = getMessageRecyclerView();
+ if (messageRecyclerView == null) {
+ throw new UnknownUiException("Could not find message recycler view");
+ }
+
+ messageRecyclerView.scroll(direction, 10.0f);
+ }
+
+ /*
+ * {@inheritDoc}
+ */
+ @Override
+ public void clickComposeMessageText() {
+ UiObject2 composeMessageEditText = getComposeMessageEditText();
+ if (composeMessageEditText == null) {
+ throw new IllegalStateException("Could not find compose message edit text");
+ }
+
+ composeMessageEditText.click();
+ }
+
+ /*
+ * {@inheritDoc}
+ */
+ @Override
+ public void clickSendMessageButton() {
+ UiObject2 sendMessageButton = getSendMessageButton();
+ if (sendMessageButton == null) {
+ throw new IllegalStateException("Could not find send message button");
+ }
+
+ sendMessageButton.click();
+ }
+
+ private void clickAttachMediaButton() {
+ UiObject2 attachMediaButton = getAttachMediaButton();
+ if (attachMediaButton == null) {
+ throw new IllegalStateException("Could not find attach media button");
+ }
+
+ attachMediaButton.click();
+ if (!mDevice.wait(Until.hasObject(
+ By.res(UI_PACKAGE_NAME, UI_MEDIA_PICKER_TABSTRIP_ID)), UI_DIALOG_WAIT)) {
+ throw new UnknownUiException("Could not find media picker tabstrip");
+ }
+ }
+
+ private void clickMediaFromDeviceTab() {
+ UiObject2 mediaFromDeviceTab = getMediaFromDeviceTab();
+ if (mediaFromDeviceTab == null) {
+ throw new IllegalStateException("Could not find media from device tab");
+ }
+
+ if (!mediaFromDeviceTab.isSelected()) {
+ mediaFromDeviceTab.click();
+ if (!mDevice.wait(Until.hasObject(By.pkg(UI_PACKAGE_NAME).desc(
+ UI_MEDIA_FROM_DEVICE_DESC).selected(true)), UI_DIALOG_WAIT)) {
+ throw new UnknownUiException("Media from device tab not selected");
+ }
+ }
+ }
+
+ /*
+ * {@inheritDoc}
+ */
+ @Override
+ public void attachMediaFromDevice(int index) {
+ clickAttachMediaButton();
+ clickMediaFromDeviceTab();
+ goToFullscreenChooseMediaPage();
+
+ UiObject2 mediaGalleryGridView = getMediaGalleryGridView();
+ if (mediaGalleryGridView == null) {
+ throw new UnknownUiException("Could not find media gallery grid view");
+ }
+
+ List<UiObject2> mediaGalleryChildren = mediaGalleryGridView.getChildren();
+ if (index < 0 || index >= mediaGalleryChildren.size()) {
+ throw new IndexOutOfBoundsException(String.format("index %d >= size %d",
+ index, mediaGalleryChildren.size()));
+ }
+
+ int imageChildIndex = 1;
+ UiObject2 imageView = mediaGalleryChildren.get(index).
+ getChildren().get(imageChildIndex);
+ while (getMediaGalleryGridView() != null) {
+ imageView.click();
+ // Needed to prevent StaleObjectException
+ SystemClock.sleep(2000);
+ }
+ }
+}
diff --git a/libraries/launcher-helper/src/android/support/test/launcherhelper/AospLauncherStrategy.java b/libraries/launcher-helper/src/android/support/test/launcherhelper/AospLauncherStrategy.java
index a9009ee..938e6a6 100644
--- a/libraries/launcher-helper/src/android/support/test/launcherhelper/AospLauncherStrategy.java
+++ b/libraries/launcher-helper/src/android/support/test/launcherhelper/AospLauncherStrategy.java
@@ -44,7 +44,7 @@
@Override
public void open() {
// if we see hotseat, assume at home screen already
- if (!mDevice.hasObject(HOTSEAT)) {
+ if (!mDevice.hasObject(getHotSeatSelector())) {
mDevice.pressHome();
Assert.assertTrue("Failed to open launcher",
mDevice.wait(Until.hasObject(By.pkg(LAUNCHER_PKG)), 5000));
@@ -69,9 +69,15 @@
// taps on the "apps" button at the bottom of the screen
mDevice.findObject(By.desc("Apps")).click();
// wait until hotseat disappears, so that we know that we are no longer on home screen
- mDevice.wait(Until.gone(HOTSEAT), 2000);
+ mDevice.wait(Until.gone(getHotSeatSelector()), 2000);
mDevice.waitForIdle();
}
+ // check if there's a "cling" on screen
+ UiObject2 cling = mDevice.findObject(By.res(LAUNCHER_PKG, "cling_dismiss")
+ .clazz(Button.class).text("OK"));
+ if (cling != null) {
+ cling.click();
+ }
// taps on the "apps" page selector near the top of the screen
UiObject2 appsTab = mDevice.findObject(By.desc("Apps")
.clazz(TextView.class).selected(false));
@@ -108,7 +114,7 @@
// taps on the "apps" button at the bottom of the screen
mDevice.findObject(By.desc("Apps")).click();
// wait until hotseat disappears, so that we know that we are no longer on home screen
- mDevice.wait(Until.gone(HOTSEAT), 2000);
+ mDevice.wait(Until.gone(getHotSeatSelector()), 2000);
mDevice.waitForIdle();
}
// taps on the "Widgets" page selector near the top of the screen
@@ -146,7 +152,7 @@
* {@inheritDoc}
*/
@Override
- public boolean launch(String appName, String packageName) {
+ public long launch(String appName, String packageName) {
return CommonLauncherHelper.getInstance(mDevice).launchApp(this,
By.res("").clazz(TextView.class).desc(appName), packageName);
}
@@ -195,6 +201,14 @@
* {@inheritDoc}
*/
@Override
+ public BySelector getHotSeatSelector() {
+ return HOTSEAT;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
public Direction getWorkspaceScrollDirection() {
return Direction.RIGHT;
}
diff --git a/libraries/launcher-helper/src/android/support/test/launcherhelper/BaseLauncher3Strategy.java b/libraries/launcher-helper/src/android/support/test/launcherhelper/BaseLauncher3Strategy.java
new file mode 100644
index 0000000..40aec1d
--- /dev/null
+++ b/libraries/launcher-helper/src/android/support/test/launcherhelper/BaseLauncher3Strategy.java
@@ -0,0 +1,191 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.support.test.launcherhelper;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+
+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.support.test.uiautomator.Until;
+import android.util.Log;
+import android.widget.TextView;
+import junit.framework.Assert;
+
+public abstract class BaseLauncher3Strategy implements ILauncherStrategy {
+ private static final String LOG_TAG = BaseLauncher3Strategy.class.getSimpleName();
+ protected UiDevice mDevice;
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void setUiDevice(UiDevice uiDevice) {
+ mDevice = uiDevice;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void open() {
+ // if we see hotseat, assume at home screen already
+ if (!mDevice.hasObject(getHotSeatSelector())) {
+ mDevice.pressHome();
+ // ensure launcher is shown
+ if (!mDevice.wait(Until.hasObject(getHotSeatSelector()), 5000)) {
+ // HACK: dump hierarchy to logcat
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ try {
+ mDevice.dumpWindowHierarchy(baos);
+ baos.flush();
+ baos.close();
+ String[] lines = baos.toString().split("\\r?\\n");
+ for (String line : lines) {
+ Log.d(LOG_TAG, line.trim());
+ }
+ } catch (IOException ioe) {
+ Log.e(LOG_TAG, "error dumping XML to logcat", ioe);
+ }
+ Assert.fail("Failed to open launcher");
+ }
+ mDevice.waitForIdle();
+ }
+ dismissHomeScreenCling();
+ }
+
+ /**
+ * Checks and dismisses home screen cling
+ */
+ protected void dismissHomeScreenCling() {
+ // empty default implementation
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public UiObject2 openAllApps(boolean reset) {
+ // if we see all apps container, skip the opening step
+ if (!mDevice.hasObject(getAllAppsSelector())) {
+ open();
+ // taps on the "apps" button at the bottom of the screen
+ mDevice.findObject(By.desc("Apps")).click();
+ // wait until hotseat disappears, so that we know that we are no longer on home screen
+ mDevice.wait(Until.gone(getHotSeatSelector()), 2000);
+ mDevice.waitForIdle();
+ }
+ UiObject2 allAppsContainer = mDevice.wait(Until.findObject(getAllAppsSelector()), 2000);
+ Assert.assertNotNull("openAllApps: did not find all apps container", allAppsContainer);
+ if (reset) {
+ CommonLauncherHelper.getInstance(mDevice).scrollBackToBeginning(
+ allAppsContainer, Direction.reverse(getAllAppsScrollDirection()));
+ }
+ return allAppsContainer;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public Direction getAllAppsScrollDirection() {
+ return Direction.DOWN;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public UiObject2 openAllWidgets(boolean reset) {
+ if (!mDevice.hasObject(getAllWidgetsSelector())) {
+ open();
+ // trigger the wallpapers/widgets/settings view
+ mDevice.pressMenu();
+ mDevice.waitForIdle();
+ mDevice.findObject(By.res(getSupportedLauncherPackage(), "widget_button")).click();
+ }
+ UiObject2 allWidgetsContainer = mDevice.wait(
+ Until.findObject(getAllWidgetsSelector()), 2000);
+ Assert.assertNotNull("openAllWidgets: did not find all widgets container",
+ allWidgetsContainer);
+ if (reset) {
+ CommonLauncherHelper.getInstance(mDevice).scrollBackToBeginning(
+ allWidgetsContainer, Direction.reverse(getAllWidgetsScrollDirection()));
+ }
+ return allWidgetsContainer;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public Direction getAllWidgetsScrollDirection() {
+ return Direction.DOWN;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public long launch(String appName, String packageName) {
+ BySelector app = By.res(
+ getSupportedLauncherPackage(), "icon").clazz(TextView.class).desc(appName);
+ return CommonLauncherHelper.getInstance(mDevice).launchApp(this, app, packageName);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public BySelector getAllAppsSelector() {
+ return By.res(getSupportedLauncherPackage(), "apps_list_view");
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public BySelector getAllWidgetsSelector() {
+ return By.res(getSupportedLauncherPackage(), "widgets_list_view");
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public BySelector getWorkspaceSelector() {
+ return By.res(getSupportedLauncherPackage(), "workspace");
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public BySelector getHotSeatSelector() {
+ return By.res(getSupportedLauncherPackage(), "hotseat");
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public Direction getWorkspaceScrollDirection() {
+ return Direction.RIGHT;
+ }
+}
diff --git a/libraries/launcher-helper/src/android/support/test/launcherhelper/CommonLauncherHelper.java b/libraries/launcher-helper/src/android/support/test/launcherhelper/CommonLauncherHelper.java
index de1776d..1ed33d0 100644
--- a/libraries/launcher-helper/src/android/support/test/launcherhelper/CommonLauncherHelper.java
+++ b/libraries/launcher-helper/src/android/support/test/launcherhelper/CommonLauncherHelper.java
@@ -16,6 +16,8 @@
package android.support.test.launcherhelper;
import android.graphics.Rect;
+import android.os.RemoteException;
+import android.os.SystemClock;
import android.support.test.uiautomator.By;
import android.support.test.uiautomator.BySelector;
import android.support.test.uiautomator.Direction;
@@ -75,7 +77,7 @@
attempts++;
if (attempts > maxAttempts) {
throw new RuntimeException(
- "scrollBackToBeginning: exceeded max attampts: " + maxAttempts);
+ "scrollBackToBeginning: exceeded max attempts: " + maxAttempts);
}
}
}
@@ -119,7 +121,7 @@
* @param packageName
* @return
*/
- public boolean launchApp(ILauncherStrategy launcherStrategy, BySelector app,
+ public long launchApp(ILauncherStrategy launcherStrategy, BySelector app,
String packageName) {
return launchApp(launcherStrategy, app, packageName, MAX_SCROLL_ATTEMPTS);
}
@@ -131,10 +133,19 @@
* @param app
* @param packageName
* @param maxScrollAttempts
- * @return
+ * @return the SystemClock#uptimeMillis timestamp just before launching the application.
*/
- public boolean launchApp(ILauncherStrategy launcherStrategy, BySelector app,
+ public long launchApp(ILauncherStrategy launcherStrategy, BySelector app,
String packageName, int maxScrollAttempts) {
+ unlockDeviceIfAsleep();
+
+ if (isAppOpen(packageName)) {
+ // Application is already open
+ return 0;
+ }
+
+ // Go to the home page
+ launcherStrategy.open();
Direction dir = launcherStrategy.getAllAppsScrollDirection();
// attempt to find the app icon if it's not already on the screen
if (!mDevice.hasObject(app)) {
@@ -147,7 +158,7 @@
attempts++;
if (attempts > maxScrollAttempts) {
throw new RuntimeException(
- "launchApp: exceeded max attampts to locate app icon: "
+ "launchApp: exceeded max attempts to locate app icon: "
+ maxScrollAttempts);
}
}
@@ -156,17 +167,43 @@
ensureIconVisible(app, container, dir);
}
+ long ready = SystemClock.uptimeMillis();
if (!mDevice.findObject(app).clickAndWait(Until.newWindow(), APP_LAUNCH_TIMEOUT)) {
Log.w(LOG_TAG, "no new window detected after app launch attempt.");
- return false;
+ return ILauncherStrategy.LAUNCH_FAILED_TIMESTAMP;
}
mDevice.waitForIdle();
if (packageName != null) {
Log.w(LOG_TAG, String.format(
"No UI element with package name %s detected.", packageName));
- return mDevice.wait(Until.hasObject(By.pkg(packageName).depth(0)), APP_LAUNCH_TIMEOUT);
+ boolean success = mDevice.wait(Until.hasObject(
+ By.pkg(packageName).depth(0)), APP_LAUNCH_TIMEOUT);
+ if (success) {
+ return ready;
+ } else {
+ return ILauncherStrategy.LAUNCH_FAILED_TIMESTAMP;
+ }
} else {
- return true;
+ return ready;
+ }
+ }
+
+ private boolean isAppOpen (String appPackage) {
+ return mDevice.hasObject(By.pkg(appPackage).depth(0));
+ }
+
+ private void unlockDeviceIfAsleep () {
+ // Turn screen on if necessary
+ try {
+ if (!mDevice.isScreenOn()) {
+ mDevice.wakeUp();
+ }
+ } catch (RemoteException e) {
+ Log.e(LOG_TAG, "Failed to unlock the screen-off device.", e);
+ }
+ // Check for lock screen element
+ if (mDevice.hasObject(By.res("com.android.systemui", "keyguard_bottom_area"))) {
+ mDevice.pressMenu();
}
}
}
diff --git a/libraries/launcher-helper/src/android/support/test/launcherhelper/GoogleExperienceLauncherStrategy.java b/libraries/launcher-helper/src/android/support/test/launcherhelper/GoogleExperienceLauncherStrategy.java
index bd92b8b..bc78c33 100644
--- a/libraries/launcher-helper/src/android/support/test/launcherhelper/GoogleExperienceLauncherStrategy.java
+++ b/libraries/launcher-helper/src/android/support/test/launcherhelper/GoogleExperienceLauncherStrategy.java
@@ -15,177 +15,15 @@
*/
package android.support.test.launcherhelper;
-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.support.test.uiautomator.Until;
-import android.util.Log;
-import android.widget.TextView;
-
-import junit.framework.Assert;
-
-import java.io.ByteArrayOutputStream;
-import java.io.IOException;
-
/**
* Implementation of {@link ILauncherStrategy} to support Google experience launcher
*/
-public class GoogleExperienceLauncherStrategy implements ILauncherStrategy {
+public class GoogleExperienceLauncherStrategy extends BaseLauncher3Strategy {
- private static final String LOG_TAG = GoogleExperienceLauncherStrategy.class.getSimpleName();
private static final String LAUNCHER_PKG = "com.google.android.googlequicksearchbox";
- private static final BySelector APPS_CONTAINER = By.res(LAUNCHER_PKG, "all_apps_container");
- private static final BySelector WIDGETS_CONTAINER = By.res(LAUNCHER_PKG, "widgets_list_view");
- private static final BySelector WORKSPACE = By.res(LAUNCHER_PKG, "workspace");
- private static final BySelector HOTSEAT = By.res(LAUNCHER_PKG, "hotseat");
- private UiDevice mDevice;
- /**
- * {@inheritDoc}
- */
- @Override
- public void setUiDevice(UiDevice uiDevice) {
- mDevice = uiDevice;
- }
-
- /**
- * {@inheritDoc}
- */
- @Override
- public void open() {
- // if we see hotseat, assume at home screen already
- if (!mDevice.hasObject(HOTSEAT)) {
- mDevice.pressHome();
- // ensure launcher is shown
- if (!mDevice.wait(Until.hasObject(By.res(LAUNCHER_PKG, "hotseat")), 5000)) {
- // HACK: dump hierarchy to logcat
- ByteArrayOutputStream baos = new ByteArrayOutputStream();
- try {
- mDevice.dumpWindowHierarchy(baos);
- baos.flush();
- baos.close();
- String[] lines = baos.toString().split("\\r?\\n");
- for (String line : lines) {
- Log.d(LOG_TAG, line.trim());
- }
- } catch (IOException ioe) {
- Log.e(LOG_TAG, "error dumping XML to logcat", ioe);
- }
- Assert.fail("Failed to open launcher");
- }
- mDevice.waitForIdle();
- }
- }
-
- /**
- * {@inheritDoc}
- */
- @Override
- public UiObject2 openAllApps(boolean reset) {
- // if we see all apps container, skip the opening step
- if (!mDevice.hasObject(APPS_CONTAINER)) {
- open();
- // taps on the "apps" button at the bottom of the screen
- mDevice.findObject(By.desc("Apps")).click();
- // wait until hotseat disappears, so that we know that we are no longer on home screen
- mDevice.wait(Until.gone(HOTSEAT), 2000);
- mDevice.waitForIdle();
- }
- UiObject2 allAppsContainer = mDevice.wait(Until.findObject(APPS_CONTAINER), 2000);
- Assert.assertNotNull("openAllApps: did not find all apps container", allAppsContainer);
- if (reset) {
- CommonLauncherHelper.getInstance(mDevice).scrollBackToBeginning(
- allAppsContainer, Direction.reverse(getAllAppsScrollDirection()));
- }
- return allAppsContainer;
- }
-
- /**
- * {@inheritDoc}
- */
- @Override
- public Direction getAllAppsScrollDirection() {
- return Direction.DOWN;
- }
-
- /**
- * {@inheritDoc}
- */
- @Override
- public UiObject2 openAllWidgets(boolean reset) {
- if (!mDevice.hasObject(WIDGETS_CONTAINER)) {
- open();
- // trigger the wallpapers/widgets/settings view
- mDevice.pressMenu();
- mDevice.waitForIdle();
- mDevice.findObject(By.res(LAUNCHER_PKG, "widget_button")).click();
- }
- UiObject2 allWidgetsContainer = mDevice.wait(Until.findObject(WIDGETS_CONTAINER), 2000);
- Assert.assertNotNull("openAllWidgets: did not find all widgets container",
- allWidgetsContainer);
- if (reset) {
- CommonLauncherHelper.getInstance(mDevice).scrollBackToBeginning(
- allWidgetsContainer, Direction.reverse(getAllWidgetsScrollDirection()));
- }
- return allWidgetsContainer;
- }
-
- /**
- * {@inheritDoc}
- */
- @Override
- public Direction getAllWidgetsScrollDirection() {
- return Direction.DOWN;
- }
-
- /**
- * {@inheritDoc}
- */
- @Override
- public boolean launch(String appName, String packageName) {
- BySelector app = By.res(LAUNCHER_PKG, "icon").clazz(TextView.class).desc(appName);
- return CommonLauncherHelper.getInstance(mDevice).launchApp(this, app, packageName);
- }
-
- /**
- * {@inheritDoc}
- */
@Override
public String getSupportedLauncherPackage() {
return LAUNCHER_PKG;
}
-
- /**
- * {@inheritDoc}
- */
- @Override
- public BySelector getAllAppsSelector() {
- return APPS_CONTAINER;
- }
-
- /**
- * {@inheritDoc}
- */
- @Override
- public BySelector getAllWidgetsSelector() {
- return WIDGETS_CONTAINER;
- }
-
- /**
- * {@inheritDoc}
- */
- @Override
- public BySelector getWorkspaceSelector() {
- return WORKSPACE;
- }
-
- /**
- * {@inheritDoc}
- */
- @Override
- public Direction getWorkspaceScrollDirection() {
- return Direction.RIGHT;
- }
}
diff --git a/libraries/launcher-helper/src/android/support/test/launcherhelper/ILauncherStrategy.java b/libraries/launcher-helper/src/android/support/test/launcherhelper/ILauncherStrategy.java
index 301bd1b..bf26bb4 100644
--- a/libraries/launcher-helper/src/android/support/test/launcherhelper/ILauncherStrategy.java
+++ b/libraries/launcher-helper/src/android/support/test/launcherhelper/ILauncherStrategy.java
@@ -27,6 +27,7 @@
* method.
*/
public interface ILauncherStrategy {
+ public static final long LAUNCH_FAILED_TIMESTAMP = -1;
/**
* Returns the launcher application package that this {@link ILauncherStrategy} can automate
@@ -90,6 +91,12 @@
public BySelector getWorkspaceSelector();
/**
+ * Returns a {@link BySelector} describing the home screen hot seat (app icons at the bottom)
+ * @return
+ */
+ public BySelector getHotSeatSelector();
+
+ /**
* Retrieves the home screen workspace forward scroll direction as implemented by the launcher
* @return
*/
@@ -104,5 +111,5 @@
* @return <code>true</code> if application is verified to be in foreground after launch, or the
* verification is skipped; <code>false</code> otherwise.
*/
- public boolean launch(String appName, String packageName);
+ public long launch(String appName, String packageName);
}
diff --git a/libraries/launcher-helper/src/android/support/test/launcherhelper/ILeanbackLauncherStrategy.java b/libraries/launcher-helper/src/android/support/test/launcherhelper/ILeanbackLauncherStrategy.java
new file mode 100644
index 0000000..52e3eea
--- /dev/null
+++ b/libraries/launcher-helper/src/android/support/test/launcherhelper/ILeanbackLauncherStrategy.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 android.support.test.launcherhelper;
+
+import android.support.test.uiautomator.BySelector;
+import android.support.test.uiautomator.UiDevice;
+import android.support.test.uiautomator.UiObject2;
+
+/**
+ * Defines the common use cases a leanback launcher UI automation helper should fulfill.
+ * <p>Class will be instantiated by {@link LauncherStrategyFactory} based on current launcher
+ * package, and a {@link UiDevice} instance will be provided via {@link #setUiDevice(UiDevice)}
+ * method.
+ */
+public interface ILeanbackLauncherStrategy extends ILauncherStrategy {
+
+ /**
+ * Searches for a given query on leanback launcher
+ */
+ public void search(String query);
+
+ /**
+ * Returns a {@link BySelector} describing the search row
+ * @return
+ */
+ public BySelector getSearchRowSelector();
+
+ /**
+ * Returns a {@link BySelector} describing the notification row (or recommendation row)
+ * @return
+ */
+ public BySelector getNotificationRowSelector();
+
+ /**
+ * Returns a {@link BySelector} describing the apps row
+ * @return
+ */
+ public BySelector getAppsRowSelector();
+
+ /**
+ * Returns a {@link BySelector} describing the games row
+ * @return
+ */
+ public BySelector getGamesRowSelector();
+
+ /**
+ * Returns a {@link BySelector} describing the settings row
+ * @return
+ */
+ public BySelector getSettingsRowSelector();
+
+ /**
+ * Returns a {@link UiObject2} describing the focused search row
+ * @return
+ */
+ public UiObject2 selectSearchRow();
+
+ /**
+ * Returns a {@link UiObject2} describing the focused notification row
+ * @return
+ */
+ public UiObject2 selectNotificationRow();
+
+ /**
+ * Returns a {@link UiObject2} describing the focused apps row
+ * @return
+ */
+ public UiObject2 selectAppsRow();
+
+ /**
+ * Returns a {@link UiObject2} describing the focused games row
+ * @return
+ */
+ public UiObject2 selectGamesRow();
+
+ /**
+ * Returns a {@link UiObject2} describing the focused settings row
+ * @return
+ */
+ public UiObject2 selectSettingsRow();
+}
diff --git a/libraries/launcher-helper/src/android/support/test/launcherhelper/Launcher3Strategy.java b/libraries/launcher-helper/src/android/support/test/launcherhelper/Launcher3Strategy.java
new file mode 100644
index 0000000..60b946c
--- /dev/null
+++ b/libraries/launcher-helper/src/android/support/test/launcherhelper/Launcher3Strategy.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 android.support.test.launcherhelper;
+
+import android.support.test.uiautomator.By;
+import android.support.test.uiautomator.UiObject2;
+
+public class Launcher3Strategy extends BaseLauncher3Strategy {
+
+ private static final String LAUNCHER_PKG = "com.android.launcher3";
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public String getSupportedLauncherPackage() {
+ return LAUNCHER_PKG;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ protected void dismissHomeScreenCling() {
+ super.dismissHomeScreenCling();
+ // dismiss first run cling
+ UiObject2 gotItButton = mDevice.findObject(
+ By.res(getSupportedLauncherPackage(), "cling_dismiss_longpress_info"));
+ if (gotItButton != null) {
+ gotItButton.click();
+ }
+ }
+}
diff --git a/libraries/launcher-helper/src/android/support/test/launcherhelper/LauncherStrategyFactory.java b/libraries/launcher-helper/src/android/support/test/launcherhelper/LauncherStrategyFactory.java
index 880638b..f591fe5 100644
--- a/libraries/launcher-helper/src/android/support/test/launcherhelper/LauncherStrategyFactory.java
+++ b/libraries/launcher-helper/src/android/support/test/launcherhelper/LauncherStrategyFactory.java
@@ -41,11 +41,13 @@
mKnownLauncherStrategies = new HashSet<>();
registerLauncherStrategy(AospLauncherStrategy.class);
registerLauncherStrategy(GoogleExperienceLauncherStrategy.class);
+ registerLauncherStrategy(Launcher3Strategy.class);
+ registerLauncherStrategy(LeanbackLauncherStrategy.class);
}
/**
* Retrieves an instance of the {@link LauncherStrategyFactory}
- * @param context
+ * @param uiDevice
* @return
*/
public static LauncherStrategyFactory getInstance(UiDevice uiDevice) {
@@ -79,7 +81,7 @@
* Retrieves a {@link ILauncherStrategy} that supports the current default launcher
* <p>
* {@link ILauncherStrategy} maybe registered via
- * {@link LauncherStrategyRegistry#registerLauncherStrategy(String, Class)} by identifying the
+ * {@link LauncherStrategyFactory#registerLauncherStrategy(Class)} by identifying the
* launcher package name supported
* @return
*/
@@ -87,4 +89,17 @@
String launcherPkg = mUiDevice.getLauncherPackageName();
return mInstanceMap.get(launcherPkg);
}
+
+ /**
+ * Retrieves a {@link ILeanbackLauncherStrategy} that supports the current default leanback
+ * launcher
+ * @return
+ */
+ public ILeanbackLauncherStrategy getLeanbackLauncherStrategy() {
+ ILauncherStrategy launcherStrategy = getLauncherStrategy();
+ if (launcherStrategy instanceof ILeanbackLauncherStrategy) {
+ return (ILeanbackLauncherStrategy)launcherStrategy;
+ }
+ throw new RuntimeException("This LauncherStrategy is not for leanback launcher.");
+ }
}
diff --git a/libraries/launcher-helper/src/android/support/test/launcherhelper/LeanbackLauncherStrategy.java b/libraries/launcher-helper/src/android/support/test/launcherhelper/LeanbackLauncherStrategy.java
new file mode 100644
index 0000000..d3d4c0b
--- /dev/null
+++ b/libraries/launcher-helper/src/android/support/test/launcherhelper/LeanbackLauncherStrategy.java
@@ -0,0 +1,500 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package android.support.test.launcherhelper;
+
+import android.graphics.Point;
+import android.os.RemoteException;
+import android.os.SystemClock;
+import android.support.test.uiautomator.*;
+import android.util.Log;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+
+public class LeanbackLauncherStrategy implements ILeanbackLauncherStrategy {
+
+ private static final String LOG_TAG = LeanbackLauncherStrategy.class.getSimpleName();
+ private static final String PACKAGE_LAUNCHER = "com.google.android.leanbacklauncher";
+ private static final String PACKAGE_SEARCH = "com.google.android.katniss";
+
+ private static final int MAX_SCROLL_ATTEMPTS = 20;
+ private static final int APP_LAUNCH_TIMEOUT = 10000;
+ private static final int SHORT_WAIT_TIME = 5000; // 5 sec
+
+ protected UiDevice mDevice;
+
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public String getSupportedLauncherPackage() {
+ return PACKAGE_LAUNCHER;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void setUiDevice(UiDevice uiDevice) {
+ mDevice = uiDevice;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void open() {
+ // if we see main list view, assume at home screen already
+ if (!mDevice.hasObject(getWorkspaceSelector())) {
+ mDevice.pressHome();
+ // ensure launcher is shown
+ if (!mDevice.wait(Until.hasObject(getWorkspaceSelector()), SHORT_WAIT_TIME)) {
+ // HACK: dump hierarchy to logcat
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ try {
+ mDevice.dumpWindowHierarchy(baos);
+ baos.flush();
+ baos.close();
+ String[] lines = baos.toString().split("\\r?\\n");
+ for (String line : lines) {
+ Log.d(LOG_TAG, line.trim());
+ }
+ } catch (IOException ioe) {
+ Log.e(LOG_TAG, "error dumping XML to logcat", ioe);
+ }
+ throw new RuntimeException("Failed to open leanback launcher");
+ }
+ mDevice.waitForIdle();
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public UiObject2 openAllApps(boolean reset) {
+ UiObject2 appsRow = selectAppsRow();
+ if (appsRow == null) {
+ throw new RuntimeException("Could not find all apps row");
+ }
+ if (reset) {
+ Log.w(LOG_TAG, "The reset will be ignored on leanback launcher");
+ }
+ return appsRow;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public BySelector getWorkspaceSelector() {
+ return By.res(getSupportedLauncherPackage(), "main_list_view");
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public BySelector getSearchRowSelector() {
+ return By.res(getSupportedLauncherPackage(), "search_view");
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public BySelector getNotificationRowSelector() {
+ return By.res(getSupportedLauncherPackage(), "notification_view");
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public BySelector getAppsRowSelector() {
+ return By.res(getSupportedLauncherPackage(), "list").desc("Apps");
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public BySelector getGamesRowSelector() {
+ return By.res(getSupportedLauncherPackage(), "list").desc("Games");
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public BySelector getSettingsRowSelector() {
+ return By.res(getSupportedLauncherPackage(), "list").desc("")
+ .hasDescendant(By.res("icon"));
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public Direction getAllAppsScrollDirection() {
+ return Direction.RIGHT;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public BySelector getAllAppsSelector() {
+ // On Leanback launcher the Apps row corresponds to the All Apps on phone UI
+ return getAppsRowSelector();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public long launch(String appName, String packageName) {
+ BySelector app = By.res(getSupportedLauncherPackage(), "app_banner").desc(appName);
+ return launchApp(this, app, packageName);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void search(String query) {
+ if (selectSearchRow() == null) {
+ throw new RuntimeException("Could not find search row.");
+ }
+
+ BySelector keyboardOrb = By.res(getSupportedLauncherPackage(), "keyboard_orb");
+ UiObject2 orbButton = mDevice.wait(Until.findObject(keyboardOrb), SHORT_WAIT_TIME);
+ if (orbButton == null) {
+ throw new RuntimeException("Could not find keyboard orb.");
+ }
+ if (orbButton.isFocused()) {
+ mDevice.pressDPadCenter();
+ } else {
+ // Move the focus to keyboard orb by DPad button.
+ mDevice.pressDPadRight();
+ if (orbButton.isFocused()) {
+ mDevice.pressDPadCenter();
+ }
+ }
+ mDevice.wait(Until.gone(keyboardOrb), SHORT_WAIT_TIME);
+
+ BySelector searchEditor = By.res(PACKAGE_SEARCH, "search_text_editor");
+ UiObject2 editText = mDevice.wait(Until.findObject(searchEditor), SHORT_WAIT_TIME);
+ if (editText == null) {
+ throw new RuntimeException("Could not find search text input.");
+ }
+
+ editText.setText(query);
+ SystemClock.sleep(SHORT_WAIT_TIME);
+
+ // Note that Enter key is pressed instead of DPad keys to dismiss leanback IME
+ mDevice.pressEnter();
+ mDevice.wait(Until.gone(searchEditor), SHORT_WAIT_TIME);
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * Assume that the rows are sorted in the following order from the top:
+ * Search, Notification(, Partner), Apps, Games, Settings(, and Inputs)
+ */
+ @Override
+ public UiObject2 selectNotificationRow() {
+ if (!isNotificationRowSelected()) {
+ open();
+ mDevice.pressHome(); // Home key to move to the first card in the Notification row
+ }
+ return mDevice.wait(Until.findObject(
+ getNotificationRowSelector().hasChild(By.focused(true))), SHORT_WAIT_TIME);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public UiObject2 selectSearchRow() {
+ if (!isSearchRowSelected()) {
+ selectNotificationRow();
+ mDevice.pressDPadUp();
+ }
+ return mDevice.wait(Until.findObject(
+ getSearchRowSelector().hasDescendant(By.focused(true))), SHORT_WAIT_TIME);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public UiObject2 selectAppsRow() {
+ // Start finding Apps row from Notification row
+ if (!isAppsRowSelected()) {
+ selectNotificationRow();
+ mDevice.pressDPadDown();
+ }
+ return mDevice.wait(Until.findObject(
+ getAllAppsSelector().hasChild(By.focused(true))), SHORT_WAIT_TIME);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public UiObject2 selectGamesRow() {
+ if (!isGamesRowSelected()) {
+ selectAppsRow();
+ mDevice.pressDPadDown();
+ // If more than or equal to 16 apps are installed, the app banner could be cut off
+ // into two rows at maximum. It needs to scroll down once more.
+ if (!isGamesRowSelected()) {
+ mDevice.pressDPadDown();
+ }
+ }
+ return mDevice.wait(Until.findObject(
+ getGamesRowSelector().hasChild(By.focused(true))), SHORT_WAIT_TIME);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public UiObject2 selectSettingsRow() {
+ if (!isSettingsRowSelected()) {
+ open();
+ mDevice.pressHome(); // Home key to move to the first card in the Notification row
+ // The Settings row is at the last position
+ final int MAX_ROW_NUMS = 8;
+ for (int i = 0; i < MAX_ROW_NUMS; ++i) {
+ mDevice.pressDPadDown();
+ }
+ }
+ return null;
+ }
+
+ @SuppressWarnings("unused")
+ @Override
+ public UiObject2 openAllWidgets(boolean reset) {
+ throw new UnsupportedOperationException(
+ "All Widgets is not available on Leanback Launcher.");
+ }
+
+ @SuppressWarnings("unused")
+ @Override
+ public BySelector getAllWidgetsSelector() {
+ throw new UnsupportedOperationException(
+ "All Widgets is not available on Leanback Launcher.");
+ }
+
+ @SuppressWarnings("unused")
+ @Override
+ public Direction getAllWidgetsScrollDirection() {
+ throw new UnsupportedOperationException(
+ "All Widgets is not available on Leanback Launcher.");
+ }
+
+ @SuppressWarnings("unused")
+ @Override
+ public BySelector getHotSeatSelector() {
+ throw new UnsupportedOperationException(
+ "Hot Seat is not available on Leanback Launcher.");
+ }
+
+ @SuppressWarnings("unused")
+ @Override
+ public Direction getWorkspaceScrollDirection() {
+ throw new UnsupportedOperationException(
+ "Workspace is not available on Leanback Launcher.");
+ }
+
+ protected long launchApp(ILauncherStrategy launcherStrategy, BySelector app,
+ String packageName) {
+ return launchApp(launcherStrategy, app, packageName, MAX_SCROLL_ATTEMPTS);
+ }
+
+ protected long launchApp(ILauncherStrategy launcherStrategy, BySelector app,
+ String packageName, int maxScrollAttempts) {
+ unlockDeviceIfAsleep();
+
+ if (isAppOpen(packageName)) {
+ // Application is already open
+ return 0;
+ }
+
+ // Go to the home page
+ launcherStrategy.open();
+ // attempt to find the app icon if it's not already on the screen
+ UiObject2 container = launcherStrategy.openAllApps(false);
+ UiObject2 appIcon = container.findObject(app);
+ int attempts = 0;
+ while (attempts++ < maxScrollAttempts) {
+ // Compare the focused icon and the app icon to search for.
+ UiObject2 focusedIcon = container.findObject(By.focused(true))
+ .findObject(By.res(getSupportedLauncherPackage(), "app_banner"));
+
+ if (appIcon == null) {
+ appIcon = findApp(container, focusedIcon, app);
+ if (appIcon == null) {
+ throw new RuntimeException("Failed to find the app icon on screen: "
+ + packageName);
+ }
+ continue;
+ } else if (focusedIcon.equals(appIcon)) {
+ // The app icon is on the screen, and selected.
+ break;
+ } else {
+ // The app icon is on the screen, but not selected yet
+ // Move one step closer to the app icon
+ Point currentPosition = focusedIcon.getVisibleCenter();
+ Point targetPosition = appIcon.getVisibleCenter();
+ int dx = targetPosition.x - currentPosition.x;
+ int dy = targetPosition.y - currentPosition.y;
+ final int MARGIN = 10;
+ // The sequence of moving should be kept in the following order so as not to
+ // be stuck in case that the apps row are not even.
+ if (dx < -MARGIN) {
+ mDevice.pressDPadLeft();
+ continue;
+ }
+ if (dy < -MARGIN) {
+ mDevice.pressDPadUp();
+ continue;
+ }
+ if (dx > MARGIN) {
+ mDevice.pressDPadRight();
+ continue;
+ }
+ if (dy > MARGIN) {
+ mDevice.pressDPadDown();
+ continue;
+ }
+ throw new RuntimeException(
+ "Failed to navigate to the app icon on screen: " + packageName);
+ }
+ }
+
+ if (attempts == maxScrollAttempts) {
+ throw new RuntimeException(
+ "scrollBackToBeginning: exceeded max attempts: " + maxScrollAttempts);
+ }
+
+ // The app icon is already found and focused.
+ long ready = SystemClock.uptimeMillis();
+ mDevice.pressDPadCenter();
+ mDevice.waitForIdle();
+ if (packageName != null) {
+ Log.w(LOG_TAG, String.format(
+ "No UI element with package name %s detected.", packageName));
+ boolean success = mDevice.wait(Until.hasObject(
+ By.pkg(packageName).depth(0)), APP_LAUNCH_TIMEOUT);
+ if (success) {
+ return ready;
+ } else {
+ return ILauncherStrategy.LAUNCH_FAILED_TIMESTAMP;
+ }
+ } else {
+ return ready;
+ }
+ }
+
+ protected boolean isSearchRowSelected() {
+ UiObject2 row = mDevice.findObject(getSearchRowSelector());
+ if (row == null) {
+ return false;
+ }
+ return row.hasObject(By.focused(true));
+ }
+
+ protected boolean isAppsRowSelected() {
+ UiObject2 row = mDevice.findObject(getAppsRowSelector());
+ if (row == null) {
+ return false;
+ }
+ return row.hasObject(By.focused(true));
+ }
+
+ protected boolean isGamesRowSelected() {
+ UiObject2 row = mDevice.findObject(getGamesRowSelector());
+ if (row == null) {
+ return false;
+ }
+ return row.hasObject(By.focused(true));
+ }
+
+ protected boolean isNotificationRowSelected() {
+ UiObject2 row = mDevice.findObject(getNotificationRowSelector());
+ if (row == null) {
+ return false;
+ }
+ return row.hasObject(By.focused(true));
+ }
+
+ protected boolean isSettingsRowSelected() {
+ // Settings label is only visible if the settings row is selected
+ return mDevice.hasObject(By.res(getSupportedLauncherPackage(), "label").text("Settings"));
+ }
+
+ protected boolean isAppOpen (String appPackage) {
+ return mDevice.hasObject(By.pkg(appPackage).depth(0));
+ }
+
+ protected void unlockDeviceIfAsleep () {
+ // Turn screen on if necessary
+ try {
+ if (!mDevice.isScreenOn()) {
+ mDevice.wakeUp();
+ }
+ } catch (RemoteException e) {
+ Log.e(LOG_TAG, "Failed to unlock the screen-off device.", e);
+ }
+ }
+
+ protected UiObject2 findApp(UiObject2 container, UiObject2 focusedIcon, BySelector app) {
+ UiObject2 appIcon;
+ // The app icon is not on the screen.
+ // Search by going left first until it finds the app icon on the screen
+ String prevText = focusedIcon.getContentDescription();
+ String nextText;
+ do {
+ mDevice.pressDPadLeft();
+ appIcon = container.findObject(app);
+ if (appIcon != null) {
+ return appIcon;
+ }
+ nextText = container.findObject(By.focused(true)).findObject(
+ By.res(getSupportedLauncherPackage(),
+ "app_banner")).getContentDescription();
+ } while (nextText != null && !nextText.equals(prevText));
+
+ // If we haven't found it yet, search by going right
+ do {
+ mDevice.pressDPadRight();
+ appIcon = container.findObject(app);
+ if (appIcon != null) {
+ return appIcon;
+ }
+ nextText = container.findObject(By.focused(true)).findObject(
+ By.res(getSupportedLauncherPackage(),
+ "app_banner")).getContentDescription();
+ } while (nextText != null && !nextText.equals(prevText));
+ return null;
+ }
+}
diff --git a/libraries/maps-app-helper/Android.mk b/libraries/maps-app-helper/Android.mk
new file mode 100644
index 0000000..74259a3
--- /dev/null
+++ b/libraries/maps-app-helper/Android.mk
@@ -0,0 +1,23 @@
+#
+# Copyright (C) 2016 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT 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 := maps-app-helper
+LOCAL_JAVA_LIBRARIES := ub-uiautomator base-app-helpers
+LOCAL_SRC_FILES := $(call all-java-files-under, src)
+
+include $(BUILD_STATIC_JAVA_LIBRARY)
diff --git a/libraries/maps-app-helper/src/android/platform/test/helpers/MapsHelperImpl.java b/libraries/maps-app-helper/src/android/platform/test/helpers/MapsHelperImpl.java
new file mode 100644
index 0000000..a24b5a3
--- /dev/null
+++ b/libraries/maps-app-helper/src/android/platform/test/helpers/MapsHelperImpl.java
@@ -0,0 +1,397 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.platform.test.helpers;
+
+import android.app.Instrumentation;
+import android.content.pm.PackageManager.NameNotFoundException;
+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.Until;
+import android.support.test.uiautomator.UiDevice;
+import android.support.test.uiautomator.UiObject2;
+import android.util.Log;
+
+import java.util.regex.Pattern;
+
+
+public class MapsHelperImpl extends AbstractMapsHelper {
+ private static final String LOG_TAG = MapsHelperImpl.class.getSimpleName();
+
+ private static final String UI_CLOSE_NAVIGATION_DESC = "Close navigation";
+ private static final String UI_DIRECTIONS_BUTTON_ID = "placepage_directions_button";
+ private static String UI_PACKAGE;
+ private static final String UI_START_NAVIGATION_BUTTON_ID = "start_button";
+ private static final String UI_TEXTVIEW_CLASS = "android.widget.TextView";
+ private static final String UI_PROGRESSBAR_CLASS = "android.widget.ProgressBar";
+
+ private static final int UI_RESPONSE_WAIT = 5000;
+ private static final int SEARCH_RESPONSE_WAIT = 25000;
+ private static final int MAP_SERVER_CONNECT_WAIT = 120000;
+
+ private boolean mIsVersion9p30;
+
+ private static final int MAX_CONNECT_TO_SERVER_RETRY = 5;
+ private static final int MAX_START_NAV_RETRY = 5;
+ private static final int MAX_DISMISS_INITIAL_DIALOG_RETRY = 2;
+
+ public MapsHelperImpl(Instrumentation instr) {
+ super(instr);
+
+ try {
+ mIsVersion9p30 = getVersion().startsWith("9.30.");
+ if (mIsVersion9p30) {
+ UI_PACKAGE = "com.google.android.apps.maps";
+ } else {
+ UI_PACKAGE = "com.google.android.apps.gmm";
+ }
+ } catch (NameNotFoundException e) {
+ Log.e(LOG_TAG, String.format("Unable to find package by name, %s", getPackage()));
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public String getPackage() {
+ return "com.google.android.apps.maps";
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public String getLauncherName() {
+ return "Maps";
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void dismissInitialDialogs() {
+ Log.v(LOG_TAG, "Maps dismissing initial welcome screen.");
+
+ // ToS welcome dialog
+ boolean successTosDismiss = hasSearchBar(0);
+
+ String text = "ACCEPT & CONTINUE";
+ Pattern pattern = Pattern.compile(text, Pattern.CASE_INSENSITIVE);
+
+ UiObject2 terms = mDevice.wait(Until.findObject(By.text(pattern)), 10000);
+ int tryCounter = 0;
+
+ while ((terms != null) && (tryCounter < MAX_DISMISS_INITIAL_DIALOG_RETRY)) {
+ terms.click();
+
+ mDevice.wait(Until.gone(By.pkg(UI_PACKAGE).clazz(UI_PROGRESSBAR_CLASS)),
+ MAP_SERVER_CONNECT_WAIT);
+
+ if (!checkServerConnectivity()) {
+ throw new IllegalStateException("Unable to connect to Google Maps server");
+ }
+
+ terms = mDevice.wait(Until.findObject(By.text(pattern)), UI_RESPONSE_WAIT);
+ tryCounter += 1;
+ }
+
+ if (terms != null) {
+ throw new IllegalStateException("Unable to dismiss initial dialogs");
+ }
+
+ if (mIsVersion9p30) {
+ exit();
+ open();
+ }
+
+ // Location services dialog
+ text = "YES, I'M IN";
+ pattern = Pattern.compile(text, Pattern.CASE_INSENSITIVE);
+ UiObject2 location = mDevice.wait(Until.findObject(By.text(pattern)),
+ UI_RESPONSE_WAIT);
+ if (location != null) {
+ location.click();
+ mDevice.waitForIdle();
+ } else {
+ Log.e(LOG_TAG, "Did not find a location services dialog.");
+ }
+
+ if (!mIsVersion9p30) {
+ // Tap here dialog
+ UiObject2 cling = mDevice.wait(
+ Until.findObject(By.res(UI_PACKAGE, "tapherehint_textbox")),
+ UI_RESPONSE_WAIT);
+ if (cling != null) {
+ cling.click();
+ mDevice.waitForIdle();
+ } else {
+ Log.e(LOG_TAG, "Did not find 'tap here' dialog");
+ }
+
+ // Reset map dialog
+ UiObject2 resetView = mDevice.wait(
+ Until.findObject(By.res(UI_PACKAGE, "mylocation_button")),
+ UI_RESPONSE_WAIT);
+ if (resetView != null) {
+ resetView.click();
+ mDevice.waitForIdle();
+ } else {
+ Log.e(LOG_TAG, "Did not find 'reset map' dialog.");
+ }
+ }
+
+ // 'Side menu' dialog
+ text = "GOT IT";
+ pattern = Pattern.compile(text, Pattern.CASE_INSENSITIVE);
+ BySelector gotIt = By.text(Pattern.compile("GOT IT", Pattern.CASE_INSENSITIVE));
+ UiObject2 sideMenuTut = mDevice.wait(Until.findObject(gotIt), 5000);
+ if (sideMenuTut != null) {
+ sideMenuTut.click();
+ } else {
+ Log.e(LOG_TAG, "Did not find any 'side menu' dialog.");
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void doSearch(String query) {
+ Log.v(LOG_TAG, "Maps doing an address search");
+
+ if (!checkServerConnectivity()) {
+ throw new IllegalStateException("Cannot connect to Google Maps servers");
+ }
+
+ // Navigate if necessary
+ goToQueryScreen();
+ // Select search bar
+ UiObject2 searchSelect = getSelectableSearchBar(0);
+ if (searchSelect == null) {
+ throw new IllegalStateException("No selectable search bar found.");
+ }
+ searchSelect.click();
+
+ // Edit search query
+ UiObject2 searchEdit = getEditableSearchBar(UI_RESPONSE_WAIT);
+ if (searchEdit == null) {
+ throw new IllegalStateException("Not editable search bar found.");
+ }
+ searchEdit.clear();
+ searchEdit.setText(query);
+
+ // Search and wait for the directions option
+ UiObject2 firstAddressResult = mDevice.wait(Until.findObject(By.pkg(UI_PACKAGE).clazz(
+ UI_TEXTVIEW_CLASS)), SEARCH_RESPONSE_WAIT);
+ if (firstAddressResult == null) {
+ String err_msg = String.format("Did not detect address result after %d seconds",
+ (int) Math.floor(SEARCH_RESPONSE_WAIT / 1000));
+ throw new IllegalStateException(err_msg);
+ }
+ firstAddressResult.click();
+
+ if (getDirectionsButton(SEARCH_RESPONSE_WAIT) == null) {
+ throw new IllegalStateException("Could not find directions button");
+ }
+ }
+
+ /*
+ * {@inheritDoc}
+ */
+ @Override
+ public void getDirections() {
+ Log.v(LOG_TAG, "Maps getting direction.");
+
+ dismissPullUpDialog();
+
+ UiObject2 directionsButton = getDirectionsButton(UI_RESPONSE_WAIT);
+ if (directionsButton == null) {
+ throw new IllegalStateException("Unable to find start direction button");
+ }
+ directionsButton.click();
+
+ dismissGetARidePopUp();
+ if (getStartNavigationButton(UI_RESPONSE_WAIT) == null) {
+ throw new IllegalStateException("Unable to find start navigation button");
+ }
+ }
+
+ /*
+ * {@inheritDoc}
+ */
+ @Override
+ public void startNavigation() {
+ Log.v(LOG_TAG, "starting navigation.");
+
+ UiObject2 startNavigationButton = getStartNavigationButton(UI_RESPONSE_WAIT);
+
+ if (startNavigationButton == null) {
+ dismissGetARidePopUp();
+ startNavigationButton = getStartNavigationButton(UI_RESPONSE_WAIT);
+
+ if (startNavigationButton == null) {
+ throw new IllegalStateException("Unable to find start navigation button");
+ }
+ }
+ startNavigationButton.click();
+
+ boolean hasCloseNavigationDesc = (getCloseNavigationButton(UI_RESPONSE_WAIT) != null);
+ int tryCounter = 0;
+ while ((tryCounter < MAX_START_NAV_RETRY) && (!hasCloseNavigationDesc)) {
+ dismissBetaUseDialog();
+ dismissSearchAlongRoutePopUp();
+ hasCloseNavigationDesc = (getCloseNavigationButton(UI_RESPONSE_WAIT) != null);
+ tryCounter += 1;
+ }
+
+ if (!hasCloseNavigationDesc) {
+ throw new IllegalStateException("Unable to find close navigation button");
+ }
+ }
+
+ /*
+ * {@inheritDoc}
+ */
+ @Override
+ public void stopNavigation() {
+ Log.v(LOG_TAG, "stopping navigation.");
+
+ dismissSearchAlongRoutePopUp();
+
+ UiObject2 closeNavigationButton = getCloseNavigationButton(0);
+
+ if (closeNavigationButton != null) {
+ closeNavigationButton.click();
+ }
+
+ if (hasNavigationButton(UI_RESPONSE_WAIT)) {
+ mDevice.pressBack();
+ }
+ }
+
+ private void goToQueryScreen() {
+ for (int backup = 5; backup > 0; backup--) {
+ if (hasSearchBar(0)) {
+ return;
+ } else {
+ mDevice.pressBack();
+ mDevice.waitForIdle();
+ }
+ }
+ }
+
+ private UiObject2 getSelectableSearchBar(int wait_time) {
+ return mDevice.wait(Until.findObject(By.res(UI_PACKAGE, "search_omnibox_text_box")),
+ wait_time);
+ }
+
+ private UiObject2 getEditableSearchBar(int wait_time) {
+ return mDevice.wait(Until.findObject(By.res(UI_PACKAGE, "search_omnibox_edit_text")),
+ wait_time);
+ }
+
+ private UiObject2 getStartNavigationButton(int wait_time) {
+ return mDevice.wait(Until.findObject(By.res(UI_PACKAGE, UI_START_NAVIGATION_BUTTON_ID)),
+ wait_time);
+ }
+
+ private UiObject2 getCloseNavigationButton(int wait_time) {
+ return mDevice.wait(Until.findObject(By.pkg(UI_PACKAGE).desc(UI_CLOSE_NAVIGATION_DESC)),
+ wait_time);
+ }
+
+ private UiObject2 getDirectionsButton(int wait_time) {
+ return mDevice.wait(Until.findObject(By.res(UI_PACKAGE, UI_DIRECTIONS_BUTTON_ID)),
+ wait_time);
+ }
+
+ private boolean hasSearchBar(int wait_time) {
+ return ((getSelectableSearchBar(wait_time) != null) ||
+ (getEditableSearchBar(wait_time) != null));
+ }
+
+ private boolean hasNavigationButton(int wait_time) {
+ return ((getStartNavigationButton(wait_time) != null) ||
+ (getDirectionsButton(wait_time) != null));
+ }
+
+ // check connectivity issues by looking for "TRY AGAIN" pop-up dialog
+ private boolean checkServerConnectivity() {
+ int tryCounter = 0;
+
+ UiObject2 tryAgainButton = mDevice.wait(Until.findObject(By.text("TRY AGAIN")),
+ UI_RESPONSE_WAIT);
+ while ((tryCounter < MAX_CONNECT_TO_SERVER_RETRY) && (tryAgainButton != null)) {
+ tryAgainButton.click();
+
+ tryAgainButton = mDevice.wait(Until.findObject(By.text("TRY AGAIN")),
+ MAP_SERVER_CONNECT_WAIT);
+ tryCounter += 1;
+ }
+
+ if (tryAgainButton != null) {
+ return false;
+ }
+ else {
+ return true;
+ }
+ }
+
+ // Dismiss pop up dialog with title "Google Maps Navigation is in beta. Use caution"
+ private void dismissBetaUseDialog() {
+ UiObject2 acceptButton = mDevice.wait(
+ Until.findObject(By.text("ACCEPT")),
+ UI_RESPONSE_WAIT);
+ if (acceptButton != null) {
+ acceptButton.click();
+ mDevice.wait(Until.gone(By.text("ACCEPT")), UI_RESPONSE_WAIT);
+ }
+ }
+
+ // Dismiss pop-up dialog with title "Search along route"
+ private void dismissSearchAlongRoutePopUp() {
+ UiObject2 searchAlongRoute = mDevice.wait(
+ Until.findObject(By.textContains("Search along route")),
+ UI_RESPONSE_WAIT);
+ if (searchAlongRoute != null) {
+ mDevice.pressBack();
+ }
+ }
+
+ // Dismiss pop-up dialog with title "Pull up"
+ private void dismissPullUpDialog() {
+ UiObject2 gotItButton = mDevice.wait(
+ Until.findObject(By.text("GOT IT")),
+ UI_RESPONSE_WAIT);
+ if (gotItButton != null) {
+ gotItButton.click();
+ mDevice.wait(Until.gone(By.text("GOT IT")), UI_RESPONSE_WAIT);
+ }
+ }
+
+ // Dismiss pop-up advertising for taxi-ride with title "Get a ride in minutes"
+ private void dismissGetARidePopUp() {
+ UiObject2 getARide = mDevice.wait(
+ Until.findObject(By.textContains("Get a ride in minutes")),
+ UI_RESPONSE_WAIT);
+ if (getARide != null) {
+ mDevice.pressBack();
+ }
+ }
+}
diff --git a/libraries/photos-app-helper/Android.mk b/libraries/photos-app-helper/Android.mk
new file mode 100644
index 0000000..6d3dcba
--- /dev/null
+++ b/libraries/photos-app-helper/Android.mk
@@ -0,0 +1,23 @@
+#
+# Copyright (C) 2016 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT 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 := photos-app-helper
+LOCAL_JAVA_LIBRARIES := ub-uiautomator base-app-helpers
+LOCAL_SRC_FILES := $(call all-java-files-under, src)
+
+include $(BUILD_STATIC_JAVA_LIBRARY)
diff --git a/libraries/photos-app-helper/src/android/platform/test/helpers/PhotosHelperImpl.java b/libraries/photos-app-helper/src/android/platform/test/helpers/PhotosHelperImpl.java
new file mode 100644
index 0000000..adbabb1
--- /dev/null
+++ b/libraries/photos-app-helper/src/android/platform/test/helpers/PhotosHelperImpl.java
@@ -0,0 +1,509 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.platform.test.helpers;
+
+import android.app.Instrumentation;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.os.SystemClock;
+import android.platform.test.helpers.exceptions.UnknownUiException;
+import android.support.test.uiautomator.By;
+import android.support.test.uiautomator.BySelector;
+import android.support.test.uiautomator.Direction;
+import android.support.test.uiautomator.Until;
+import android.support.test.uiautomator.UiDevice;
+import android.support.test.uiautomator.UiObject2;
+import android.util.Log;
+
+import java.util.List;
+import java.util.regex.Pattern;
+
+import junit.framework.Assert;
+
+
+import android.os.Environment;
+import java.io.File;
+import java.io.IOException;
+
+public class PhotosHelperImpl extends AbstractPhotosHelper {
+ private static final String LOG_TAG = PhotosHelperImpl.class.getSimpleName();
+
+ private static final long APP_LOAD_WAIT = 7500;
+ private static final long HACKY_WAIT = 2500;
+ private static final long PICTURE_LOAD_WAIT = 20000;
+ private static final long UI_NAVIGATION_WAIT = 5000;
+
+ private static final Pattern UI_PHOTO_DESC = Pattern.compile("^Photo.*");
+
+ private static final String UI_DONE_BUTTON_ID = "done_button";
+ private static final String UI_GET_STARTED_CONTAINER = "get_started_container";
+ private static final String UI_GET_STARTED_ID = "get_started";
+ private static final String UI_LOADING_ICON_ID = "list_empty_progress_bar";
+ private static final String UI_NEXT_BUTTON_ID = "next_button";
+ private static final String UI_PACKAGE_NAME = "com.google.android.apps.photos";
+ private static final String UI_PHOTO_TAB_ID = "tab_photos";
+ private static final String UI_DEVICE_FOLDER_TEXT = "Device folders";
+ private static final String UI_PHOTO_VIEW_PAGER_ID = "photo_view_pager";
+ private static final String UI_PHOTO_SCROLL_VIEW_ID = "recycler_view";
+ private static final String UI_NAVIGATION_LIST_ID = "navigation_list";
+ private static final int MAX_UI_SCROLL_COUNT = 20;
+ private static final int MAX_DISMISS_INIT_DIALOG_RETRY = 20;
+
+ public PhotosHelperImpl(Instrumentation instr) {
+ super(instr);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public String getPackage() {
+ return "com.google.android.apps.photos";
+ }
+
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public String getLauncherName() {
+ return "Photos";
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void dismissInitialDialogs() {
+ // Target Photos version 1.18.0.119671374
+ SystemClock.sleep(APP_LOAD_WAIT);
+
+ if (isOnInitialDialogScreen()) {
+ UiObject2 getStartedButton = mDevice.wait(
+ Until.findObject(By.res(UI_PACKAGE_NAME, UI_GET_STARTED_ID)), APP_LOAD_WAIT);
+ int retryCount = 0;
+ while ((retryCount < MAX_DISMISS_INIT_DIALOG_RETRY) &&
+ (getStartedButton == null)) {
+ /*
+ The UiAutomator sometimes cannot find GET STARTED button even though
+ it is seen on the screen.
+ The reason is because the initial "spinner" animation screen updates
+ views too quickly for UiAutomator to catch the change.
+
+ The following hack is used to reload the init dialog for UiAutomator to
+ retry catching the GET STARTED button.
+ */
+
+ mDevice.pressBack();
+ mDevice.waitForIdle();
+ mDevice.pressHome();
+ mDevice.waitForIdle();
+ open();
+
+ getStartedButton = mDevice.wait(
+ Until.findObject(By.res(UI_PACKAGE_NAME, UI_GET_STARTED_ID)),
+ APP_LOAD_WAIT);
+ retryCount += 1;
+
+ if (!isOnInitialDialogScreen()) {
+ break;
+ }
+ }
+
+ if (isOnInitialDialogScreen() && (getStartedButton == null)) {
+ throw new IllegalStateException("UiAutomator cannot catch GET STARTED button");
+ }
+ else {
+ if (getStartedButton != null) {
+ getStartedButton.click();
+ }
+ }
+ }
+ else {
+ Log.e(LOG_TAG, "Didn't find GET STARTED button.");
+ }
+
+ // Address dialogs with an account vs. without an account
+ Pattern signInWords = Pattern.compile("Sign in", Pattern.CASE_INSENSITIVE);
+ boolean hasAccount = !mDevice.hasObject(By.text(signInWords));
+ if (!hasAccount) {
+ // Select 'NO THANKS' if no account exists
+ Pattern noThanksWords = Pattern.compile("No thanks", Pattern.CASE_INSENSITIVE);
+ UiObject2 noThanksButton = mDevice.findObject(By.text(noThanksWords));
+ if (noThanksButton != null) {
+ noThanksButton.click();
+ mDevice.waitForIdle();
+ } else {
+ Log.e(LOG_TAG, "Unable to find NO THANKS button.");
+ }
+ } else {
+ UiObject2 doneButton = mDevice.wait(Until.findObject(
+ By.res(UI_PACKAGE_NAME, UI_DONE_BUTTON_ID)), 5000);
+ if (doneButton != null) {
+ doneButton.click();
+ mDevice.waitForIdle();
+ }
+ else {
+ Log.e(LOG_TAG, "Didn't find DONE button.");
+ }
+
+ // Press the next button (arrow and check mark) four consecutive times
+ for (int repeat = 0; repeat < 4; repeat++) {
+ UiObject2 nextButton = mDevice.findObject(
+ By.res(UI_PACKAGE_NAME, UI_NEXT_BUTTON_ID));
+ if (nextButton != null) {
+ nextButton.click();
+ mDevice.waitForIdle();
+ } else {
+ Log.e(LOG_TAG, "Unable to find arrow or check mark buttons.");
+ }
+ }
+
+ mDevice.wait(Until.gone(
+ By.res(UI_PACKAGE_NAME, UI_LOADING_ICON_ID)), PICTURE_LOAD_WAIT);
+ }
+
+ mDevice.waitForIdle();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void openFirstClip() {
+ if (searchForVideoClip()) {
+ UiObject2 clip = getFirstClip();
+ if (clip != null) {
+ clip.click();
+ mDevice.wait(Until.findObject(
+ By.res(UI_PACKAGE_NAME, "photos_videoplayer_play_button_holder")), 2000);
+ }
+ else {
+ throw new IllegalStateException("Cannot play a video after finding video clips");
+ }
+ }
+ else {
+ throw new UnsupportedOperationException("Cannot find a video clip");
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void pauseClip() {
+ UiObject2 holder = mDevice.findObject(
+ By.res(UI_PACKAGE_NAME, "photos_videoplayer_play_button_holder"));
+ if (holder != null) {
+ holder.click();
+ } else {
+ throw new UnknownUiException("Unable to find pause button holder.");
+ }
+
+ UiObject2 pause = mDevice.wait(Until.findObject(
+ By.res(UI_PACKAGE_NAME, "photos_videoplayer_pause_button")), 2500);
+ if (pause != null) {
+ pause.click();
+ mDevice.wait(Until.findObject(By.desc("Play video")), 2500);
+ } else {
+ throw new UnknownUiException("Unable to find pause button.");
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void playClip() {
+ UiObject2 play = mDevice.findObject(By.desc("Play video"));
+ if (play != null) {
+ play.click();
+ mDevice.wait(Until.findObject(
+ By.res(UI_PACKAGE_NAME, "photos_videoplayer_pause_button")), 2500);
+ } else {
+ throw new UnknownUiException("Unable to find play button");
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void goToMainScreen() {
+ for (int retriesRemaining = 5; retriesRemaining > 0 && !isOnMainScreen();
+ --retriesRemaining) {
+ // check if we see the Photos tab at the bottom of the screen
+ // If we do, clicking on the tab should go to home screen.
+ UiObject2 photosButton = mDevice.findObject(
+ By.res(UI_PACKAGE_NAME, UI_PHOTO_TAB_ID));
+ if (photosButton != null) {
+ photosButton.click();
+ }
+ else {
+ mDevice.pressBack();
+ }
+ mDevice.waitForIdle();
+ }
+
+ if (!isOnMainScreen()) {
+ throw new IllegalStateException("Cannot go to main screen");
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void openPicture(int index) {
+
+ mDevice.waitForIdle();
+ List<UiObject2> photos = mDevice.findObjects(By.pkg(UI_PACKAGE_NAME).desc(UI_PHOTO_DESC));
+
+ if (photos == null) {
+ throw new IllegalStateException("Cannot find photos on current view screen");
+ }
+
+ if ((index < 0) || (index >= photos.size())) {
+ String errMsg = String.format("Photo index (%d) out of bound (0..%d)",
+ index, photos.size());
+ throw new IllegalArgumentException(errMsg);
+ }
+
+ UiObject2 photo = photos.get(index);
+ photo.click();
+ if (!mDevice.wait(Until.hasObject(By.res(UI_PACKAGE_NAME, UI_PHOTO_VIEW_PAGER_ID)),
+ UI_NAVIGATION_WAIT)) {
+ throw new IllegalStateException("Cannot display photo on screen");
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void scrollAlbum(Direction direction) {
+ if (!(Direction.LEFT.equals(direction) || Direction.RIGHT.equals(direction))) {
+ throw new IllegalArgumentException("Scroll direction must be LEFT or RIGHT");
+ }
+
+ UiObject2 scrollContainer = mDevice.findObject(
+ By.res(UI_PACKAGE_NAME, UI_PHOTO_VIEW_PAGER_ID));
+
+ if (scrollContainer == null) {
+ throw new UnknownUiException("Cannot find scroll container");
+ }
+
+ scrollContainer.scroll(direction, 1.0f);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void goToDeviceFolderScreen() {
+ if (!isOnDeviceFolderScreen()) {
+
+ if (!isOnMainScreen()) {
+ goToMainScreen();
+ }
+
+ openNavigationDrawer();
+
+ UiObject2 deviceFolderButton = mDevice.wait(Until.findObject(
+ By.text(UI_DEVICE_FOLDER_TEXT)), UI_NAVIGATION_WAIT);
+ if (deviceFolderButton != null) {
+ deviceFolderButton.click();
+ }
+ else {
+ UiObject2 photosButton = mDevice.wait(Until.findObject(By.text("Photos")),
+ UI_NAVIGATION_WAIT);
+ if (photosButton != null) {
+ photosButton.click();
+ }
+ else {
+ throw new IllegalStateException("No device folder in navigation drawer");
+ }
+ }
+ }
+
+ if (!isOnDeviceFolderScreen()) {
+ throw new UnknownUiException("Can not go to device folder screen");
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public boolean searchForDeviceFolder(String folderName) {
+ boolean foundFolder = false;
+ int scrollCount = 0;
+ while (!foundFolder && (scrollCount < MAX_UI_SCROLL_COUNT)) {
+ foundFolder = mDevice.wait(Until.hasObject(By.text(folderName)), 2000);
+ if (!foundFolder) {
+ if (!scrollView(Direction.DOWN)) {
+ break;
+ }
+ }
+ scrollCount += 1;
+ }
+
+ if (!foundFolder) {
+ foundFolder = mDevice.wait(Until.hasObject(By.text(folderName)), 2000);
+ }
+
+ return foundFolder;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public boolean searchForVideoClip() {
+ boolean foundVideoClip = false;
+ int scrollCount = 0;
+ while (!foundVideoClip && (scrollCount < MAX_UI_SCROLL_COUNT)) {
+ foundVideoClip = (getFirstClip() != null);
+ if (!foundVideoClip) {
+ if (!scrollView(Direction.DOWN)) {
+ break;
+ }
+ }
+ scrollCount += 1;
+ }
+ return foundVideoClip;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public boolean searchForPicture() {
+ boolean foundPicture = false;
+ int scrollCount = 0;
+ while (!foundPicture && (scrollCount < MAX_UI_SCROLL_COUNT)) {
+ foundPicture = mDevice.wait(Until.hasObject(By.descStartsWith("Photo")), 2000);
+ if (!foundPicture) {
+ if (!scrollView(Direction.DOWN)) {
+ break;
+ }
+ }
+ scrollCount += 1;
+ }
+ return foundPicture;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void openDeviceFolder(String folderName) {
+ UiObject2 deviceFolder = mDevice.wait(Until.findObject(By.text(folderName)),
+ UI_NAVIGATION_WAIT);
+ if (deviceFolder != null) {
+ deviceFolder.click();
+ }
+ else {
+ throw new IllegalArgumentException(String.format("Cannot open device folder %s",
+ folderName));
+ }
+ }
+
+ private UiObject2 getFirstClip() {
+ return mDevice.wait(Until.findObject(By.descStartsWith("Video")), 2000);
+ }
+
+ /**
+ * This function returns true if Photos is currently on the first-use
+ * initial dialog screen, with "Get Started" button displayed on screen
+ *
+ * @return Returns true if app is on the initial dialog screen, false otherwise
+ */
+ private boolean isOnInitialDialogScreen() {
+ return mDevice.hasObject(By.res(UI_PACKAGE_NAME, UI_GET_STARTED_CONTAINER));
+ }
+
+ private boolean isOnMainScreen() {
+ return mDevice.hasObject(By.descContains("Photos, selected"));
+ }
+
+ /**
+ * This function returns true if Photos is currently in the
+ * photo-viewing screen, displaying either one photo
+ * or video on the screen.
+ *
+ * @return Returns true if one photo or video is displayed on the screen,
+ * false otherwise.
+ */
+ private boolean isOnPhotoViewingScreen() {
+ return mDevice.hasObject(By.res(UI_PACKAGE_NAME, UI_PHOTO_VIEW_PAGER_ID));
+ }
+
+ private boolean isOnDeviceFolderScreen() {
+
+ if (mDevice.hasObject(By.pkg(UI_PACKAGE_NAME).text(UI_DEVICE_FOLDER_TEXT))) {
+ return true;
+ }
+
+ // sometimes the "Device Folder" tab is hidden.
+ // scroll down once to make sure the tab is visible
+ UiObject2 scrollContainer = mDevice.findObject(
+ By.res(UI_PACKAGE_NAME, UI_PHOTO_SCROLL_VIEW_ID));
+ if (scrollContainer != null) {
+ scrollContainer.scroll(Direction.DOWN, 1.0f);
+ return mDevice.hasObject(By.pkg(UI_PACKAGE_NAME).text(UI_DEVICE_FOLDER_TEXT));
+ }
+ else {
+ return false;
+ }
+ }
+
+ /**
+ * This function performs one scroll on the current screen, in the direction
+ * specified by input argument.
+ *
+ * @param dir The direction of the scroll
+ * @return Returns whether the object can still scroll in the given direction
+ */
+ private boolean scrollView(Direction dir) {
+ UiObject2 scrollContainer = mDevice.findObject(By.res(UI_PACKAGE_NAME,
+ UI_PHOTO_SCROLL_VIEW_ID));
+ if (scrollContainer == null) {
+ return false;
+ }
+
+ return scrollContainer.scroll(dir, 1.0f);
+ }
+
+ private void openNavigationDrawer() {
+ UiObject2 navigationDrawer = mDevice.findObject(By.desc("Show Navigation Drawer"));
+ if (navigationDrawer == null) {
+ mDevice.pressBack();
+ navigationDrawer = mDevice.wait(Until.findObject(By.desc("Show Navigation Drawer")),
+ UI_NAVIGATION_WAIT);
+ }
+
+ if (navigationDrawer == null) {
+ throw new UnknownUiException("Cannot find navigation drawer");
+ }
+
+ navigationDrawer.click();
+
+ if (!mDevice.hasObject(By.res(UI_PACKAGE_NAME, UI_NAVIGATION_LIST_ID))) {
+ throw new UnknownUiException("Cannot open navigation drawer");
+ }
+ }
+}
diff --git a/libraries/play-books-app-helper/Android.mk b/libraries/play-books-app-helper/Android.mk
new file mode 100644
index 0000000..47da20e
--- /dev/null
+++ b/libraries/play-books-app-helper/Android.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.
+#
+LOCAL_PATH := $(call my-dir)
+
+include $(CLEAR_VARS)
+LOCAL_MODULE := play-books-app-helper
+LOCAL_JAVA_LIBRARIES := ub-uiautomator base-app-helpers
+LOCAL_SRC_FILES := $(call all-java-files-under, src)
+
+include $(BUILD_STATIC_JAVA_LIBRARY)
+
diff --git a/libraries/play-books-app-helper/src/android/platform/test/helpers/PlayBooksHelperImpl.java b/libraries/play-books-app-helper/src/android/platform/test/helpers/PlayBooksHelperImpl.java
new file mode 100644
index 0000000..e54985a
--- /dev/null
+++ b/libraries/play-books-app-helper/src/android/platform/test/helpers/PlayBooksHelperImpl.java
@@ -0,0 +1,280 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.platform.test.helpers;
+
+import android.app.Instrumentation;
+import android.os.SystemClock;
+import android.support.test.uiautomator.By;
+import android.support.test.uiautomator.Direction;
+import android.support.test.uiautomator.Until;
+import android.support.test.uiautomator.UiObject2;
+
+import junit.framework.Assert;
+
+import java.lang.IllegalStateException;
+
+/**
+ * UI test helper for Play Books: The Official App (package: com.google.android.apps.books).
+ * Implementation based on app version: 3.8.15
+ */
+
+public class PlayBooksHelperImpl extends AbstractPlayBooksHelper {
+
+ private static final String LOG_TAG = PlayBooksHelperImpl.class.getSimpleName();
+
+ private static final String UI_PACKAGE_NAME = "com.google.android.apps.books";
+ private static final String UI_NAVIGATE_UP_DESC = "Navigate up";
+ private static final String UI_NAVIGATION_DRAWER_BUTTON_DESC = "Show navigation drawer";
+ private static final String UI_EXIT_BOOK_DESC = "Exit book";
+ private static final String UI_TAB_ALL_BOOKS_TEXT = "ALL BOOKS";
+ private static final String UI_NAVIGATION_DRAWER_SETTING_TEXT = "Settings";
+ private static final String UI_NAVIGATION_DRAWER_MYLIBRARY_TEXT = "My library";
+ private static final String UI_OPTION_MENU_READ_ALOUD_TEXT = "Read aloud";
+ private static final String UI_TURN_SYNC_ON_TEXT = "TURN SYNC ON";
+ private static final String UI_SKIP_TEXT = "SKIP";
+ private static final String UI_FULL_SCREEN_READER = "reader";
+ private static final String UI_PLAY_DRAWER_ROOT = "play_drawer_root";
+ private static final String UI_BOOK_THUMBNAIL = "li_thumbnail";
+
+ private static final long SKIP_DELAY = 2000; // 2 secs
+ private static final long UI_ANIMATION_TIMEOUT = 2500; // 2.5 secs
+ private static final long OPEN_BOOK_TIMEOUT = 10000; // 10 secs
+ private static final long SYNCING_BOOKS_TIMEOUT = 10000; //10 secs
+
+ public PlayBooksHelperImpl(Instrumentation instr) {
+ super(instr);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public String getPackage() {
+ return "com.google.android.apps.books";
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public String getLauncherName() {
+ return "Play Books";
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void dismissInitialDialogs() {
+ UiObject2 skipButton = getSkipButton();
+ if (skipButton != null) {
+ skipButton.click();
+ SystemClock.sleep(SKIP_DELAY);
+ }
+ UiObject2 turnSyncOnButton = getTurnSyncOnButton();
+ if (turnSyncOnButton != null) {
+ turnSyncOnButton.click();
+ SystemClock.sleep(SYNCING_BOOKS_TIMEOUT);
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void goToAllBooksTab() {
+ closeOptionMenu();
+ exitReadingMode();
+ closeSettingPanel();
+ openNavigationDrawer();
+ UiObject2 myLibraryButton = getMyLibraryButton();
+ Assert.assertNotNull("Can't find \"My Library\" button", myLibraryButton);
+ myLibraryButton.click();
+ UiObject2 allBooksButton = mDevice.wait(Until.findObject(
+ By.text(UI_TAB_ALL_BOOKS_TEXT).clickable(true)),
+ UI_ANIMATION_TIMEOUT);
+ Assert.assertNotNull("Can't find \"ALL BOOKS\" tab button", allBooksButton);
+ allBooksButton.click();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void openBook() {
+ UiObject2 bookThumbNail = getBookThumbnail();
+ Assert.assertNotNull("No book in \"ALL BOOKS\" library", bookThumbNail);
+ bookThumbNail.click();
+ mDevice.wait(Until.hasObject(
+ By.res(UI_PACKAGE_NAME, UI_FULL_SCREEN_READER)),
+ OPEN_BOOK_TIMEOUT);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void exitReadingMode() {
+ UiObject2 exitBookButton = null;
+ UiObject2 fullScreenReader = getFullScreenReader();
+ if (fullScreenReader != null) {
+ fullScreenReader.click();
+ exitBookButton = mDevice.wait(
+ Until.findObject(By.desc(UI_EXIT_BOOK_DESC)),
+ UI_ANIMATION_TIMEOUT);
+ Assert.assertNotNull("Fail to exit full screen reader mode", exitBookButton);
+ } else {
+ exitBookButton = getExitBookButton();
+ }
+ if (exitBookButton != null) {
+ exitBookButton.click();
+ boolean hasNavButton = mDevice.wait(Until.hasObject(
+ By.desc(UI_NAVIGATION_DRAWER_BUTTON_DESC)),
+ UI_ANIMATION_TIMEOUT);
+ Assert.assertTrue("Fail to exit reading mode", hasNavButton);
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void goToNextPage() {
+ UiObject2 fullScreenReader = getFullScreenReader();
+ if (fullScreenReader == null) {
+ throw new IllegalStateException("Not on a full-screen page of a book");
+ }
+ int displayHeight = mDevice.getDisplayHeight();
+ int displayWidth = mDevice.getDisplayWidth();
+ int nextPageX = displayWidth - 1;
+ int nextPageY = displayHeight / 2;
+ mDevice.click(nextPageX, nextPageY);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void goToPreviousPage() {
+ UiObject2 fullScreenReader = getFullScreenReader();
+ if (fullScreenReader == null) {
+ throw new IllegalStateException("Not on a full-screen page of a book");
+ }
+ int displayHeight = mDevice.getDisplayHeight();
+ int displayWidth = mDevice.getDisplayWidth();
+ int previousPageX = 0;
+ int previousPageY = displayHeight / 2;
+ mDevice.click(previousPageX, previousPageY);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void scrollToNextPage() {
+ UiObject2 fullScreenReader = getFullScreenReader();
+ if (fullScreenReader == null) {
+ throw new IllegalStateException("Not on a full-screen page of a book");
+ }
+ fullScreenReader.scroll(Direction.RIGHT, 1.0f);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void scrollToPreviousPage() {
+ UiObject2 fullScreenReader = getFullScreenReader();
+ if (fullScreenReader == null) {
+ throw new IllegalStateException("Not on a full-screen page of a book");
+ }
+ fullScreenReader.scroll(Direction.LEFT, 1.0f);
+ }
+
+ private void closeOptionMenu() {
+ if (isOptionMenuExpanded()) {
+ mDevice.pressBack();
+ }
+ }
+
+ private void closeSettingPanel() {
+ UiObject2 backButton = getBackButton();
+ if (backButton != null) {
+ backButton.click();
+ boolean hasNavButton = mDevice.wait(Until.hasObject(
+ By.desc(UI_NAVIGATION_DRAWER_BUTTON_DESC)),
+ UI_ANIMATION_TIMEOUT);
+ Assert.assertNotNull("Fail to close setting panel", hasNavButton);
+ }
+ }
+
+ private void openNavigationDrawer() {
+ if (isDrawerOpen()) {
+ return;
+ }
+ UiObject2 navButton = getNavButton();
+ Assert.assertNotNull("Unable to find navigation drawer button", navButton);
+ navButton.click();
+ waitForNavigationDrawerOpen();
+ }
+
+ private boolean isOptionMenuExpanded() {
+ return mDevice.hasObject(By.text(UI_OPTION_MENU_READ_ALOUD_TEXT));
+ }
+
+ private boolean isDrawerOpen() {
+ return mDevice.hasObject(By.res(UI_PACKAGE_NAME, UI_PLAY_DRAWER_ROOT));
+ }
+
+ private UiObject2 getSkipButton() {
+ return mDevice.findObject(By.text(UI_SKIP_TEXT));
+ }
+
+ private UiObject2 getTurnSyncOnButton() {
+ return mDevice.findObject(By.text(UI_TURN_SYNC_ON_TEXT));
+ }
+
+ private UiObject2 getFullScreenReader() {
+ return mDevice.findObject(By.res(UI_PACKAGE_NAME, UI_FULL_SCREEN_READER));
+ }
+
+ private UiObject2 getExitBookButton() {
+ return mDevice.findObject(By.desc(UI_EXIT_BOOK_DESC));
+ }
+
+ private UiObject2 getBackButton() {
+ return mDevice.findObject(By.desc(UI_NAVIGATE_UP_DESC));
+ }
+
+ private UiObject2 getNavButton() {
+ return mDevice.findObject(By.desc(UI_NAVIGATION_DRAWER_BUTTON_DESC));
+ }
+
+ private UiObject2 getMyLibraryButton() {
+ return mDevice.findObject(By.text(UI_NAVIGATION_DRAWER_MYLIBRARY_TEXT).clickable(true));
+ }
+
+ private UiObject2 getBookThumbnail() {
+ return mDevice.findObject(By.res(UI_PACKAGE_NAME, UI_BOOK_THUMBNAIL));
+ }
+
+ private void waitForNavigationDrawerOpen() {
+ mDevice.wait(Until.hasObject(
+ By.text(UI_NAVIGATION_DRAWER_SETTING_TEXT).clickable(true)),
+ UI_ANIMATION_TIMEOUT);
+ }
+}
\ No newline at end of file
diff --git a/libraries/play-movies-app-helper/Android.mk b/libraries/play-movies-app-helper/Android.mk
new file mode 100644
index 0000000..9c39a62
--- /dev/null
+++ b/libraries/play-movies-app-helper/Android.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.
+#
+LOCAL_PATH := $(call my-dir)
+
+include $(CLEAR_VARS)
+LOCAL_MODULE := play-movies-app-helper
+LOCAL_JAVA_LIBRARIES := ub-uiautomator base-app-helpers
+LOCAL_SRC_FILES := $(call all-java-files-under, src)
+
+include $(BUILD_STATIC_JAVA_LIBRARY)
+
diff --git a/libraries/play-movies-app-helper/src/android/platform/test/helpers/PlayMoviesHelperImpl.java b/libraries/play-movies-app-helper/src/android/platform/test/helpers/PlayMoviesHelperImpl.java
new file mode 100644
index 0000000..99830f5
--- /dev/null
+++ b/libraries/play-movies-app-helper/src/android/platform/test/helpers/PlayMoviesHelperImpl.java
@@ -0,0 +1,215 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.platform.test.helpers;
+
+import android.app.Instrumentation;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.os.SystemClock;
+import android.platform.test.helpers.exceptions.UnknownUiException;
+import android.support.test.uiautomator.By;
+import android.support.test.uiautomator.BySelector;
+import android.support.test.uiautomator.Configurator;
+import android.support.test.uiautomator.Direction;
+import android.support.test.uiautomator.Until;
+import android.support.test.uiautomator.UiDevice;
+import android.support.test.uiautomator.UiObject2;
+import android.util.Log;
+import android.widget.EditText;
+
+import java.util.regex.Pattern;
+
+public class PlayMoviesHelperImpl extends AbstractPlayMoviesHelper {
+ private static final String LOG_TAG = PlayMoviesHelperImpl.class.getSimpleName();
+
+ private static final String UI_PACKAGE = "com.google.android.videos";
+ private static final String UI_NAV_DRAWER_ID = "play_drawer_list";
+ private static final String UI_MOVIE_LIST_ID = "play_header_listview";
+
+ private static final int SEARCH_MOVIES_SCROLL_RETRY = 4;
+ private static final long APP_INIT_WAIT = 5000;
+
+ private boolean mIsVersion3p8 = false;
+
+ public PlayMoviesHelperImpl(Instrumentation instr) {
+ super(instr);
+
+ try {
+ mIsVersion3p8 = getVersion().startsWith("3.8");
+ } catch (NameNotFoundException e) {
+ Log.e(LOG_TAG, String.format("Unable to find package by name, %s", getPackage()));
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void open() {
+ long original = Configurator.getInstance().getWaitForIdleTimeout();
+ Configurator.getInstance().setWaitForIdleTimeout(1500);
+
+ super.open();
+
+ Configurator.getInstance().setWaitForIdleTimeout(original);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public String getPackage() {
+ return UI_PACKAGE;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public String getLauncherName() {
+ return "Play Movies & TV";
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void dismissInitialDialogs() {
+ if (mIsVersion3p8) {
+ BySelector nextButton = By.res(UI_PACKAGE, "end_button");
+ int count = 0;
+ while (mDevice.hasObject(nextButton) && count < 10) {
+ mDevice.findObject(nextButton).click();
+ mDevice.wait(Until.gone(nextButton), 1000);
+ count += 1;
+ }
+ BySelector gotIt = By.textContains("Got It");
+ count = 0;
+ while (mDevice.hasObject(gotIt) && count < 3) {
+ UiObject2 gotItButton = mDevice.findObject(gotIt);
+ if (gotItButton != null) {
+ gotItButton.click();
+ mDevice.wait(Until.gone(gotIt), 1000);
+ }
+ count += 1;
+ }
+ } else {
+ long original = Configurator.getInstance().getWaitForIdleTimeout();
+ Configurator.getInstance().setWaitForIdleTimeout(1500);
+
+ for (int retry = 0; retry < 5; retry++) {
+ Pattern words = Pattern.compile("GET STARTED", Pattern.CASE_INSENSITIVE);
+ UiObject2 startedButton = mDevice.wait(Until.findObject(By.text(words)), 5000);
+ if (startedButton != null) {
+ startedButton.click();
+ }
+ }
+
+ Configurator.getInstance().setWaitForIdleTimeout(original);
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void openMoviesTab() {
+ // Navigate to the Movies tab through the Navigation drawer
+ openNavigationDrawer();
+ Pattern myLibraryPattern = Pattern.compile("My Library", Pattern.CASE_INSENSITIVE);
+ UiObject2 libraryButton = mDevice.findObject(By.text(myLibraryPattern).clickable(true));
+ libraryButton.click();
+ waitForNavigationDrawerClose();
+ // Select the Movies tab if necessary
+ UiObject2 moviesTab = getMoviesTab();
+ if (moviesTab == null) {
+ throw new UnknownUiException("Unable to find the movies tab.");
+ }
+ if (!moviesTab.isSelected()) {
+ moviesTab.click();
+ mDevice.waitForIdle();
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void playMovie(String name) {
+ UiObject2 title = null;
+ for (int retry = 0; retry < SEARCH_MOVIES_SCROLL_RETRY; retry++) {
+ title = mDevice.findObject(By.textContains(name));
+ if (title == null) {
+ UiObject2 scroller = mDevice.findObject(By.res(UI_PACKAGE, UI_MOVIE_LIST_ID));
+ if (scroller != null) {
+ scroller.scroll(Direction.DOWN, 1.0f);
+ }
+ }
+ }
+ if (title == null) {
+ throw new IllegalArgumentException(
+ String.format("Failed to find movie by name %s", name));
+ }
+ title.click();
+ UiObject2 play = mDevice.wait(Until.findObject(By.res(UI_PACKAGE, "play")), 5000);
+ if (play == null) {
+ throw new UnknownUiException("Failed to find the play button.");
+ }
+ play.click();
+ mDevice.waitForIdle();
+ }
+
+ private boolean isNavigationDrawerOpen () {
+ return mDevice.hasObject(By.res(UI_PACKAGE, UI_NAV_DRAWER_ID));
+ }
+
+ private void openNavigationDrawer() {
+ if (isNavigationDrawerOpen()) {
+ return;
+ }
+
+ UiObject2 backButton = mDevice.findObject(By.pkg(getPackage()).desc("Navigate up"));
+ if (backButton != null) {
+ backButton.click();
+ mDevice.wait(Until.findObject(By.desc("Show navigation drawer")), 5000);
+ }
+
+ UiObject2 navButton = mDevice.findObject(By.desc("Show navigation drawer"));
+ if (navButton == null) {
+ throw new UnknownUiException("Unable to find the navigation drawer button.");
+ }
+ navButton.click();
+ waitForNavigationDrawerOpen();
+ }
+
+ private void waitForNavigationDrawerOpen() {
+ mDevice.wait(Until.hasObject(By.text("Settings").clickable(true)), 2500);
+ }
+
+ private void waitForNavigationDrawerClose() {
+ mDevice.wait(Until.gone(By.text("Settings").clickable(true)), 2500);
+ }
+
+ private UiObject2 getMoviesTab() {
+ Pattern moviesText = Pattern.compile("MY MOVIES", Pattern.CASE_INSENSITIVE);
+ UiObject2 tab = mDevice.findObject(By.text(moviesText));
+ if (tab == null) {
+ moviesText = Pattern.compile("MOVIES", Pattern.CASE_INSENSITIVE);
+ tab = mDevice.findObject(By.text(moviesText));
+ }
+ return tab;
+ }
+}
diff --git a/libraries/play-music-app-helper/Android.mk b/libraries/play-music-app-helper/Android.mk
new file mode 100644
index 0000000..b17b528
--- /dev/null
+++ b/libraries/play-music-app-helper/Android.mk
@@ -0,0 +1,23 @@
+#
+# Copyright (C) 2016 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT 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 := play-music-app-helper
+LOCAL_JAVA_LIBRARIES := ub-uiautomator base-app-helpers
+LOCAL_SRC_FILES := $(call all-java-files-under, src)
+
+include $(BUILD_STATIC_JAVA_LIBRARY)
diff --git a/libraries/play-music-app-helper/src/android/platform/test/helpers/PlayMusicHelperImpl.java b/libraries/play-music-app-helper/src/android/platform/test/helpers/PlayMusicHelperImpl.java
new file mode 100644
index 0000000..b051ff1
--- /dev/null
+++ b/libraries/play-music-app-helper/src/android/platform/test/helpers/PlayMusicHelperImpl.java
@@ -0,0 +1,273 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.platform.test.helpers;
+
+import android.app.Instrumentation;
+import android.content.pm.PackageManager.NameNotFoundException;
+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.Until;
+import android.support.test.uiautomator.UiDevice;
+import android.support.test.uiautomator.UiObject2;
+import android.util.Log;
+import android.widget.Button;
+import android.widget.ImageButton;
+import android.widget.TextView;
+
+import java.util.regex.Pattern;
+
+import junit.framework.Assert;
+
+public class PlayMusicHelperImpl extends AbstractPlayMusicHelper {
+ private static final String LOG_TAG = PlayMusicHelperImpl.class.getSimpleName();
+ private static final String UI_PACKAGE = "com.google.android.music";
+
+ private static final String UI_TAB_HEADER_ID = "play_header_list_tab_scroll";
+ private static final String UI_PAUSE_PLAY_BUTTON_ID = "play_pause_header";
+
+ private static final long APP_LOAD_WAIT = 10000;
+ private static final long APP_INIT_WAIT = 10000;
+ private static final long TAB_TRANSITION_WAIT = 5000;
+ private static final long EXPAND_WAIT = 5000;
+ private static final long NAV_BAR_WAIT = 5000;
+ private static final long TOGGLE_PAUSE_PLAY_WAIT = 5000;
+
+ public PlayMusicHelperImpl(Instrumentation instr) {
+ super(instr);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public String getPackage() {
+ return "com.google.android.music";
+ }
+
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public String getLauncherName() {
+ return "Play Music";
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void dismissInitialDialogs() {
+ // Dismiss "Add account" Dialog
+ UiObject2 skipButton = mDevice.wait(Until.findObject(By.res(UI_PACKAGE, "skip_button")),
+ APP_LOAD_WAIT);
+ if (skipButton != null) {
+ skipButton.clickAndWait(Until.newWindow(), APP_INIT_WAIT);
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void goToTab(String tabTitle) {
+ if (isLibraryTabSelected(tabTitle)) {
+ return;
+ } else {
+ // Go to the "Library" page
+ goToMyLibrary();
+
+ for (int retries = 3; retries > 0; retries--) {
+ UiObject2 title = getLibraryTab(tabTitle);
+ if (title != null) {
+ title.click();
+ Assert.assertTrue(
+ String.format("Tab %s was not found selected", tabTitle.toUpperCase()),
+ mDevice.wait(
+ Until.hasObject(getLibraryTabSelector(tabTitle).selected(true)),
+ TAB_TRANSITION_WAIT));
+ } else {
+ UiObject2 headerList = mDevice.findObject(By.res(UI_PACKAGE, UI_TAB_HEADER_ID));
+ Assert.assertNotNull("Could not find library header to scroll.", headerList);
+ headerList.scroll(Direction.RIGHT, 1.0f);
+ }
+ }
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void selectSong(String album, String song) {
+ UiObject2 albumItem = mDevice.wait(Until.findObject(By.res(UI_PACKAGE, "li_title")
+ .textStartsWith(album)), EXPAND_WAIT);
+ Assert.assertNotNull("Unable to find album item", albumItem);
+ albumItem.click();
+
+ mDevice.wait(Until.findObject(By.res(UI_PACKAGE, "title").textStartsWith(album)),
+ EXPAND_WAIT);
+
+ for (int retries = 5; retries > 0; retries--) {
+ UiObject2 songItem = mDevice.findObject(By.res(UI_PACKAGE, "li_title").
+ textStartsWith(song));
+ if (songItem != null) {
+ songItem.click();
+ mDevice.wait(Until.findObject(
+ By.res(UI_PACKAGE, "trackname").textStartsWith(song)), EXPAND_WAIT);
+
+ // Waits for the animation to complete.
+ mDevice.waitForIdle();
+ return;
+ } else {
+ UiObject2 scroller = mDevice.findObject(
+ By.scrollable(true));
+ scroller.setGestureMargin(500);
+ scroller.scroll(Direction.DOWN, 1.0f);
+ }
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void pauseSong() {
+ BySelector selector1play = By.res(UI_PACKAGE, UI_PAUSE_PLAY_BUTTON_ID).desc("Play");
+ BySelector selector1pause = By.res(UI_PACKAGE, UI_PAUSE_PLAY_BUTTON_ID).desc("Pause");
+ BySelector selector2play = By.res(UI_PACKAGE, "pause").desc("Play");
+ BySelector selector2pause = By.res(UI_PACKAGE, "pause").desc("Pause");
+
+ UiObject2 button = null;
+ if ((button = mDevice.findObject(selector1play)) != null) {
+ return;
+ } else if ((button = mDevice.findObject(selector1pause)) != null) {
+ button.click();
+ mDevice.wait(Until.findObject(selector1play), TOGGLE_PAUSE_PLAY_WAIT);
+ } else if ((button = mDevice.findObject(selector2play)) != null) {
+ return;
+ } else if ((button = mDevice.findObject(selector2pause)) != null) {
+ button.click();
+ mDevice.wait(Until.findObject(selector2play), TOGGLE_PAUSE_PLAY_WAIT);
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void playSong() {
+ BySelector selector1play = By.res(UI_PACKAGE, UI_PAUSE_PLAY_BUTTON_ID).desc("Play");
+ BySelector selector1pause = By.res(UI_PACKAGE, UI_PAUSE_PLAY_BUTTON_ID).desc("Pause");
+ BySelector selector2play = By.res(UI_PACKAGE, "pause").desc("Play");
+ BySelector selector2pause = By.res(UI_PACKAGE, "pause").desc("Pause");
+
+ UiObject2 button = null;
+ if ((button = mDevice.findObject(selector1pause)) != null) {
+ return;
+ } else if ((button = mDevice.findObject(selector1play)) != null) {
+ button.click();
+ mDevice.wait(Until.findObject(selector1pause), TOGGLE_PAUSE_PLAY_WAIT);
+ } else if ((button = mDevice.findObject(selector2pause)) != null) {
+ return;
+ } else if ((button = mDevice.findObject(selector2play)) != null) {
+ button.click();
+ mDevice.wait(Until.findObject(selector2pause), TOGGLE_PAUSE_PLAY_WAIT);
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void expandMediaControls() {
+ UiObject2 header = mDevice.findObject(By.res(UI_PACKAGE, "trackname"));
+ Assert.assertNotNull("Unable to find header to expand media controls.", header);
+ header.click();
+ mDevice.wait(Until.findObject(By.res(UI_PACKAGE, "lightsUpInterceptor")), EXPAND_WAIT);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void pressShuffleAll() {
+ if (!isLibraryTabSelected("Songs")) {
+ throw new IllegalStateException("The Songs tab was not selected");
+ }
+
+ UiObject2 shuffleAll = mDevice.findObject(By.text("SHUFFLE ALL"));
+ Assert.assertNotNull("Could not find a 'SHUFFLE ALL' button.", shuffleAll);
+ shuffleAll.click();
+ Assert.assertTrue("Did not detect a song playing", mDevice.wait(Until.hasObject(
+ By.res(UI_PACKAGE, UI_PAUSE_PLAY_BUTTON_ID)), TOGGLE_PAUSE_PLAY_WAIT));
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void pressRepeat() {
+ UiObject2 repeatButton = mDevice.findObject(By.res(UI_PACKAGE, "repeat"));
+ Assert.assertNotNull("Unable to find repeat button to press.", repeatButton);
+ repeatButton.click();
+ mDevice.waitForIdle();
+ }
+
+ private void goToMyLibrary() {
+ // Select for the title: "Library"
+ if (mDevice.findObject(getLibraryTextSelector().clickable(false)) != null) {
+ return;
+ }
+
+ openNavigationBar();
+ // Select for the button: "Library"
+ mDevice.findObject(getLibraryTextSelector().clickable(true)).click();
+ mDevice.wait(Until.gone(By.res(UI_PACKAGE, "play_drawer_root")), NAV_BAR_WAIT);
+ }
+
+ private void openNavigationBar () {
+ UiObject2 navBar = getNavigationBarButton();
+ Assert.assertNotNull("Did not find navigation drawer button.", navBar);
+ navBar.click();
+ mDevice.wait(Until.findObject(By.res(UI_PACKAGE, "play_drawer_root")), NAV_BAR_WAIT);
+ }
+
+ private UiObject2 getNavigationBarButton() {
+ return mDevice.findObject(By.desc("Show navigation drawer"));
+ }
+
+ private boolean isLibraryTabSelected(String tabTitle) {
+ return mDevice.hasObject(getLibraryTabSelector(tabTitle).selected(true));
+ }
+
+ private UiObject2 getLibraryTab(String tabTitle) {
+ return mDevice.findObject(getLibraryTabSelector(tabTitle));
+ }
+
+ private BySelector getLibraryTabSelector(String tabTitle) {
+ return By.res(UI_PACKAGE, "title").text(tabTitle.toUpperCase());
+ }
+
+ private BySelector getLibraryTextSelector() {
+ String libraryText = "Music library";
+ Pattern libraryTextPattern = Pattern.compile(libraryText, Pattern.CASE_INSENSITIVE);
+ return By.text(libraryTextPattern);
+ }
+}
diff --git a/libraries/play-store-app-helper/Android.mk b/libraries/play-store-app-helper/Android.mk
new file mode 100644
index 0000000..0a18e48
--- /dev/null
+++ b/libraries/play-store-app-helper/Android.mk
@@ -0,0 +1,23 @@
+#
+# Copyright (C) 2016 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT 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 := play-store-app-helper
+LOCAL_JAVA_LIBRARIES := ub-uiautomator base-app-helpers
+LOCAL_SRC_FILES := $(call all-java-files-under, src)
+
+include $(BUILD_STATIC_JAVA_LIBRARY)
diff --git a/libraries/play-store-app-helper/src/android/platform/test/helpers/PlayStoreHelperImpl.java b/libraries/play-store-app-helper/src/android/platform/test/helpers/PlayStoreHelperImpl.java
new file mode 100644
index 0000000..2fbbcc5
--- /dev/null
+++ b/libraries/play-store-app-helper/src/android/platform/test/helpers/PlayStoreHelperImpl.java
@@ -0,0 +1,154 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.platform.test.helpers;
+
+import android.app.Instrumentation;
+import android.content.pm.PackageManager.NameNotFoundException;
+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.Until;
+import android.support.test.uiautomator.UiDevice;
+import android.support.test.uiautomator.UiObject2;
+import android.util.Log;
+import android.widget.EditText;
+
+import junit.framework.Assert;
+
+public class PlayStoreHelperImpl extends AbstractPlayStoreHelper {
+ private static final String LOG_TAG = PlayStoreHelperImpl.class.getSimpleName();
+ private static final String UI_PACKAGE = "com.android.vending";
+
+ public PlayStoreHelperImpl(Instrumentation instr) {
+ super(instr);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public String getPackage() {
+ return "com.android.vending";
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public String getLauncherName() {
+ return "Play Store";
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void dismissInitialDialogs() {
+ UiObject2 tos = mDevice.findObject(By.res(UI_PACKAGE, "positive_button"));
+ if (tos != null) {
+ tos.clickAndWait(Until.newWindow(), 5000);
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void doSearch(String query) {
+ // Back up and scroll up until search is visible
+ for (int retries = 3; retries > 0; retries--) {
+ if (getSearchBox() != null) {
+ break;
+ } else {
+ UiObject2 scroller = getScrollContainer();
+ if (scroller != null) {
+ scroller.scroll(Direction.UP, 100.0f);
+ } else {
+ mDevice.pressBack();
+ }
+ }
+ }
+
+ //Interact with the search box
+ UiObject2 searchBox = getSearchBox();
+ if (searchBox != null) {
+ searchBox.click();
+ } else {
+ Assert.fail("Unable to select search box.");
+ }
+ UiObject2 edit = mDevice.wait(
+ Until.findObject(By.clazz(EditText.class)), 5000);
+ Assert.assertNotNull("Could not find edit box", edit);
+ edit.setText(query);
+ mDevice.pressEnter();
+
+ // Wait until the search results container is open
+ Assert.assertTrue("Could not find search results",
+ mDevice.wait(Until.hasObject(By.res(UI_PACKAGE, "search_results_list")), 5000));
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void selectFirstResult() {
+ try {
+ if (getVersion().startsWith("5.")) {
+ expandSection("Apps");
+ }
+ } catch (NameNotFoundException e) {
+ Log.e(LOG_TAG, "Unable to find version for package: " + UI_PACKAGE);
+ }
+ UiObject2 result = mDevice.findObject(By.res(UI_PACKAGE, "play_card"));
+ Assert.assertNotNull("Failed to find a result card", result);
+ result.click();
+ }
+
+ private void expandSection(String header) {
+ for (int retries = 3; retries > 0; retries--) {
+ BySelector section = By.res(UI_PACKAGE, "header_title_main").text(header);
+ UiObject2 title = mDevice.findObject(section);
+ if (title != null) {
+ title.click();
+ mDevice.wait(Until.gone(section), 5000);
+ return;
+ } else {
+ UiObject2 container = mDevice.findObject(By.res(UI_PACKAGE, "search_results_list"));
+ container.scroll(Direction.DOWN, 1.0f);
+ }
+ }
+ Assert.fail("Failed to find section header.");
+ }
+
+ private UiObject2 getSearchBox() {
+ UiObject2 searchBox = mDevice.findObject(By.res(UI_PACKAGE, "search_box_idle_text"));
+ if (searchBox == null) {
+ searchBox = mDevice.findObject(By.res(UI_PACKAGE, "search_button"));
+ }
+ return searchBox;
+ }
+
+ private UiObject2 getScrollContainer() {
+ UiObject2 scroller = mDevice.findObject(By.res(UI_PACKAGE, "recycler_view"));
+ if (scroller == null) {
+ scroller = mDevice.findObject(By.res(UI_PACKAGE, "viewpager"));
+ }
+ return scroller;
+ }
+}
+
diff --git a/libraries/recents-app-helper/Android.mk b/libraries/recents-app-helper/Android.mk
new file mode 100644
index 0000000..b565ad8
--- /dev/null
+++ b/libraries/recents-app-helper/Android.mk
@@ -0,0 +1,23 @@
+#
+# Copyright (C) 2016 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT 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 := recents-app-helper
+LOCAL_JAVA_LIBRARIES := ub-uiautomator base-app-helpers
+LOCAL_SRC_FILES := $(call all-java-files-under, src)
+
+include $(BUILD_STATIC_JAVA_LIBRARY)
diff --git a/libraries/recents-app-helper/src/android/platform/test/helpers/RecentsHelperImpl.java b/libraries/recents-app-helper/src/android/platform/test/helpers/RecentsHelperImpl.java
new file mode 100644
index 0000000..58e39ae
--- /dev/null
+++ b/libraries/recents-app-helper/src/android/platform/test/helpers/RecentsHelperImpl.java
@@ -0,0 +1,102 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.platform.test.helpers;
+
+import android.app.Instrumentation;
+import android.os.RemoteException;
+import android.os.SystemClock;
+import android.support.test.uiautomator.By;
+import android.support.test.uiautomator.Direction;
+import android.support.test.uiautomator.UiObject2;
+import android.support.test.uiautomator.Until;
+import android.util.Log;
+import android.widget.EditText;
+
+import junit.framework.Assert;
+
+public class RecentsHelperImpl extends AbstractRecentsHelper {
+ private static final String LOG_TAG = RecentsHelperImpl.class.getSimpleName();
+ private static final String UI_PACKAGE = "com.android.systemui";
+
+ private static final long RECENTS_SELECTION_TIMEOUT = 5000;
+
+ public RecentsHelperImpl(Instrumentation instr) {
+ super(instr);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public String getPackage() {
+ throw new UnsupportedOperationException("This method is not supported for Recents");
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public String getLauncherName() {
+ throw new UnsupportedOperationException("This method is not supported for Recents");
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void open() {
+ try {
+ mDevice.pressRecentApps();
+ mDevice.waitForIdle();
+ } catch (RemoteException ex) {
+ Log.e(LOG_TAG, ex.toString());
+ }
+ }
+
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void exit() {
+ mDevice.pressHome();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void dismissInitialDialogs() {
+ // Nothing to do.
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void flingRecents(Direction dir) {
+ UiObject2 recentsScroller = getRecentsScroller();
+ Assert.assertNotNull("Unable to find scrolling mechanism for Recents", recentsScroller);
+ recentsScroller.setGestureMargin(recentsScroller.getVisibleBounds().height() / 4);
+ recentsScroller.fling(dir);
+ }
+
+ private UiObject2 getRecentsScroller() {
+ return mDevice.wait(Until.findObject(By.res(UI_PACKAGE, "recents_view")),
+ RECENTS_SELECTION_TIMEOUT);
+ }
+}
diff --git a/libraries/reddit-app-helper/Android.mk b/libraries/reddit-app-helper/Android.mk
new file mode 100644
index 0000000..1ad7af7
--- /dev/null
+++ b/libraries/reddit-app-helper/Android.mk
@@ -0,0 +1,23 @@
+#
+# Copyright (C) 2016 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT 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 := reddit-app-helper
+LOCAL_JAVA_LIBRARIES := ub-uiautomator base-app-helpers
+LOCAL_SRC_FILES := $(call all-java-files-under, src)
+
+include $(BUILD_STATIC_JAVA_LIBRARY)
diff --git a/libraries/reddit-app-helper/src/android/platform/test/helpers/RedditHelperImpl.java b/libraries/reddit-app-helper/src/android/platform/test/helpers/RedditHelperImpl.java
new file mode 100644
index 0000000..d6e2784
--- /dev/null
+++ b/libraries/reddit-app-helper/src/android/platform/test/helpers/RedditHelperImpl.java
@@ -0,0 +1,143 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.platform.test.helpers;
+
+import android.app.Instrumentation;
+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.Until;
+import android.support.test.uiautomator.UiDevice;
+import android.support.test.uiautomator.UiObject2;
+import android.util.Log;
+
+import junit.framework.Assert;
+
+/**
+ * UI test helper for Reddit: The Official App (package: com.reddit.frontpage)
+ */
+
+public class RedditHelperImpl extends AbstractRedditHelper {
+ private static final String TAG = RedditHelperImpl.class.getSimpleName();
+
+ private static final String UI_COMMENTS_PAGE_SCROLL_CONTAINER_ID = "detail_list";
+ private static final String UI_FRONT_PAGE_SCROLL_CONTAINER_ID = "link_list";
+ private static final String UI_LINK_TITLE_ID = "link_title";
+ private static final String UI_PACKAGE_NAME = "com.reddit.frontpage";
+ private static final String UI_REDDIT_WORDMARK_ID = "reddit_wordmark";
+ private static final String UI_SAVE_BUTTON_ID = "action_save";
+
+ private static final long UI_NAVIGATION_WAIT = 5000; // 5 secs
+
+ public RedditHelperImpl(Instrumentation instr) {
+ super(instr);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public String getPackage() {
+ return UI_PACKAGE_NAME;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public String getLauncherName() {
+ return "Reddit";
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void dismissInitialDialogs() {
+
+ }
+
+ private UiObject2 getRedditWordmark() {
+ return mDevice.findObject(By.res(UI_PACKAGE_NAME, UI_REDDIT_WORDMARK_ID));
+ }
+
+ private UiObject2 getFrontPageScrollContainer() {
+ return mDevice.findObject(By.res(UI_PACKAGE_NAME, UI_FRONT_PAGE_SCROLL_CONTAINER_ID));
+ }
+
+ private UiObject2 getFirstArticleTitle() {
+ return mDevice.findObject(By.res(UI_PACKAGE_NAME, UI_LINK_TITLE_ID));
+ }
+
+ private UiObject2 getSaveButton() {
+ return mDevice.findObject(By.res(UI_PACKAGE_NAME, UI_SAVE_BUTTON_ID));
+ }
+
+ private UiObject2 getCommentPageScrollContainer() {
+ return mDevice.findObject(By.res(UI_PACKAGE_NAME, UI_COMMENTS_PAGE_SCROLL_CONTAINER_ID));
+ }
+
+ private boolean isOnFrontPage() {
+ return (getRedditWordmark() != null);
+ }
+
+ private boolean isOnCommentsPage() {
+ return (getSaveButton() != null);
+ }
+
+ public void goToFrontPage() {
+ for (int retriesRemaining = 5; retriesRemaining > 0 && !isOnFrontPage();
+ --retriesRemaining) {
+ mDevice.pressBack();
+ mDevice.waitForIdle();
+ }
+ }
+
+ public void goToFirstArticleComments() {
+ Assert.assertTrue("Not on front page", isOnFrontPage());
+
+ UiObject2 articleTitle = getFirstArticleTitle();
+ Assert.assertNotNull("Could not find first article", articleTitle);
+
+ articleTitle.click();
+ mDevice.wait(Until.hasObject(By.res(UI_PACKAGE_NAME, UI_SAVE_BUTTON_ID)),
+ UI_NAVIGATION_WAIT);
+ }
+
+ public boolean scrollFrontPage(Direction direction, float percent) {
+ Assert.assertTrue("Not on front page", isOnFrontPage());
+ Assert.assertTrue("Scroll direction must be UP or DOWN",
+ Direction.UP.equals(direction) || Direction.DOWN.equals(direction));
+
+ UiObject2 scrollContainer = getFrontPageScrollContainer();
+ Assert.assertNotNull("Could not find front page scroll container", scrollContainer);
+
+ return scrollContainer.scroll(direction, percent);
+ }
+
+ public boolean scrollCommentPage(Direction direction, float percent) {
+ Assert.assertTrue("Not on comment page", isOnCommentsPage());
+ Assert.assertTrue("Scroll direction must be UP or DOWN",
+ Direction.UP.equals(direction) || Direction.DOWN.equals(direction));
+
+ UiObject2 scrollContainer = getCommentPageScrollContainer();
+ Assert.assertNotNull("Could not find comment page scroll container", scrollContainer);
+
+ return scrollContainer.scroll(direction, percent);
+ }
+}
diff --git a/libraries/settings-app-helper/Android.mk b/libraries/settings-app-helper/Android.mk
new file mode 100644
index 0000000..462e592
--- /dev/null
+++ b/libraries/settings-app-helper/Android.mk
@@ -0,0 +1,23 @@
+#
+# Copyright (C) 2016 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT 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 := settings-app-helper
+LOCAL_JAVA_LIBRARIES := ub-uiautomator base-app-helpers
+LOCAL_SRC_FILES := $(call all-java-files-under, src)
+
+include $(BUILD_STATIC_JAVA_LIBRARY)
diff --git a/libraries/settings-app-helper/src/android/platform/test/helpers/SettingsHelperImpl.java b/libraries/settings-app-helper/src/android/platform/test/helpers/SettingsHelperImpl.java
new file mode 100644
index 0000000..206c910
--- /dev/null
+++ b/libraries/settings-app-helper/src/android/platform/test/helpers/SettingsHelperImpl.java
@@ -0,0 +1,218 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.platform.test.helpers;
+
+import android.app.Instrumentation;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.Intent;
+import android.provider.Settings;
+import android.provider.Settings.SettingNotFoundException;
+import android.support.test.uiautomator.By;
+import android.support.test.uiautomator.BySelector;
+import android.support.test.uiautomator.Direction;
+import android.support.test.uiautomator.UiObject2;
+import android.support.test.uiautomator.Until;
+import android.platform.test.helpers.AbstractSettingsHelper;
+import android.util.Log;
+
+import junit.framework.Assert;
+
+import java.util.regex.Pattern;
+
+public class SettingsHelperImpl extends AbstractSettingsHelper {
+
+ private static final int SETTINGS_DASH_TIMEOUT = 3000;
+ private static final String UI_PACKAGE_NAME = "com.android.settings";
+ private static final BySelector SETTINGS_DASHBOARD = By.res(UI_PACKAGE_NAME,
+ "dashboard_container");
+ private static final int TIMEOUT = 2000;
+ private static final String LOG_TAG = SettingsHelperImpl.class.getSimpleName();
+
+ private ContentResolver mResolver;
+
+ public static enum SettingsType {
+ SYSTEM,
+ SECURE,
+ GLOBAL
+ }
+
+ public SettingsHelperImpl(Instrumentation instr) {
+ super(instr);
+ mResolver = instr.getContext().getContentResolver();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public String getPackage() {
+ return "com.android.settings";
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public String getLauncherName() {
+ return "Settings";
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void dismissInitialDialogs() {
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void scrollThroughSettings(int numberOfFlings) throws Exception {
+ UiObject2 settingsList = loadAllSettings();
+ int count = 0;
+ while (count <= numberOfFlings && settingsList.fling(Direction.DOWN)) {
+ count++;
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void flingSettingsToStart() throws Exception {
+ UiObject2 settingsList = loadAllSettings();
+ while (settingsList.fling(Direction.UP));
+ }
+
+ public static void launchSettingsPage(Context ctx, String pageName) throws Exception {
+ Intent intent = new Intent(pageName);
+ intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ ctx.startActivity(intent);
+ Thread.sleep(TIMEOUT * 2);
+ }
+
+ public void scrollVert(boolean isUp) {
+ int w = mDevice.getDisplayWidth();
+ int h = mDevice.getDisplayHeight();
+ mDevice.swipe(w / 2, h / 2, w / 2, isUp ? h : 0, 2);
+ }
+
+ /**
+ * On N, the settingsDashboard is initially collapsed, and the user can see the "See all"
+ * element. On hitting "See all", the same settings dashboard element is now scrollable. For
+ * pre-N, the settings Dashboard is always scrollable, hence the check in the while loop. All
+ * this method does is expand the Settings list if needed, before returning the element.
+ */
+ private UiObject2 loadAllSettings() throws Exception {
+ UiObject2 settingsDashboard = mDevice.wait(Until.findObject(SETTINGS_DASHBOARD),
+ SETTINGS_DASH_TIMEOUT);
+ Assert.assertNotNull("Could not find the settings dashboard object.", settingsDashboard);
+ int count = 0;
+ while (!settingsDashboard.isScrollable() && count <= 2) {
+ mDevice.wait(Until.findObject(By.text("SEE ALL")), SETTINGS_DASH_TIMEOUT).click();
+ settingsDashboard = mDevice.wait(Until.findObject(SETTINGS_DASHBOARD),
+ SETTINGS_DASH_TIMEOUT);
+ count++;
+ }
+ return settingsDashboard;
+ }
+
+ public void clickSetting(String settingName) throws InterruptedException {
+ mDevice.wait(Until.findObject(By.text(settingName)), TIMEOUT).click();
+ Thread.sleep(400);
+ }
+
+ public void clickSetting(Pattern settingName) throws InterruptedException {
+ mDevice.wait(Until.findObject(By.text(settingName)), TIMEOUT).click();
+ Thread.sleep(400);
+ }
+
+ public boolean verifyToggleSetting(SettingsType type, String settingAction,
+ String settingName, String internalName) throws Exception {
+ return verifyToggleSetting(
+ type, settingAction, Pattern.compile(settingName), internalName, true);
+ }
+
+ public boolean verifyToggleSetting(SettingsType type, String settingAction,
+ Pattern settingName, String internalName) throws Exception {
+ return verifyToggleSetting(type, settingAction, settingName, internalName, true);
+ }
+
+ public boolean verifyToggleSetting(SettingsType type, String settingAction,
+ String settingName, String internalName, boolean doLaunch) throws Exception {
+ return verifyToggleSetting(
+ type, settingAction, Pattern.compile(settingName), internalName, doLaunch);
+ }
+
+ public boolean verifyToggleSetting(SettingsType type, String settingAction,
+ Pattern settingName, String internalName, boolean doLaunch) throws Exception {
+ String onSettingBaseVal = getStringSetting(type, internalName);
+ if (onSettingBaseVal == null) {
+ onSettingBaseVal = "0";
+ }
+ int onSetting = Integer.parseInt(onSettingBaseVal);
+ Log.d(null, "On Setting value is : " + onSetting);
+ if (doLaunch) {
+ launchSettingsPage(mInstrumentation.getContext(), settingAction);
+ }
+ clickSetting(settingName);
+ Log.d(null, "Clicked setting : " + settingName);
+ Thread.sleep(1000);
+ String changedSetting = getStringSetting(type, internalName);
+ Log.d(null, "Changed Setting value is : " + changedSetting);
+ if (changedSetting == null) {
+ Log.d(null, "Changed Setting value is : NULL");
+ changedSetting = "0";
+ }
+ return (1 - onSetting) == Integer.parseInt(changedSetting);
+ }
+
+ public boolean verifyRadioSetting(SettingsType type, String settingAction,
+ String baseName, String settingName,
+ String internalName, String testVal) throws Exception {
+ if (baseName != null) clickSetting(baseName);
+ clickSetting(settingName);
+ Thread.sleep(500);
+ return getStringSetting(type, internalName).equals(testVal);
+ }
+
+ private String getStringSetting(SettingsType type, String sName) {
+ switch (type) {
+ case SYSTEM:
+ return Settings.System.getString(mResolver, sName);
+ case GLOBAL:
+ return Settings.Global.getString(mResolver, sName);
+ case SECURE:
+ return Settings.Secure.getString(mResolver, sName);
+ }
+ return "";
+ }
+
+ private int getIntSetting(SettingsType type, String sName) throws SettingNotFoundException {
+ switch (type) {
+ case SYSTEM:
+ return Settings.System.getInt(mResolver, sName);
+ case GLOBAL:
+ return Settings.Global.getInt(mResolver, sName);
+ case SECURE:
+ return Settings.Secure.getInt(mResolver, sName);
+ }
+ return Integer.MIN_VALUE;
+ }
+}
diff --git a/libraries/tunein-app-helper/Android.mk b/libraries/tunein-app-helper/Android.mk
new file mode 100644
index 0000000..bf4c2a3
--- /dev/null
+++ b/libraries/tunein-app-helper/Android.mk
@@ -0,0 +1,23 @@
+#
+# Copyright (C) 2016 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT 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 := tunein-app-helper
+LOCAL_JAVA_LIBRARIES := ub-uiautomator base-app-helpers
+LOCAL_SRC_FILES := $(call all-java-files-under, src)
+
+include $(BUILD_STATIC_JAVA_LIBRARY)
diff --git a/libraries/tunein-app-helper/src/android/platform/test/helpers/TuneInHelperImpl.java b/libraries/tunein-app-helper/src/android/platform/test/helpers/TuneInHelperImpl.java
new file mode 100644
index 0000000..f92476a
--- /dev/null
+++ b/libraries/tunein-app-helper/src/android/platform/test/helpers/TuneInHelperImpl.java
@@ -0,0 +1,194 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.platform.test.helpers;
+
+import android.app.Instrumentation;
+import android.platform.test.helpers.exceptions.UnknownUiException;
+import android.support.test.uiautomator.By;
+import android.support.test.uiautomator.BySelector;
+import android.support.test.uiautomator.Until;
+import android.support.test.uiautomator.UiDevice;
+import android.support.test.uiautomator.UiObject2;
+import android.util.Log;
+import junit.framework.Assert;
+
+public class TuneInHelperImpl extends AbstractTuneInHelper {
+ private static final String TAG = TuneInHelperImpl.class.getCanonicalName();
+
+ private static final String UI_PACKAGE_NAME = "tunein.player";
+ private static final long UI_ACTION_TIMEOUT = 5000;
+ private static final int MAX_BACK_ATTEMPTS = 5;
+
+ private static final String UI_LOCAL_RADIO_TEXT = "Local Radio";
+ private static final String UI_FM_LIST_ID = "view_model_list";
+ private static final String UI_START_PLAY_ID = "profile_primary_button";
+ private static final String UI_MINI_PLAYER_PLAY_ID = "mini_player_play";
+ private static final String UI_MINI_PLAYER_STOP_ID = "mini_player_stop";
+
+ public TuneInHelperImpl(Instrumentation instr) {
+ super(instr);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public String getPackage() {
+ return UI_PACKAGE_NAME;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public String getLauncherName() {
+ return "TuneIn Radio";
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void dismissInitialDialogs() {
+
+ }
+
+ private boolean isOnBrowsePage() {
+ return mDevice.hasObject(By.text("Browse"));
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void goToBrowsePage() {
+ for (int tries = MAX_BACK_ATTEMPTS; tries > 0; tries--) {
+ if (isOnBrowsePage()) {
+ break;
+ }
+ mDevice.pressBack();
+ mDevice.waitForIdle();
+ }
+ if (!isOnBrowsePage()) {
+ throw new IllegalStateException("Fail to go to Browse Page");
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void goToLocalRadio() {
+ if (!isOnBrowsePage()) {
+ throw new IllegalStateException("Not on Browse Page");
+ }
+
+ UiObject2 localRadio = mDevice.findObject(By.text(UI_LOCAL_RADIO_TEXT));
+
+ if (localRadio == null) {
+ throw new UnknownUiException("Cannot not find local radio");
+ }
+ else {
+ if (!localRadio.clickAndWait(Until.newWindow(), UI_ACTION_TIMEOUT)) {
+ throw new UnknownUiException("Fail to load Local Radio page");
+ }
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void selectFM(int i) {
+ UiObject2 fmList = mDevice.wait(
+ Until.findObject(By.res(UI_PACKAGE_NAME, UI_FM_LIST_ID)),
+ UI_ACTION_TIMEOUT
+ );
+
+ if (fmList == null) {
+ throw new UnknownUiException("Cannot not find fm list to select FM");
+ }
+
+ if (i <= 0 && i >= fmList.getChildren().size()) {
+ String errMsg = String.format("Trying to select %dth FM radio, valid range = (1, %d)",
+ i, fmList.getChildren().size() - 1);
+ throw new IllegalArgumentException(errMsg);
+ }
+
+ UiObject2 fm = fmList.getChildren().get(i);
+
+ if (!fm.clickAndWait(Until.newWindow(), UI_ACTION_TIMEOUT)) {
+ throw new UnknownUiException("Fail to load into fm profile page");
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void startChannel() {
+ if (isOnFeedbackScreen()) {
+ dismissFeedbackScreen();
+ }
+
+ UiObject2 start = mDevice
+ .findObject(By.res(UI_PACKAGE_NAME, UI_START_PLAY_ID));
+
+ if (start == null) {
+ throw new UnknownUiException("Cannot find start play button");
+ }
+
+ if (!start.clickAndWait(Until.newWindow(), UI_ACTION_TIMEOUT)) {
+ throw new UnknownUiException("Fail to start playing the fm");
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void stopChannel() {
+ if (isOnFeedbackScreen()) {
+ dismissFeedbackScreen();
+ }
+
+ UiObject2 stop = mDevice
+ .findObject(By.res(UI_PACKAGE_NAME, UI_MINI_PLAYER_STOP_ID));
+
+ if (stop == null) {
+ throw new UnknownUiException("Could not find stop button");
+ }
+
+ stop.click();
+
+ if (!stop.wait(Until.enabled(!stop.isEnabled()), UI_ACTION_TIMEOUT)) {
+ throw new UnknownUiException("Fail to stop playing the fm");
+ }
+ }
+
+ private boolean isOnFeedbackScreen() {
+ return mDevice.hasObject(By.text("Do you love TuneIn Radio?"));
+ }
+
+ private void dismissFeedbackScreen() {
+ UiObject2 button = mDevice.findObject(By.text("MAYBE LATER"));
+
+ if (button != null) {
+ button.click();
+ }
+ }
+
+}
diff --git a/libraries/youtube-app-helper/Android.mk b/libraries/youtube-app-helper/Android.mk
new file mode 100644
index 0000000..10fb921
--- /dev/null
+++ b/libraries/youtube-app-helper/Android.mk
@@ -0,0 +1,23 @@
+#
+# Copyright (C) 2016 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT 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 := youtube-app-helper
+LOCAL_JAVA_LIBRARIES := ub-uiautomator base-app-helpers
+LOCAL_SRC_FILES := $(call all-java-files-under, src)
+
+include $(BUILD_STATIC_JAVA_LIBRARY)
diff --git a/libraries/youtube-app-helper/src/android/platform/test/helpers/YouTubeHelperImpl.java b/libraries/youtube-app-helper/src/android/platform/test/helpers/YouTubeHelperImpl.java
new file mode 100644
index 0000000..2750f47
--- /dev/null
+++ b/libraries/youtube-app-helper/src/android/platform/test/helpers/YouTubeHelperImpl.java
@@ -0,0 +1,493 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.platform.test.helpers;
+
+import android.app.Instrumentation;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.content.res.Configuration;
+import android.graphics.Point;
+import android.os.SystemClock;
+import android.platform.test.helpers.exceptions.UiTimeoutException;
+import android.platform.test.helpers.exceptions.UnknownUiException;
+import android.support.test.uiautomator.By;
+import android.support.test.uiautomator.BySelector;
+import android.support.test.uiautomator.Direction;
+import android.support.test.uiautomator.Until;
+import android.support.test.uiautomator.UiDevice;
+import android.support.test.uiautomator.UiObject2;
+import android.util.Log;
+
+import java.util.regex.Pattern;
+
+public class YouTubeHelperImpl extends AbstractYouTubeHelper {
+ private static final String TAG = AbstractYouTubeHelper.class.getSimpleName();
+
+ private static final String UI_ACCOUNT_BUTTON_DESC = "Account";
+ private static final String UI_HOME_CONTAINER_ID = "results";
+ private static final String UI_FULLSCREEN_BUTTON_DESC = "Enter fullscreen";
+ private static final String UI_HELP_AND_FEEDBACK_TEXT = "Help & feedback";
+ private static final String UI_HOME_BUTTON_DESC = "Home";
+ private static final String UI_HOME_PAGE_VIDEO_ID = "event_item";
+ private static final String UI_VIDEO_INFO_VIEW_ID = "video_info_view";
+ private static final String UI_PACKAGE_NAME = "com.google.android.youtube";
+ private static final String UI_PLAY_VIDEO_DESC = "Play video";
+ private static final String UI_PROGRESS_ID = "load_progress";
+ private static final String UI_RESULT_FILTER_ID = "menu_filter_results";
+ private static final String UI_SEARCH_BUTTON_ID = "menu_search";
+ private static final String UI_SEARCH_EDIT_TEXT_ID = "search_edit_text";
+ private static final String UI_SELECT_DIALOG_LISTVIEW_ID = "select_dialog_listview";
+ private static final String UI_TRENDING_BUTTON_DESC = "Trending";
+ private static final String UI_VIDEO_PLAYER_ID = "watch_player";
+ private static final String UI_VIDEO_PLAYER_OVERFLOW_BUTTON_ID = "player_overflow_button";
+ private static final String UI_VIDEO_PLAYER_PLAY_PAUSE_REPLAY_BUTTON_ID =
+ "player_control_play_pause_replay_button";
+ private static final String UI_VIDEO_PLAYER_QUALITY_BUTTON_ID = "quality_button";
+
+ private static final long MAX_HOME_LOAD_WAIT = 30 * 1000;
+ private static final long MAX_VIDEO_LOAD_WAIT = 30 * 1000;
+
+ private static final long APP_INIT_WAIT = 20000;
+ private static final long STANDARD_DIALOG_WAIT = 5000;
+ private static final long UI_NAVIGATION_WAIT = 5000;
+
+ public YouTubeHelperImpl(Instrumentation instr) {
+ super(instr);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public String getPackage() {
+ return "com.google.android.youtube";
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public String getLauncherName() {
+ return "YouTube";
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void dismissInitialDialogs() {
+ BySelector dialog1 = By.text("OK");
+ // Dismiss the splash screen that might appear on first start.
+ UiObject2 splash = mDevice.wait(Until.findObject(dialog1), APP_INIT_WAIT);
+ if (splash != null) {
+ splash.click();
+ mDevice.wait(Until.gone(dialog1), STANDARD_DIALOG_WAIT);
+ }
+
+ UiObject2 laterButton = mDevice.wait(Until.findObject(
+ By.res(UI_PACKAGE_NAME, "later_button")), STANDARD_DIALOG_WAIT);
+ if (laterButton != null) {
+ laterButton.clickAndWait(Until.newWindow(), STANDARD_DIALOG_WAIT);
+ }
+
+ UiObject2 helpAndFeedbackButton = mDevice.findObject(
+ By.pkg(UI_PACKAGE_NAME).text(UI_HELP_AND_FEEDBACK_TEXT));
+ if (helpAndFeedbackButton != null) {
+ mDevice.pressBack();
+ mDevice.wait(Until.gone(By.pkg(UI_PACKAGE_NAME).text(UI_HELP_AND_FEEDBACK_TEXT)),
+ STANDARD_DIALOG_WAIT);
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void playHomePageVideo() {
+ if (!isOnHomePage()) {
+ throw new IllegalStateException("YouTube is not on the home page.");
+ }
+
+ if (hasConnectionEstablishedMessage()) {
+ pressGoOnline();
+ }
+
+ for (int i = 0; i < 3; i++) {
+ UiObject2 video = getPlayableVideo();
+ if (video != null) {
+ video.click();
+ waitForVideoToLoad(UI_NAVIGATION_WAIT);
+ return;
+ } else {
+ scrollHomePage(Direction.DOWN);
+ }
+ }
+
+ if (isLoading()) {
+ throw new UiTimeoutException("Timed out waiting for video search results.");
+ }
+
+ throw new UnknownUiException("Unsuccessful attempt playing home page video.");
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void playSearchResultPageVideo() {
+ if (!isOnSearchResultsPage()) {
+ throw new IllegalStateException("YouTube is not on the home page.");
+ }
+
+ for (int i = 0; i < 3; i++) {
+ UiObject2 video = getPlayableVideo();
+ if (video != null) {
+ video.click();
+ waitForVideoToLoad(UI_NAVIGATION_WAIT);
+ return;
+ } else {
+ scrollSearchResultsPage(Direction.DOWN);
+ }
+ }
+
+ throw new UnknownUiException("Unsuccessful attempt playing search result video.");
+ }
+
+ private UiObject2 getHomePageContainer() {
+ return mDevice.findObject(By.res(UI_PACKAGE_NAME, UI_HOME_CONTAINER_ID));
+ }
+
+ private UiObject2 getSearchResultsPageContainer() {
+ return getHomePageContainer();
+ }
+
+ private UiObject2 getHomeButton() {
+ return mDevice.findObject(By.pkg(UI_PACKAGE_NAME).desc(UI_HOME_BUTTON_DESC));
+ }
+
+ private UiObject2 getTrendingButton() {
+ return mDevice.findObject(By.pkg(UI_PACKAGE_NAME).desc(UI_TRENDING_BUTTON_DESC));
+ }
+
+ private UiObject2 getAccountButton() {
+ return mDevice.findObject(By.pkg(UI_PACKAGE_NAME).desc(UI_ACCOUNT_BUTTON_DESC));
+ }
+
+ private UiObject2 getSearchButton() {
+ return mDevice.findObject(By.res(UI_PACKAGE_NAME, UI_SEARCH_BUTTON_ID));
+ }
+
+ private void scrollHomePage(Direction dir) {
+ if (dir == Direction.RIGHT || dir == Direction.LEFT) {
+ throw new IllegalArgumentException("Can only scroll up and down.");
+ }
+
+ UiObject2 scrollContainer = getHomePageContainer();
+ if (scrollContainer != null) {
+ scrollContainer.scroll(dir, 1.0f);
+ mDevice.waitForIdle();
+ } else {
+ throw new UnknownUiException("No scrolling mechanism found.");
+ }
+ }
+
+ private void scrollSearchResultsPage(Direction dir) {
+ if (dir == Direction.RIGHT || dir == Direction.LEFT) {
+ throw new IllegalArgumentException("Can only scroll up and down.");
+ }
+
+ UiObject2 scrollContainer = getSearchResultsPageContainer();
+ if (scrollContainer != null) {
+ scrollContainer.scroll(dir, 1.0f);
+ mDevice.waitForIdle();
+ } else {
+ throw new UnknownUiException("No scrolling mechanism found.");
+ }
+ }
+
+ private boolean isLoading() {
+ // TODO: Is loading what? Requires more documentation.
+ return mDevice.hasObject(By.res(UI_PACKAGE_NAME, UI_PROGRESS_ID));
+ }
+
+ private boolean isOnHomePage() {
+ UiObject2 homeButton = getHomeButton();
+ return (homeButton != null && homeButton.isSelected());
+ }
+
+ private boolean isOnTrendingPage() {
+ UiObject2 trendingButton = getTrendingButton();
+ return (trendingButton != null && trendingButton.isSelected());
+ }
+
+ private boolean isOnAccountPage() {
+ UiObject2 accountButton = getAccountButton();
+ return (accountButton != null && accountButton.isSelected());
+ }
+
+ private boolean isOnSearchResultsPage() {
+ // Simplest way to identify search result page is the result filter button.
+ UiObject2 resultFilterButton =
+ mDevice.findObject(By.res(UI_PACKAGE_NAME, UI_RESULT_FILTER_ID));
+ return (resultFilterButton != null);
+ }
+
+ private UiObject2 getPlayableVideo() {
+ UiObject2 video = mDevice.findObject(By.res(UI_PACKAGE_NAME, UI_HOME_PAGE_VIDEO_ID));
+ if (video == null) {
+ video = mDevice.findObject(By.res(UI_PACKAGE_NAME, UI_VIDEO_INFO_VIEW_ID));
+ }
+ return video;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public boolean waitForVideoToLoad(long timeout) {
+ return mDevice.wait(Until.hasObject(
+ By.res(UI_PACKAGE_NAME, UI_VIDEO_PLAYER_ID)), timeout);
+ }
+
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void goToHomePage() {
+ for (int retriesRemaining = 5; retriesRemaining > 0 && getHomeButton() == null &&
+ getTrendingButton() == null && getAccountButton() == null; --retriesRemaining) {
+ mDevice.pressBack();
+ SystemClock.sleep(3000);
+ }
+ // Get and press the home button
+ UiObject2 homeButton = getHomeButton();
+ if (homeButton == null) {
+ throw new UnknownUiException("Could not find home button.");
+ } else if (!homeButton.isSelected()) {
+ homeButton.click();
+ // Validate the home button is selected
+ if (!mDevice.wait(Until.hasObject(
+ By.pkg(UI_PACKAGE_NAME).desc(UI_HOME_BUTTON_DESC).selected(true)),
+ UI_NAVIGATION_WAIT)) {
+ throw new UnknownUiException("Not on home page after pressing home button.");
+ } else {
+ // Make sure the transition is complete
+ mDevice.waitForIdle();
+ }
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void goToSearchPage() {
+ if (!isOnHomePage()) {
+ throw new IllegalStateException("YouTube is not on the home page.");
+ }
+
+ UiObject2 searchButton = getSearchButton();
+ if (searchButton == null) {
+ throw new UnknownUiException("Could not find search button.");
+ } else {
+ searchButton.click();
+ if (!mDevice.wait(Until.hasObject(
+ By.res(UI_PACKAGE_NAME, UI_SEARCH_EDIT_TEXT_ID)), UI_NAVIGATION_WAIT)) {
+ throw new UnknownUiException("Not on search page after pressing search button.");
+ }
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void goToFullscreenMode() {
+ if (!isOnVideo()) {
+ throw new IllegalStateException("YouTube is not on a video page.");
+ }
+
+ if (getOrientation() == Configuration.ORIENTATION_LANDSCAPE) {
+ return;
+ }
+
+ UiObject2 fullscreenButton = null;
+ for (int retriesRemaining = 5; retriesRemaining > 0; --retriesRemaining) {
+ UiObject2 miniVideoPlayer = getVideoPlayer();
+ if (miniVideoPlayer == null) {
+ throw new UnknownUiException("Could not find mini video player.");
+ }
+
+ miniVideoPlayer.click();
+ SystemClock.sleep(1500);
+ fullscreenButton = getFullscreenButton();
+ if (fullscreenButton != null) {
+ fullscreenButton.click();
+ // TODO: Add a valid wait for fullscreen
+ break;
+ }
+ }
+
+ if (fullscreenButton == null) {
+ throw new UnknownUiException("Did not find a fullscreen button.");
+ }
+ }
+
+ private UiObject2 getVideoPlayer() {
+ return mDevice.findObject(By.res(UI_PACKAGE_NAME, UI_VIDEO_PLAYER_ID));
+ }
+
+ private boolean isOnVideo() {
+ return (getVideoPlayer() != null);
+ }
+
+ private UiObject2 getVideoPlayerOverflowButton() {
+ return mDevice.findObject(By.res(UI_PACKAGE_NAME, UI_VIDEO_PLAYER_OVERFLOW_BUTTON_ID));
+ }
+
+ private UiObject2 getVideoPlayerQualityButton() {
+ UiObject2 videoPlayer = getVideoPlayer();
+ UiObject2 qualityButton = null;
+
+ if (videoPlayer != null) {
+ qualityButton = mDevice.findObject(
+ By.res(UI_PACKAGE_NAME, UI_VIDEO_PLAYER_QUALITY_BUTTON_ID));
+ }
+ return qualityButton;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public boolean waitForSearchResults(long timeout) {
+ return mDevice.wait(Until.hasObject(
+ By.res(UI_PACKAGE_NAME, UI_VIDEO_INFO_VIEW_ID)), timeout);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void setVideoQuality(VideoQuality quality) {
+ if (!isOnVideo()) {
+ throw new IllegalStateException("YouTube is not on a video page.");
+ }
+
+ UiObject2 overflowButton = getVideoPlayerOverflowButton();
+ // Open the mini video player
+ if (overflowButton == null) {
+ UiObject2 miniVideoPlayer = getVideoPlayer();
+ if (miniVideoPlayer == null) {
+ throw new UnknownUiException("Could not find mini video player.");
+ }
+
+ miniVideoPlayer.click();
+ mDevice.wait(Until.findObject(By.res(
+ UI_PACKAGE_NAME, UI_VIDEO_PLAYER_OVERFLOW_BUTTON_ID)), UI_NAVIGATION_WAIT);
+ overflowButton = getVideoPlayerOverflowButton();
+ }
+
+ if (overflowButton == null) {
+ throw new UnknownUiException("Could not find overflow button.");
+ }
+
+ overflowButton.click();
+ UiObject2 qualityButton = mDevice.wait(Until.findObject(
+ By.res(UI_PACKAGE_NAME, UI_VIDEO_PLAYER_QUALITY_BUTTON_ID)), UI_NAVIGATION_WAIT);
+ if (qualityButton == null) {
+ throw new UnknownUiException("Could not find video quality button.");
+ }
+
+ qualityButton.click();
+ UiObject2 quality360pLabel = mDevice.wait(Until.findObject(By.text(
+ AbstractYouTubeHelper.VideoQuality.QUALITY_360p.getText())), UI_NAVIGATION_WAIT);
+ if (quality360pLabel == null) {
+ throw new UnknownUiException("Could not find 360p quality label.");
+ }
+
+ UiObject2 selectDialog = quality360pLabel.getParent();
+ if (selectDialog == null) {
+ throw new UnknownUiException("Could not find video quality dialog.");
+ }
+
+ UiObject2 qualityLabel = null;
+ for (int retriesRemaining = 5; retriesRemaining > 0; --retriesRemaining) {
+ qualityLabel = mDevice.findObject(By.text(quality.getText()));
+ if (qualityLabel != null) {
+ break;
+ }
+ selectDialog.scroll(Direction.DOWN, 1.0f);
+ mDevice.waitForIdle();
+ }
+ if (qualityLabel == null) {
+ throw new UnknownUiException(
+ String.format("Could not find quality %s label", quality.getText()));
+ }
+
+ Log.v(TAG, String.format("Found quality %s label", quality.getText()));
+ qualityLabel.click();
+ if (!mDevice.wait(Until.hasObject(By.res(UI_PACKAGE_NAME, UI_VIDEO_PLAYER_ID)),
+ UI_NAVIGATION_WAIT)) {
+ throw new UnknownUiException("Did not find video player after selecting quality.");
+ }
+ }
+
+ private UiObject2 getFullscreenButton() {
+ return mDevice.findObject(By.desc(UI_FULLSCREEN_BUTTON_DESC));
+ }
+
+ private UiObject2 getPlayPauseReplayButton() {
+ return mDevice.findObject(
+ By.res(UI_PACKAGE_NAME, UI_VIDEO_PLAYER_PLAY_PAUSE_REPLAY_BUTTON_ID));
+ }
+
+ public void resumeVideo() {
+ UiObject2 videoPlayer = getVideoPlayer();
+ if (videoPlayer == null) {
+ throw new UnknownUiException("Could not find video player.");
+ }
+
+ videoPlayer.click();
+ UiObject2 playPauseReplayButton = mDevice.wait(Until.findObject(By.res(UI_PACKAGE_NAME,
+ UI_VIDEO_PLAYER_PLAY_PAUSE_REPLAY_BUTTON_ID)), UI_NAVIGATION_WAIT);
+ if (playPauseReplayButton == null) {
+ throw new UnknownUiException("Could not find the pause/play button.");
+ }
+
+ if (UI_PLAY_VIDEO_DESC.equals(playPauseReplayButton.getContentDescription())) {
+ playPauseReplayButton.click();
+ }
+ }
+
+ private boolean hasConnectionEstablishedMessage() {
+ Pattern establishedMsg =
+ Pattern.compile("Connection established", Pattern.CASE_INSENSITIVE);
+ return mDevice.hasObject(By.res(UI_PACKAGE_NAME, "message").text(establishedMsg));
+ }
+
+ private void pressGoOnline() {
+ Pattern goOnlineMsg = Pattern.compile("Go online", Pattern.CASE_INSENSITIVE);
+ UiObject2 button = mDevice.findObject(By.res(UI_PACKAGE_NAME, "action").text(goOnlineMsg));
+ if (button != null) {
+ button.click();
+ mDevice.waitForIdle();
+ } else {
+ throw new UnknownUiException("Unable to find GO ONLINE button.");
+ }
+ }
+}
diff --git a/scripts/perf-setup/Android.mk b/scripts/perf-setup/Android.mk
new file mode 100644
index 0000000..2d9119c
--- /dev/null
+++ b/scripts/perf-setup/Android.mk
@@ -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.
+
+
+# Rules to generate setup script for device perf tests
+# Different devices may share the same script. To add a new script, define a
+# new variable named <device name>_script, pointing at the script in current
+# source folder.
+# At execution time, scripts will be pushed onto device and run with root
+# identity
+
+LOCAL_PATH:= $(call my-dir)
+
+# only define the target if a perf setup script is defined by the BoardConfig
+# of the device we are building.
+#
+# To add a new script:
+# 1. add a new setup script suitable for the device at:
+# platform_testing/scripts/perf-setup/
+# 2. modify BoardConfig.mk of the corresponding device under:
+# device/<OEM name>/<device name/
+# 3. add variable "BOARD_PERFSETUP_SCRIPT", and point it at the path to the new
+# perf setup script; the path should be relative to the build root
+ifneq ($(strip $(BOARD_PERFSETUP_SCRIPT)),)
+
+include $(CLEAR_VARS)
+LOCAL_MODULE := perf-setup.sh
+LOCAL_MODULE_CLASS := EXECUTABLES
+LOCAL_MODULE_TAGS := optional
+LOCAL_MODULE_PATH := $(TARGET_OUT_DATA)/local/tmp
+LOCAL_PREBUILT_MODULE_FILE := $(BOARD_PERFSETUP_SCRIPT)
+include $(BUILD_PREBUILT)
+
+endif
diff --git a/scripts/perf-setup/angler-setup.sh b/scripts/perf-setup/angler-setup.sh
new file mode 100755
index 0000000..7080df7
--- /dev/null
+++ b/scripts/perf-setup/angler-setup.sh
@@ -0,0 +1,28 @@
+if [[ "`id -u`" -ne "0" ]]; then
+ echo "WARNING: running as non-root, proceeding anyways..."
+fi
+
+stop thermal-engine
+stop perfd
+
+echo -n 0 > /sys/devices/system/cpu/cpu0/online
+echo -n 0 > /sys/devices/system/cpu/cpu1/online
+echo -n 0 > /sys/devices/system/cpu/cpu2/online
+echo -n 0 > /sys/devices/system/cpu/cpu3/online
+
+echo -n 1 > /sys/devices/system/cpu/cpu4/online
+echo -n performance > /sys/devices/system/cpu/cpu4/cpufreq/scaling_governor
+
+echo -n 1 > /sys/devices/system/cpu/cpu5/online
+echo -n performance > /sys/devices/system/cpu/cpu5/cpufreq/scaling_governor
+
+echo -n 0 > /sys/devices/system/cpu/cpu6/online
+echo -n 0 > /sys/devices/system/cpu/cpu7/online
+
+echo performance > /sys/class/kgsl/kgsl-3d0/devfreq/governor
+
+echo 0 > /sys/class/kgsl/kgsl-3d0/bus_split
+echo 1 > /sys/class/kgsl/kgsl-3d0/force_clk_on
+
+echo 10000 > /sys/class/kgsl/kgsl-3d0/idle_timer
+
diff --git a/tests/androidbvt/Android.mk b/tests/androidbvt/Android.mk
new file mode 100644
index 0000000..970a3df
--- /dev/null
+++ b/tests/androidbvt/Android.mk
@@ -0,0 +1,29 @@
+# Copyright 2016 Google Inc. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT 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 := tests
+
+LOCAL_SDK_VERSION := system_current
+media_framework_app_base := frameworks/base/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest
+LOCAL_SRC_FILES := $(call all-subdir-java-files)
+LOCAL_JAVA_LIBRARIES := android.test.runner
+LOCAL_STATIC_JAVA_LIBRARIES := android-support-test ub-uiautomator launcher-helper-lib
+
+LOCAL_PACKAGE_NAME := AndroidBvtTests
+LOCAL_CERTIFICATE := platform
+
+include $(BUILD_PACKAGE)
diff --git a/tests/androidbvt/AndroidManifest.xml b/tests/androidbvt/AndroidManifest.xml
new file mode 100644
index 0000000..c38bc88
--- /dev/null
+++ b/tests/androidbvt/AndroidManifest.xml
@@ -0,0 +1,58 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Copyright 2016 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.android.androidbvt">
+ <uses-sdk android:minSdkVersion="19"
+ android:targetSdkVersion="24" />
+ <uses-feature android:name="android.hardware.camera"
+ android:required="true" />
+
+ <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
+ <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
+ <uses-permission android:name="android.permission.CAPTURE_VIDEO_OUTPUT" />
+ <uses-permission android:name="android.permission.CALL_PHONE" />
+ <uses-permission android:name="android.permission.CAMERA" />
+ <uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
+ <uses-permission android:name="android.permission.INTERNET" />
+ <uses-permission android:name="android.permission.GET_ACCOUNTS" />
+ <uses-permission android:name="android.permission.GET_DETAILED_TASKS" />
+ <uses-permission android:name="android.permission.READ_PHONE_STATE" />
+ <uses-permission android:name="android.permission.SEND_SMS" />
+ <uses-permission android:name="android.permission.SET_WALLPAPER" />
+ <uses-permission android:name="android.permission.TETHER_PRIVILEGED" />
+ <uses-permission android:name="android.permission.WAKE_LOCK" />
+ <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
+ <uses-permission android:name="android.permission.WRITE_SETTINGS" />
+
+ <application>
+ <uses-library android:name="android.test.runner" />
+ <activity android:label="MediaPlaybackTest"
+ android:name=".app.MediaPlaybackTestApp"
+ android:screenOrientation="landscape">
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN" />
+ <category android:name="android.intent.category.LAUNCHER" />
+ </intent-filter>
+ </activity>
+
+ </application>
+ <instrumentation
+ android:name="android.support.test.runner.AndroidJUnitRunner"
+ android:targetPackage="com.android.androidbvt"
+ android:label="AndroidBVT Tests" />
+</manifest>
diff --git a/tests/androidbvt/res/drawable-xhdpi/stat_notify_email.png b/tests/androidbvt/res/drawable-xhdpi/stat_notify_email.png
new file mode 100644
index 0000000..23c4672
--- /dev/null
+++ b/tests/androidbvt/res/drawable-xhdpi/stat_notify_email.png
Binary files differ
diff --git a/tests/androidbvt/res/layout/surface_view.xml b/tests/androidbvt/res/layout/surface_view.xml
new file mode 100644
index 0000000..4999e5d
--- /dev/null
+++ b/tests/androidbvt/res/layout/surface_view.xml
@@ -0,0 +1,46 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2011 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical">
+
+ <FrameLayout
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <SurfaceView
+ android:id="@+id/surface_view"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_centerInParent="true"
+ />
+
+ <ImageView android:id="@+id/overlay_layer"
+ android:layout_width="0dip"
+ android:layout_height="392dip"/>
+
+ <VideoView
+ android:id="@+id/video_view"
+ android:layout_width="320px"
+ android:layout_height="240px"
+ />
+
+ </FrameLayout>
+
+</LinearLayout>
+
diff --git a/tests/androidbvt/res/raw/bbb.mkv b/tests/androidbvt/res/raw/bbb.mkv
new file mode 100644
index 0000000..e286e01
--- /dev/null
+++ b/tests/androidbvt/res/raw/bbb.mkv
Binary files differ
diff --git a/tests/androidbvt/src/com/android/androidbvt/AndroidBvtHelper.java b/tests/androidbvt/src/com/android/androidbvt/AndroidBvtHelper.java
new file mode 100644
index 0000000..5428ad1
--- /dev/null
+++ b/tests/androidbvt/src/com/android/androidbvt/AndroidBvtHelper.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.androidbvt;
+
+import android.app.DownloadManager;
+import android.app.UiAutomation;
+import android.content.Context;
+import android.net.ConnectivityManager;
+import android.net.wifi.WifiManager;
+import android.os.ParcelFileDescriptor;
+import android.support.test.uiautomator.UiDevice;
+import android.telecom.TelecomManager;
+import android.util.Log;
+
+import java.io.BufferedReader;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Defines constants & implements common methods to be used by Framework, SysUI, System e2e BVT
+ * tests Also ensures single instance of this object
+ */
+public class AndroidBvtHelper {
+ public static final String TEST_TAG = "AndroidBVT";
+ public static final int SHORT_TIMEOUT = 1000;
+ public static final int LONG_TIMEOUT = 5000;
+ private static AndroidBvtHelper mInstance = null;
+ private Context mContext = null;
+ private UiDevice mDevice = null;
+ private UiAutomation mUiAutomation = null;
+
+ public AndroidBvtHelper(UiDevice device, Context context, UiAutomation uiAutomation) {
+ mContext = context;
+ mDevice = device;
+ mUiAutomation = uiAutomation;
+ }
+
+ public static AndroidBvtHelper getInstance(UiDevice device, Context context,
+ UiAutomation uiAutomation) {
+ if (mInstance == null) {
+ mInstance = new AndroidBvtHelper(device, context, uiAutomation);
+ }
+ return mInstance;
+ }
+
+ public TelecomManager getTelecomManager() {
+ return (TelecomManager) mContext.getSystemService(Context.TELECOM_SERVICE);
+ }
+
+ public WifiManager getWifiManager() {
+ return (WifiManager) mContext.getSystemService(Context.WIFI_SERVICE);
+ }
+
+ public ConnectivityManager getConnectivityManager() {
+ return (ConnectivityManager) (ConnectivityManager) mContext
+ .getSystemService(Context.CONNECTIVITY_SERVICE);
+ }
+
+ public DownloadManager getDownloadManager() {
+ return (DownloadManager) (DownloadManager) mContext
+ .getSystemService(Context.DOWNLOAD_SERVICE);
+ }
+
+ /**
+ * Only executes 'adb shell' commands that run in the same process as the runner Converts output
+ * of the command from ParcelFileDescriptior to user friendly list of strings
+ * https://developer.android.com/reference/android/app/UiAutomation.html#executeShellCommand(
+ * java.lang.String)
+ */
+ public List<String> executeShellCommand(String cmd) {
+ if (cmd == null || cmd.isEmpty()) {
+ return null;
+ }
+ List<String> output = new ArrayList<String>();
+ ParcelFileDescriptor pfd = mUiAutomation.executeShellCommand(cmd);
+ try (BufferedReader reader = new BufferedReader(
+ new InputStreamReader(new FileInputStream(pfd.getFileDescriptor())))) {
+ String line;
+ while ((line = reader.readLine()) != null) {
+ output.add(line);
+ }
+ } catch (IOException e) {
+ Log.e(TEST_TAG, e.getMessage());
+ }
+ return output;
+ }
+}
diff --git a/tests/androidbvt/src/com/android/androidbvt/ConnectivityWifiTests.java b/tests/androidbvt/src/com/android/androidbvt/ConnectivityWifiTests.java
new file mode 100644
index 0000000..c9bbd92
--- /dev/null
+++ b/tests/androidbvt/src/com/android/androidbvt/ConnectivityWifiTests.java
@@ -0,0 +1,208 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.androidbvt;
+
+import android.content.Context;
+import android.content.Intent;
+import android.net.wifi.WifiConfiguration;
+import android.net.wifi.WifiConfiguration.AuthAlgorithm;
+import android.net.wifi.WifiConfiguration.KeyMgmt;
+import android.net.wifi.WifiManager;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.uiautomator.By;
+import android.support.test.uiautomator.UiDevice;
+import android.support.test.uiautomator.Until;
+import android.test.suitebuilder.annotation.LargeTest;
+import android.test.suitebuilder.annotation.Suppress;
+import android.util.Log;
+
+import junit.framework.TestCase;
+
+import java.io.IOException;
+import java.net.HttpURLConnection;
+import java.net.URL;
+
+public class ConnectivityWifiTests extends TestCase {
+ private final static String DEFAULT_PING_SITE = "www.google.com";
+ private final String NETWORK_ID = "AndroidAP";
+ private final String PASSWD = "androidwifi";
+ private UiDevice mDevice;
+ private WifiManager mWifiManager = null;
+ private Context mContext = null;
+ private AndroidBvtHelper mABvtHelper = null;
+ private WifiConfiguration mOriginalConfig = null;
+
+ @Override
+ public void setUp() throws Exception {
+ super.setUp();
+ mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());
+ mDevice.setOrientationNatural();
+ mContext = InstrumentationRegistry.getTargetContext();
+ mABvtHelper = AndroidBvtHelper.getInstance(mDevice, mContext,
+ InstrumentationRegistry.getInstrumentation().getUiAutomation());
+ mWifiManager = mABvtHelper.getWifiManager();
+ }
+
+ @Override
+ public void tearDown() throws Exception {
+ mDevice.wakeUp();
+ mDevice.unfreezeRotation();
+ mDevice.pressHome();
+ mDevice.waitForIdle();
+ super.tearDown();
+ }
+
+ /**
+ * Test verifies wifi can be disconnected, disabled followed by enable and reconnect. As part of
+ * connection check, it pings a site and ensures HTTP_OK return
+ */
+ @LargeTest
+ public void testWifiConnection() throws InterruptedException {
+ // Wifi is already connected as part of tradefed device setup, assert that
+ assertTrue("Wifi should be connected", isWifiConnected());
+ assertNotNull("Wifi manager is null", mWifiManager);
+ assertTrue("Wifi isn't enabled", mWifiManager.isWifiEnabled());
+ // Disconnect wifi and disable network, save NetId to be used for re-enabling network
+ int netId = mWifiManager.getConnectionInfo().getNetworkId();
+ disconnectWifi();
+ Log.d("MyTestTag", "before sleep");
+ Thread.sleep(mABvtHelper.LONG_TIMEOUT);
+ Log.d("MyTestTag", "after sleep");
+ assertFalse("Wifi shouldn't be connected", isWifiConnected());
+ // Network enabled successfully
+ assertTrue("Network isn't enabled", mWifiManager.enableNetwork(netId, true));
+ // Allow time to settle down
+ Thread.sleep(mABvtHelper.LONG_TIMEOUT * 2);
+ assertTrue("Wifi should be connected", isWifiConnected());
+ }
+
+ /**
+ * Test verifies from UI that bunch of AP are listed on enabling Wifi
+ */
+ @LargeTest
+ public void testWifiDiscoveredAPShownUI() throws InterruptedException {
+ Intent intent_as = new Intent(
+ android.provider.Settings.ACTION_WIFI_SETTINGS);
+ mContext.startActivity(intent_as);
+ Thread.sleep(mABvtHelper.LONG_TIMEOUT);
+ assertNotNull("AP list shouldn't be null",
+ mDevice.wait(Until.findObject(By.res("com.android.settings:id/list")),
+ mABvtHelper.LONG_TIMEOUT));
+ assertTrue("At least 1 AP should be visible",
+ mDevice.wait(Until.findObject(By.res("com.android.settings:id/list")),
+ mABvtHelper.LONG_TIMEOUT)
+ .getChildren().size() > 0);
+ }
+
+ /**
+ * Verifies WifiAp is by default disabled Then enable adn disable it
+ */
+ @LargeTest
+ @Suppress
+ public void testWifiTetheringDisableEnable() throws InterruptedException {
+ WifiConfiguration config = new WifiConfiguration();
+ config.SSID = NETWORK_ID;
+ config.allowedKeyManagement.set(KeyMgmt.WPA_PSK);
+ config.allowedAuthAlgorithms.set(AuthAlgorithm.OPEN);
+ config.preSharedKey = PASSWD;
+ int counter;
+ try {
+ // disable wifiap
+ assertTrue("wifi hotspot not disabled by default",
+ mWifiManager.getWifiApState() == WifiManager.WIFI_AP_STATE_DISABLED);
+ // Enable wifiap
+ assertTrue("failed to disable wifi hotspot",
+ mWifiManager.setWifiApEnabled(config, true));
+ Log.d("MyTestTag", "Now checkign wifi ap");
+ counter = 10;
+ while (--counter > 0
+ && mWifiManager.getWifiApState() != WifiManager.WIFI_AP_STATE_ENABLED) {
+ Thread.sleep(mABvtHelper.SHORT_TIMEOUT);
+ }
+ assertTrue("wifi hotspot not enabled",
+ mWifiManager.getWifiApState() == WifiManager.WIFI_AP_STATE_ENABLED);
+ // Navigate to Wireless Settings page and verify Wifi AP setting is on
+ Intent intent_as = new Intent(
+ android.provider.Settings.ACTION_WIRELESS_SETTINGS);
+ mContext.startActivity(intent_as);
+ Thread.sleep(mABvtHelper.LONG_TIMEOUT);
+ mDevice.wait(Until.findObject(By.text("Tethering & portable hotspot")),
+ mABvtHelper.LONG_TIMEOUT).click();
+ Thread.sleep(mABvtHelper.SHORT_TIMEOUT);
+ assertTrue("Settings UI for Wifi AP is not ON",
+ mDevice.wait(Until.hasObject(By.text("Portable hotspot AndroidAP active")),
+ mABvtHelper.LONG_TIMEOUT));
+
+ mDevice.wait(Until.findObject(By.text("Portable Wi‑Fi hotspot")),
+ mABvtHelper.LONG_TIMEOUT).click();
+ assertTrue("Wifi ap disable call fails", mWifiManager.setWifiApEnabled(config,
+ false));
+ counter = 5;
+ while (--counter > 0
+ && mWifiManager.getWifiApState() != WifiManager.WIFI_AP_STATE_DISABLED) {
+ Thread.sleep(mABvtHelper.LONG_TIMEOUT);
+ }
+ assertTrue("wifi hotspot not enabled",
+ mWifiManager.getWifiApState() == WifiManager.WIFI_AP_STATE_DISABLED);
+ Thread.sleep(mABvtHelper.LONG_TIMEOUT * 2);
+ } finally {
+ assertTrue("Wifi enable call fails", mWifiManager
+ .enableNetwork(mWifiManager.getConnectionInfo().getNetworkId(), false));
+ counter = 10;
+ while (--counter > 0 && !mWifiManager.isWifiEnabled()) {
+ Thread.sleep(mABvtHelper.LONG_TIMEOUT);
+ }
+ assertTrue("Wifi isn't enabled", mWifiManager.isWifiEnabled());
+ }
+ }
+
+ /**
+ * Checks if wifi connection is active by sending an HTTP request, check for HTTP_OK
+ */
+ private boolean isWifiConnected() throws InterruptedException {
+ int counter = 10;
+ while (--counter > 0) {
+ try {
+ String mPingSite = String.format("http://%s", DEFAULT_PING_SITE);
+ URL url = new URL(mPingSite);
+ HttpURLConnection conn = (HttpURLConnection) url.openConnection();
+ conn.setRequestMethod("GET");
+ conn.setConnectTimeout(mABvtHelper.LONG_TIMEOUT * 5);
+ conn.setReadTimeout(mABvtHelper.LONG_TIMEOUT * 5);
+ if (conn.getResponseCode() == HttpURLConnection.HTTP_OK) {
+ return true;
+ }
+ Thread.sleep(mABvtHelper.SHORT_TIMEOUT);
+ } catch (IOException ex) {
+ // Wifi being flaky in the lab, test retries 5 times to connect to google.com
+ // as IOException is throws connection isn't made and response stream is null
+ // so for retrying purpose, exception hasn't been rethrown
+ Log.i(mABvtHelper.TEST_TAG, ex.getMessage());
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Disconnects and disables network
+ */
+ private void disconnectWifi() {
+ assertTrue("Wifi not disconnected", mWifiManager.disconnect());
+ mWifiManager.disableNetwork(mWifiManager.getConnectionInfo().getNetworkId());
+ mWifiManager.saveConfiguration();
+ }
+}
diff --git a/tests/androidbvt/src/com/android/androidbvt/FrameworkDownloadTests.java b/tests/androidbvt/src/com/android/androidbvt/FrameworkDownloadTests.java
new file mode 100644
index 0000000..9f14d02
--- /dev/null
+++ b/tests/androidbvt/src/com/android/androidbvt/FrameworkDownloadTests.java
@@ -0,0 +1,175 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.androidbvt;
+
+import android.app.DownloadManager;
+import android.app.DownloadManager.Query;
+import android.app.DownloadManager.Request;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.database.Cursor;
+import android.net.Uri;
+import android.net.wifi.WifiManager;
+import android.os.ParcelFileDescriptor;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.uiautomator.UiDevice;
+import android.test.suitebuilder.annotation.LargeTest;
+import android.util.Log;
+
+import junit.framework.TestCase;
+
+import java.io.IOException;
+import java.util.HashSet;
+
+public class FrameworkDownloadTests extends TestCase {
+ private static final String TEST_TAG = "AndroidBVT";
+ private final String TEST_HOST = "209.119.80.137:10090/new_ui/all_content/photos";
+ private final String TEST_FILE = "android_apps.jpeg";
+ private final int TEST_FILE_SIZE = 159709;
+ private DownloadManager mDownloadManager = null;
+ private WifiManager mWifiManager = null;
+ private AndroidBvtHelper mABvtHelper = null;
+ private UiDevice mDevice;
+ private Context mContext = null;
+
+ @Override
+ public void setUp() throws Exception {
+ super.setUp();
+ mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());
+ mDevice.freezeRotation();
+ mContext = InstrumentationRegistry.getTargetContext();
+ mABvtHelper = AndroidBvtHelper.getInstance(mDevice, mContext,
+ InstrumentationRegistry.getInstrumentation().getUiAutomation());
+ mDownloadManager = mABvtHelper.getDownloadManager();
+ mWifiManager = mABvtHelper.getWifiManager();
+ }
+
+ @Override
+ public void tearDown() throws Exception {
+ mDevice.unfreezeRotation();
+ mDevice.pressMenu();
+ mDevice.waitForIdle();
+ super.tearDown();
+ }
+
+ /**
+ * Following test verifies that download service is running and serves any download request
+ * Enqueues a request to download a photo After download completion, compares file size that
+ * mentioned in server
+ */
+ @LargeTest
+ public void testPhotoDownloadSucceed() throws InterruptedException, IOException {
+ // Device already connected to wifi as part of tradefed setup
+ if (!mWifiManager.isWifiEnabled()) {
+ mWifiManager.enableNetwork(mWifiManager.getConnectionInfo().getNetworkId(), true);
+ int counter = 5;
+ while (--counter > 0 && mWifiManager.isWifiEnabled()) {
+ Thread.sleep(mABvtHelper.LONG_TIMEOUT);
+ }
+ }
+ assertTrue("Wifi should be enabled by now", mWifiManager.isWifiEnabled());
+ removeAllCurrentDownloads(); // if there are any in progress
+ Uri downloadUri = Uri.parse(String.format("http://%s/%s", TEST_HOST, TEST_FILE));
+ Request request = new Request(downloadUri);
+ long dlRequest = mDownloadManager.enqueue(request);
+
+ // Register receiver to listen to DownloadComplete Broadcase message
+ // Wait for download to finish
+ final DownloadCompleteReceiver receiver = new DownloadCompleteReceiver();
+ try {
+ IntentFilter intentFilter = new IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE);
+ mContext.registerReceiver(receiver, intentFilter);
+ Thread.sleep(mABvtHelper.LONG_TIMEOUT);
+ assertTrue("download not finished", receiver.isDownloadCompleted(dlRequest));
+ // Verify Download file size
+ ParcelFileDescriptor pfd = null;
+ try {
+ pfd = mDownloadManager.openDownloadedFile(dlRequest);
+ assertTrue("File size should be same as mentioned in server",
+ pfd.getStatSize() == TEST_FILE_SIZE);
+ } finally {
+ if (pfd != null) {
+ pfd.close();
+ }
+ mDownloadManager.remove(dlRequest);
+ }
+ } finally {
+ mContext.unregisterReceiver(receiver);
+ }
+ }
+
+ /**
+ * Remove all downloads those are in progress now
+ */
+ private void removeAllCurrentDownloads() {
+ DownloadManager downloadManager = (DownloadManager) mContext
+ .getSystemService(Context.DOWNLOAD_SERVICE);
+ Cursor cursor = downloadManager.query(new Query());
+ try {
+ if (cursor.moveToFirst()) {
+ do {
+ int index = cursor.getColumnIndex(DownloadManager.COLUMN_ID);
+ long downloadId = cursor.getLong(index);
+ downloadManager.remove(downloadId);
+ } while (cursor.moveToNext());
+ }
+ } finally {
+ cursor.close();
+ }
+ }
+
+ /**
+ * DownloadManager broadcasts 'DownloadManager.ACTION_DOWNLOAD_COMPLETE' once download is
+ * complete and copied from cache to Downloads folder Following receiver to intercept download
+ * intent to parse out the downloaded id to ensure that the item has been downloaded that was
+ * initiated in the test. Please note that when a download action is enqueued, DownloadManager
+ * provides a download id
+ */
+ private class DownloadCompleteReceiver extends BroadcastReceiver {
+ private HashSet<Long> mCompleteIds = new HashSet<>();
+
+ public DownloadCompleteReceiver() {
+ }
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ synchronized (mCompleteIds) {
+ mCompleteIds.add(intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, -1));
+ Log.i(TEST_TAG, "Request Id = "
+ + intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, -1));
+ mCompleteIds.notifyAll();
+ }
+ }
+
+ // Tries 5 times/5 secs for download to be completed
+ public boolean isDownloadCompleted(long id)
+ throws InterruptedException {
+ int counter = 10;
+ while (--counter > 0) {
+ synchronized (mCompleteIds) {
+ mCompleteIds.wait(mABvtHelper.LONG_TIMEOUT);
+ if (mCompleteIds.contains(id)) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+ }
+}
diff --git a/tests/androidbvt/src/com/android/androidbvt/MediaCaptureTests.java b/tests/androidbvt/src/com/android/androidbvt/MediaCaptureTests.java
new file mode 100644
index 0000000..518a49d
--- /dev/null
+++ b/tests/androidbvt/src/com/android/androidbvt/MediaCaptureTests.java
@@ -0,0 +1,144 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.androidbvt;
+
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Environment;
+import android.provider.MediaStore;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.uiautomator.By;
+import android.support.test.uiautomator.UiDevice;
+import android.support.test.uiautomator.UiObject2;
+import android.support.test.uiautomator.Until;
+import android.test.InstrumentationTestCase;
+import android.test.suitebuilder.annotation.LargeTest;
+
+import junit.framework.TestCase;
+import java.io.File;
+import java.util.regex.Pattern;
+
+/**
+ * Basic tests for the Camera app.
+ */
+public class MediaCaptureTests extends TestCase {
+ private static final int CAPTURE_TIMEOUT = 6000;
+ private static final String DESC_BTN_CAPTURE_PHOTO = "Capture photo";
+ private static final String DESC_BTN_CAPTURE_VIDEO = "Capture video";
+ private static final String DESC_BTN_DONE = "Done";
+ private static final String DESC_BTN_PHOTO_MODE = "Open photo mode";
+ private static final String DESC_BTN_VIDEO_MODE = "Open video mode";
+ private static final int FILE_CHECK_ATTEMPTS = 5;
+ private static final int VIDEO_LENGTH = 2000;
+
+ private UiDevice mDevice;
+
+ @Override
+ public void setUp() throws Exception {
+ super.setUp();
+ mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());
+ mDevice.freezeRotation();
+ // if there are any dialogues that pop up, dismiss them
+ UiObject2 maybeOkButton = mDevice.wait(Until.findObject(By.res("android:id/ok_button")),
+ CAPTURE_TIMEOUT);
+ if (maybeOkButton != null) {
+ maybeOkButton.click();
+ }
+ }
+
+ @Override
+ public void tearDown() throws Exception {
+ mDevice.unfreezeRotation();
+ super.tearDown();
+ }
+
+ /**
+ * Test that the device can capture a photo.
+ */
+ @LargeTest
+ public void testPhotoCapture() {
+ runCaptureTest(new Intent(MediaStore.ACTION_IMAGE_CAPTURE), "smoke.jpg", false);
+ }
+
+ /**
+ * Test that the device can capture a video.
+ */
+ @LargeTest
+ public void testVideoCapture() {
+ runCaptureTest(new Intent(MediaStore.ACTION_VIDEO_CAPTURE), "smoke.avi", true);
+ }
+
+ private void runCaptureTest(Intent intent, String tmpName, boolean isVideo) {
+ intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ if (intent.resolveActivity(
+ InstrumentationRegistry.getInstrumentation().getContext().getPackageManager()) != null) {
+ File outputFile = null;
+ try {
+ outputFile = new File(Environment
+ .getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM), tmpName);
+ intent.putExtra(MediaStore.EXTRA_OUTPUT, Uri.fromFile(outputFile));
+ InstrumentationRegistry.getInstrumentation().getContext().startActivity(intent);
+ switchCaptureMode(isVideo);
+ pressCaptureButton(isVideo);
+ if (isVideo) {
+ Thread.sleep(VIDEO_LENGTH);
+ pressCaptureButton(isVideo);
+ }
+ Thread.sleep(1000);
+ pushButton(DESC_BTN_DONE);
+ long fileLength = outputFile.length();
+ for (int i=0; i<FILE_CHECK_ATTEMPTS; i++) {
+ if ((fileLength = outputFile.length()) > 0) {
+ break;
+ }
+ Thread.sleep(1000);
+ }
+ assertTrue(fileLength > 0);
+ } catch (InterruptedException e) {
+ fail(e.getLocalizedMessage());
+ } finally {
+ if (outputFile != null) {
+ outputFile.delete();
+ }
+ }
+ }
+ }
+
+ private void switchCaptureMode(boolean isVideo) {
+ if (isVideo) {
+ pushButton(DESC_BTN_VIDEO_MODE);
+ } else {
+ pushButton(DESC_BTN_PHOTO_MODE);
+ }
+ }
+
+ private void pressCaptureButton(boolean isVideo) {
+ if (isVideo) {
+ pushButton(DESC_BTN_CAPTURE_VIDEO);
+ } else {
+ pushButton(DESC_BTN_CAPTURE_PHOTO);
+ }
+ }
+
+ private void pushButton(String desc) {
+ Pattern pattern = Pattern.compile(desc, Pattern.CASE_INSENSITIVE);
+ UiObject2 doneBtn = mDevice.wait(Until.findObject(By.desc(pattern)), CAPTURE_TIMEOUT);
+ if (null != doneBtn) {
+ doneBtn.clickAndWait(Until.newWindow(), 500);
+ }
+ }
+}
diff --git a/tests/androidbvt/src/com/android/androidbvt/MediaPlaybackTests.java b/tests/androidbvt/src/com/android/androidbvt/MediaPlaybackTests.java
new file mode 100644
index 0000000..474e820
--- /dev/null
+++ b/tests/androidbvt/src/com/android/androidbvt/MediaPlaybackTests.java
@@ -0,0 +1,109 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.androidbvt;
+
+import android.media.MediaPlayer;
+import android.os.Looper;
+import android.test.ActivityInstrumentationTestCase2;
+import android.test.suitebuilder.annotation.LargeTest;
+import android.util.Log;
+
+import com.android.androidbvt.app.MediaPlaybackTestApp;
+
+/**
+ * Basic tests for video playback
+ */
+public class MediaPlaybackTests extends ActivityInstrumentationTestCase2<MediaPlaybackTestApp> {
+
+ private static final String TAG = "MediaPlaybackTest";
+ private static final int LOOP_START_BUFFER_MS = 10000;
+ private static final int PLAY_BUFFER_MS = 2000;
+ private final Object mCompletionLock = new Object();
+ private final Object mLooperLock = new Object();
+ private boolean mPlaybackSucceeded = false;
+ private boolean mPlaybackError = false;
+ private Looper mLooper;
+ private MediaPlayer mPlayer;
+
+ public MediaPlaybackTests() {
+ super(MediaPlaybackTestApp.class);
+ }
+
+ @Override
+ public void setUp() throws Exception {
+ // start activity
+ getActivity();
+ }
+
+ @LargeTest
+ public void testVideoPlayback() {
+ // start the MediaPlayer on a Looper thread, so it does not deadlock itself
+ new Thread() {
+ @Override
+ public void run() {
+ Looper.prepare();
+ mLooper = Looper.myLooper();
+ mPlayer = MediaPlayer.create(getInstrumentation().getContext(), R.raw.bbb);
+ mPlayer.setDisplay(getActivity().getSurfaceHolder());
+ synchronized (mLooperLock) {
+ mLooperLock.notify();
+ }
+ Looper.loop();
+ }
+ }.start();
+ // make sure the looper is really started before we proceed
+ synchronized (mLooperLock) {
+ try {
+ mLooperLock.wait(LOOP_START_BUFFER_MS);
+ } catch (InterruptedException e) {
+ fail("Loop thread start was interrupted");
+ }
+ }
+ mPlayer.setOnErrorListener(new MediaPlayer.OnErrorListener() {
+ @Override
+ public boolean onError(MediaPlayer mp, int what, int extra) {
+ mPlaybackError = true;
+ mp.reset();
+ return true;
+ }
+ });
+ mPlayer.setOnCompletionListener(new MediaPlayer.OnCompletionListener() {
+ @Override
+ public void onCompletion(MediaPlayer mp) {
+ synchronized (mCompletionLock) {
+ Log.w(TAG, "Hit onCompletion!");
+ mPlaybackSucceeded = true;
+ mCompletionLock.notifyAll();
+ }
+ }
+ });
+ mPlayer.start();
+ int duration = mPlayer.getDuration();
+ int currentPosition = mPlayer.getCurrentPosition();
+ synchronized (mCompletionLock) {
+ try {
+ mCompletionLock.wait(duration - currentPosition + PLAY_BUFFER_MS);
+ } catch (InterruptedException e) {
+ fail("Wait for playback was interrupted");
+ }
+ }
+ mLooper.quit();
+ mPlayer.release();
+ assertFalse(mPlaybackError);
+ assertTrue(mPlaybackSucceeded);
+ }
+}
diff --git a/tests/androidbvt/src/com/android/androidbvt/SysUIGSATests.java b/tests/androidbvt/src/com/android/androidbvt/SysUIGSATests.java
new file mode 100644
index 0000000..dd7a9cf
--- /dev/null
+++ b/tests/androidbvt/src/com/android/androidbvt/SysUIGSATests.java
@@ -0,0 +1,169 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.androidbvt;
+
+import android.app.UiAutomation;
+import android.content.Context;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.uiautomator.By;
+import android.support.test.uiautomator.Direction;
+import android.support.test.uiautomator.UiDevice;
+import android.support.test.uiautomator.UiObject2;
+import android.support.test.uiautomator.Until;
+import android.test.suitebuilder.annotation.LargeTest;
+import android.view.KeyEvent;
+
+import junit.framework.TestCase;
+
+import java.util.List;
+
+/**
+ * Contains tests for features that are loosely coupled with Android system for sanity
+ */
+public class SysUIGSATests extends TestCase {
+ private final String QSB_PKG = "com.google.android.googlequicksearchbox";
+ private UiAutomation mUiAutomation = null;
+ private UiDevice mDevice;
+ private Context mContext = null;
+ private AndroidBvtHelper mABvtHelper = null;
+
+ @Override
+ public void setUp() throws Exception {
+ super.setUp();
+ mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());
+ mDevice.setOrientationNatural();
+ mContext = InstrumentationRegistry.getTargetContext();
+ mUiAutomation = InstrumentationRegistry.getInstrumentation().getUiAutomation();
+ mABvtHelper = AndroidBvtHelper.getInstance(mDevice, mContext, mUiAutomation);
+ mDevice.pressMenu();
+ mDevice.pressHome();
+ }
+
+ @Override
+ public void tearDown() throws Exception {
+ mDevice.pressHome();
+ mDevice.unfreezeRotation();
+ super.tearDown();
+ }
+
+ /**
+ * Ensures search via QSB searches both web and device apps Suuggested texts starts with
+ * searched text Remembers searched item, suggests as top suggestion next time
+ */
+ @LargeTest
+ public void testGoogleQuickSearchBar() throws InterruptedException {
+ final String TextToSearch = "co";
+ UiObject2 searchBox = null;
+ int counter = 5;
+ while (--counter > 0
+ && ((searchBox = mDevice.wait(Until.findObject(By.res(QSB_PKG, "search_box")),
+ mABvtHelper.SHORT_TIMEOUT)) == null)) {
+ Thread.sleep(mABvtHelper.SHORT_TIMEOUT);
+ mDevice.pressHome();
+ mDevice.pressSearch();
+ }
+ mDevice.wait(Until.findObject(By.res(QSB_PKG, "search_box")),
+ mABvtHelper.LONG_TIMEOUT).setText(TextToSearch);
+ Thread.sleep(mABvtHelper.LONG_TIMEOUT);
+ // make the IME down
+ mDevice.pressKeyCode(KeyEvent.KEYCODE_BACK);
+ // searching for 'co' will result from web, as well as 'Contacts' app. So there should be
+ // more than 1 container
+ UiObject2 searchSuggestionsContainer = mDevice.wait(Until.findObject(By.res(
+ QSB_PKG, "search_suggestions_container")), mABvtHelper.LONG_TIMEOUT);
+ assertTrue("QS suggestion should have more than 1 container",
+ searchSuggestionsContainer.getChildCount() > 1);
+ UiObject2 searchSuggestions = mDevice.wait(Until.findObject(By.res(
+ QSB_PKG, "search_suggestions_web")), mABvtHelper.LONG_TIMEOUT);
+ assertNotNull(
+ "Web Search suggestions shouldn't be null & should have more than 1 suggestions",
+ searchSuggestions != null && searchSuggestions.getChildCount() > 1);
+ List<UiObject2> suggestions = mDevice.wait(Until.findObjects(By.res(QSB_PKG, "text_1")),
+ mABvtHelper.LONG_TIMEOUT);
+ assertNotNull("Contacts app should be found", mDevice.wait(Until.findObject(
+ By.res(QSB_PKG, "text_1").text("Contacts")), mABvtHelper.LONG_TIMEOUT));
+ String topSuggestedText = suggestions.get(0).getText();
+ suggestions.get(0).clickAndWait(Until.newWindow(), mABvtHelper.LONG_TIMEOUT);
+ Thread.sleep(mABvtHelper.LONG_TIMEOUT);
+ // Search again and ensure last searched item showed as top suggestion
+ mDevice.pressHome();
+ Thread.sleep(mABvtHelper.SHORT_TIMEOUT);
+ mDevice.pressSearch();
+ String currentTopSuggestion = mDevice.wait(Until.findObjects(By.res(QSB_PKG, "text_1")),
+ mABvtHelper.LONG_TIMEOUT).get(0).getText();
+ assertTrue("Previous searched item isn't top suggested word",
+ topSuggestedText.toLowerCase().equals(topSuggestedText.toLowerCase()));
+ }
+
+ /**
+ * Ensures if any account is opted in GoogleNow, Google-assist offers card on long home press
+ */
+ @LargeTest
+ public void testGoogleAssist() throws InterruptedException {
+ mDevice.wait(Until.findObject(By.res(QSB_PKG, "search_plate")),
+ mABvtHelper.SHORT_TIMEOUT).click();
+ Thread.sleep(mABvtHelper.SHORT_TIMEOUT);
+ UiObject2 getStarted = mDevice.wait(Until.findObject(By.text("GET STARTED")),
+ mABvtHelper.SHORT_TIMEOUT);
+ if (getStarted != null) {
+ getStarted.clickAndWait(Until.newWindow(), mABvtHelper.SHORT_TIMEOUT);
+ mDevice.wait(Until.findObject(By.res(QSB_PKG, "text_container")),
+ mABvtHelper.SHORT_TIMEOUT).swipe(Direction.UP, 1.0f);
+ mDevice.wait(Until.findObject(By.text("YES, I’M IN")),
+ mABvtHelper.SHORT_TIMEOUT)
+ .clickAndWait(Until.newWindow(), mABvtHelper.SHORT_TIMEOUT);
+ }
+ // Search for Paris and click on first suggested text
+ mDevice.wait(Until.findObject(By.res(QSB_PKG, "text_container")),
+ mABvtHelper.LONG_TIMEOUT).setText("Paris");
+ Thread.sleep(mABvtHelper.LONG_TIMEOUT);
+ List<UiObject2> suggestedTexts = null;
+ int counter = 5;
+ while (--counter > 0
+ && ((suggestedTexts = mDevice.wait(Until.findObjects(By.res(QSB_PKG, "text_1")),
+ mABvtHelper.LONG_TIMEOUT)) == null)) {
+ Thread.sleep(mABvtHelper.SHORT_TIMEOUT);
+ }
+ assertNotNull("Suggested text shouldn't be null", suggestedTexts);
+ UiObject2 itemToClick = suggestedTexts.get(0);
+ for (UiObject2 item : suggestedTexts) {
+ if (item.getText().toLowerCase().equals("paris")) {
+ itemToClick = item;
+ }
+ }
+ itemToClick.clickAndWait(Until.newWindow(), mABvtHelper.SHORT_TIMEOUT);
+ Thread.sleep(mABvtHelper.LONG_TIMEOUT);
+ // Now long press home to load assist layer
+ mDevice.pressKeyCode(KeyEvent.KEYCODE_ASSIST);
+ UiObject2 optInYes = mDevice.wait(
+ Until.findObject(By.res(QSB_PKG, "screen_assist_opt_in_yes")),
+ mABvtHelper.SHORT_TIMEOUT);
+ if (optInYes != null) {
+ optInYes.clickAndWait(Until.newWindow(), mABvtHelper.SHORT_TIMEOUT);
+ }
+ // Ensure some cards are loaded
+ // Note card's content isn't verified
+ counter = 5;
+ UiObject2 cardContainer = null;
+ while (--counter > 0 && ((cardContainer = mDevice.wait(
+ Until.findObject(By.res(QSB_PKG, "card_container")),
+ mABvtHelper.SHORT_TIMEOUT)) != null)) {
+ Thread.sleep(mABvtHelper.SHORT_TIMEOUT);
+ }
+ assertNotNull("Some cards should be loaded", cardContainer);
+ }
+}
diff --git a/tests/androidbvt/src/com/android/androidbvt/SysUILauncherTests.java b/tests/androidbvt/src/com/android/androidbvt/SysUILauncherTests.java
new file mode 100644
index 0000000..e209817
--- /dev/null
+++ b/tests/androidbvt/src/com/android/androidbvt/SysUILauncherTests.java
@@ -0,0 +1,157 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.androidbvt;
+
+import android.app.WallpaperManager;
+import android.content.Context;
+import android.graphics.Point;
+import android.graphics.drawable.Drawable;
+import android.os.RemoteException;
+import android.provider.Settings;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.launcherhelper.ILauncherStrategy;
+import android.support.test.launcherhelper.LauncherStrategyFactory;
+import android.support.test.uiautomator.By;
+import android.support.test.uiautomator.Direction;
+import android.support.test.uiautomator.UiDevice;
+import android.support.test.uiautomator.UiObject2;
+import android.support.test.uiautomator.Until;
+import android.test.suitebuilder.annotation.LargeTest;
+
+import junit.framework.TestCase;
+
+import java.io.IOException;
+import java.util.List;
+
+public class SysUILauncherTests extends TestCase {
+ private static final int LONG_TIMEOUT = 5000;
+ private static final String APP_NAME = "Clock";
+ private static final String PKG_NAME = "com.google.android.deskclock";
+ private static final String WIDGET_PREVIEW = "widget_preview";
+ private static final String APP_WIDGET_VIEW = "android.appwidget.AppWidgetHostView";
+ private static final String WIDGET_TEXT_VIEW = "android.widget.TextView";
+ private UiDevice mDevice = null;
+ private Context mContext;
+ private ILauncherStrategy mLauncherStrategy = null;
+
+ @Override
+ public void setUp() throws Exception {
+ super.setUp();
+ mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());
+ mContext = InstrumentationRegistry.getTargetContext();
+ mDevice.setOrientationNatural();
+ mLauncherStrategy = LauncherStrategyFactory.getInstance(mDevice).getLauncherStrategy();
+ }
+
+ @Override
+ public void tearDown() throws Exception {
+ mDevice.pressHome();
+ mDevice.unfreezeRotation();
+ mDevice.waitForIdle();
+ super.tearDown();
+ }
+
+ /**
+ * Add and remove a widget from home screen
+ */
+ @LargeTest
+ public void testAddAndRemoveWidget() throws InterruptedException, IOException {
+ // press menu key
+ mDevice.pressMenu();
+ Thread.sleep(LONG_TIMEOUT);
+ mDevice.wait(Until.findObject(By.clazz(WIDGET_TEXT_VIEW)
+ .text("WIDGETS")), LONG_TIMEOUT).click();
+ Thread.sleep(LONG_TIMEOUT);
+ // Long click to add widget
+ mDevice.wait(
+ Until.findObject(
+ By.res(mDevice.getLauncherPackageName(), WIDGET_PREVIEW)),
+ LONG_TIMEOUT).click(1000);
+ mDevice.pressHome();
+ UiObject2 appWidget = mDevice.wait(
+ Until.findObject(By.clazz(APP_WIDGET_VIEW)), LONG_TIMEOUT);
+ assertNotNull("Widget has not been added", appWidget);
+ removeObject(appWidget);
+ appWidget = mDevice.wait(Until.findObject(By.clazz(APP_WIDGET_VIEW)),
+ LONG_TIMEOUT);
+ assertNull("Widget is still there", appWidget);
+ }
+
+ /**
+ * Change Wall Paper
+ */
+ @LargeTest
+ public void testChangeWallPaper() throws InterruptedException, IOException {
+ try {
+ WallpaperManager wallpaperManagerPre = WallpaperManager.getInstance(mContext);
+ wallpaperManagerPre.clear();
+ Thread.sleep(LONG_TIMEOUT);
+ Drawable wallPaperPre = wallpaperManagerPre.getDrawable().getCurrent();
+ // press menu key
+ mDevice.pressMenu();
+ Thread.sleep(LONG_TIMEOUT);
+ mDevice.wait(Until.findObject(By.clazz(WIDGET_TEXT_VIEW)
+ .text("WALLPAPERS")), LONG_TIMEOUT).click();
+ Thread.sleep(LONG_TIMEOUT);
+ // set second wall paper as current wallpaper for home screen and lockscreen
+ mDevice.wait(Until.findObject(By.descContains("Wallpaper 2")), LONG_TIMEOUT).click();
+ mDevice.wait(Until.findObject(By.text("Set wallpaper")), LONG_TIMEOUT).click();
+ UiObject2 homeScreen = mDevice
+ .wait(Until.findObject(By.text("Home screen and lock screen")), LONG_TIMEOUT);
+ if (homeScreen != null) {
+ homeScreen.click();
+ }
+ Thread.sleep(LONG_TIMEOUT);
+ WallpaperManager wallpaperManagerPost = WallpaperManager.getInstance(mContext);
+ Drawable wallPaperPost = wallpaperManagerPost.getDrawable().getCurrent();
+ assertFalse("Wallpaper has not been changed", wallPaperPre.equals(wallPaperPost));
+ } finally {
+ WallpaperManager wallpaperManagerCurrrent = WallpaperManager.getInstance(mContext);
+ wallpaperManagerCurrrent.clear();
+ Thread.sleep(LONG_TIMEOUT);
+ }
+ }
+
+ /**
+ * Add and remove short cut from home screen
+ */
+ @LargeTest
+ public void testAddAndRemoveShortCut() throws InterruptedException {
+ mLauncherStrategy.openAllApps(true);
+ Thread.sleep(LONG_TIMEOUT);
+ // This is a long press and should add the shortcut to the Home screen
+ mDevice.wait(Until.findObject(By.clazz("android.widget.TextView")
+ .desc(APP_NAME)), LONG_TIMEOUT).click(1000);
+ // Searching for the object on the Home screen
+ UiObject2 app = mDevice.wait(Until.findObject(By.text(APP_NAME)), LONG_TIMEOUT);
+ assertNotNull("Apps has been added", app);
+ removeObject(app);
+ app = mDevice.wait(Until.findObject(By.text(APP_NAME)), LONG_TIMEOUT);
+ assertNull(APP_NAME + " is still there", app);
+ }
+
+ /**
+ * Remove object from home screen
+ */
+ private void removeObject(UiObject2 app) throws InterruptedException {
+ // Drag shortcut/widget icon to Remove button which behinds Google Search bar
+ UiObject2 removeButton = mDevice.wait(Until.findObject(By.desc("Google Search")),
+ LONG_TIMEOUT);
+ app.drag(new Point(removeButton.getVisibleCenter().x, removeButton.getVisibleCenter().y),
+ 1000);
+ }
+}
diff --git a/tests/androidbvt/src/com/android/androidbvt/SysUILockScreenTests.java b/tests/androidbvt/src/com/android/androidbvt/SysUILockScreenTests.java
new file mode 100644
index 0000000..28192d6
--- /dev/null
+++ b/tests/androidbvt/src/com/android/androidbvt/SysUILockScreenTests.java
@@ -0,0 +1,213 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.androidbvt;
+
+import android.app.KeyguardManager;
+import android.content.Context;
+import android.content.Intent;
+import android.os.RemoteException;
+import android.provider.Settings;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.uiautomator.By;
+import android.support.test.uiautomator.Direction;
+import android.support.test.uiautomator.UiDevice;
+import android.support.test.uiautomator.UiObject2;
+import android.support.test.uiautomator.Until;
+import android.test.suitebuilder.annotation.LargeTest;
+
+import junit.framework.Assert;
+import junit.framework.TestCase;
+
+public class SysUILockScreenTests extends TestCase {
+ private static final String LAUNCHER_PACKAGE = "com.google.android.googlequicksearchbox";
+ private static final String SYSTEMUI_PACKAGE = "com.android.systemui";
+ private static final String EDIT_TEXT_CLASS_NAME = "android.widget.EditText";
+ private static final int SHORT_TIMEOUT = 200;
+ private static final int LONG_TIMEOUT = 2000;
+ private static final int PIN = 1234;
+ private static final String PASSWORD = "aaaa";
+ private AndroidBvtHelper mABvtHelper = null;
+ private UiDevice mDevice = null;
+ private Context mContext;
+
+ @Override
+ public void setUp() throws Exception {
+ super.setUp();
+ mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());
+ mDevice.freezeRotation();
+ mContext = InstrumentationRegistry.getTargetContext();
+ mABvtHelper = AndroidBvtHelper.getInstance(mDevice, mContext,
+ InstrumentationRegistry.getInstrumentation().getUiAutomation());
+ mDevice.wakeUp();
+ mDevice.pressHome();
+ }
+
+ @Override
+ public void tearDown() throws Exception {
+ mDevice.pressHome();
+ mDevice.unfreezeRotation();
+ mDevice.waitForIdle();
+ super.tearDown();
+ }
+
+ /**
+ * Following test will add PIN for Lock Screen, and remove PIN
+ * @throws Exception
+ */
+ @LargeTest
+ public void testLockScreenPIN() throws Exception {
+ setScreenLock(Integer.toString(PIN), "PIN");
+ sleepAndWakeUpDevice();
+ unlockScreen(Integer.toString(PIN));
+ removeScreenLock(Integer.toString(PIN));
+ Thread.sleep(mABvtHelper.LONG_TIMEOUT);
+ Assert.assertFalse("Lock Screen is still enabled", isLockScreenEnabled());
+ }
+
+ /**
+ * Following test will add password for Lock Screen, and remove Password
+ * @throws Exception
+ */
+ @LargeTest
+ public void testLockScreenPwd() throws Exception {
+ setScreenLock(PASSWORD, "Password");
+ sleepAndWakeUpDevice();
+ unlockScreen(PASSWORD);
+ removeScreenLock(PASSWORD);
+ Thread.sleep(mABvtHelper.LONG_TIMEOUT);
+ Assert.assertFalse("Lock Screen is still enabled", isLockScreenEnabled());
+ }
+
+ /**
+ * Following test will add password for Lock Screen, check Emergency Call Page existence, and
+ * remove password for Lock Screen
+ * @throws Exception
+ */
+ @LargeTest
+ public void testEmergencyCall() throws Exception {
+ setScreenLock(PASSWORD, "Password");
+ sleepAndWakeUpDevice();
+ checkCheckEmergencyCall();
+ unlockScreen(PASSWORD);
+ removeScreenLock(PASSWORD);
+ Thread.sleep(mABvtHelper.LONG_TIMEOUT);
+ Assert.assertFalse("Lock Screen is still enabled", isLockScreenEnabled());
+ }
+
+ /**
+ * Just lock the screen and slide up to unlock
+ */
+ @LargeTest
+ public void testSlideUnlock() throws Exception {
+ sleepAndWakeUpDevice();
+ mDevice.wait(Until.findObject(
+ By.res(SYSTEMUI_PACKAGE, "notification_stack_scroller")), 2000)
+ .swipe(Direction.UP, 1.0f);
+ int counter = 6;
+ UiObject2 workspace = mDevice.findObject(By.res(LAUNCHER_PACKAGE, "workspace"));
+ while (counter-- > 0 && workspace == null) {
+ workspace = mDevice.findObject(By.res(LAUNCHER_PACKAGE, "workspace"));
+ Thread.sleep(500);
+ }
+ assertNotNull("Workspace wasn't found", workspace);
+ }
+
+ /**
+ * Sets the screen lock pin or password
+ * @param pwd text of Password or Pin for lockscreen
+ * @param mode indicate if its password or PIN
+ */
+ private void setScreenLock(String pwd, String mode) throws Exception {
+ navigateToScreenLock();
+ mDevice.wait(Until.findObject(By.text(mode)), mABvtHelper.LONG_TIMEOUT).click();
+ // set up Secure start-up page
+ mDevice.wait(Until.findObject(By.text("No thanks")), mABvtHelper.LONG_TIMEOUT).click();
+ UiObject2 pinField = mDevice.wait(Until.findObject(By.clazz(EDIT_TEXT_CLASS_NAME)),
+ mABvtHelper.LONG_TIMEOUT);
+ pinField.setText(pwd);
+ // enter and verify password
+ mDevice.pressEnter();
+ pinField.setText(pwd);
+ mDevice.pressEnter();
+ mDevice.wait(Until.findObject(By.text("DONE")), mABvtHelper.LONG_TIMEOUT).click();
+ }
+
+ /**
+ * check if Emergency Call page exists
+ */
+ private void checkCheckEmergencyCall() throws Exception {
+ mDevice.pressMenu();
+ mDevice.wait(Until.findObject(By.text("EMERGENCY")), mABvtHelper.LONG_TIMEOUT).click();
+ Thread.sleep(mABvtHelper.LONG_TIMEOUT);
+ UiObject2 dialButton = mDevice.wait(Until.findObject(By.desc("dial")),
+ mABvtHelper.LONG_TIMEOUT);
+ Assert.assertNotNull("Can't reach emergency call page", dialButton);
+ mDevice.pressBack();
+ Thread.sleep(mABvtHelper.LONG_TIMEOUT);
+ }
+
+ private void removeScreenLock(String pwd) throws Exception {
+ navigateToScreenLock();
+ UiObject2 pinField = mDevice.wait(Until.findObject(By.clazz(EDIT_TEXT_CLASS_NAME)),
+ mABvtHelper.LONG_TIMEOUT);
+ pinField.setText(pwd);
+ mDevice.pressEnter();
+ mDevice.wait(Until.findObject(By.text("Swipe")), mABvtHelper.LONG_TIMEOUT).click();
+ mDevice.wait(Until.findObject(By.text("YES, REMOVE")), mABvtHelper.LONG_TIMEOUT).click();
+ }
+
+ private void unlockScreen(String pwd) throws Exception {
+ swipeUp();
+ Thread.sleep(mABvtHelper.SHORT_TIMEOUT);
+ // enter password to unlock screen
+ String command = String.format(" %s %s %s", "input", "text", pwd);
+ mDevice.executeShellCommand(command);
+ mDevice.waitForIdle();
+ Thread.sleep(mABvtHelper.SHORT_TIMEOUT);
+ mDevice.pressEnter();
+ }
+
+ private void navigateToScreenLock() throws Exception {
+ launchSettingsPage(mContext, Settings.ACTION_SECURITY_SETTINGS);
+ mDevice.wait(Until.findObject(By.text("Screen lock")), mABvtHelper.LONG_TIMEOUT).click();
+ }
+
+ private void launchSettingsPage(Context ctx, String pageName) throws Exception {
+ Intent intent = new Intent(pageName);
+ intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ ctx.startActivity(intent);
+ Thread.sleep(mABvtHelper.LONG_TIMEOUT * 2);
+ }
+
+ private void sleepAndWakeUpDevice() throws RemoteException, InterruptedException {
+ mDevice.sleep();
+ Thread.sleep(mABvtHelper.LONG_TIMEOUT);
+ mDevice.wakeUp();
+ }
+
+ private void swipeUp() throws Exception {
+ mDevice.swipe(mDevice.getDisplayWidth() / 2, mDevice.getDisplayHeight(),
+ mDevice.getDisplayWidth() / 2, 0, 30);
+ Thread.sleep(mABvtHelper.SHORT_TIMEOUT);
+ }
+
+ private boolean isLockScreenEnabled() {
+ KeyguardManager km = (KeyguardManager) mContext.getSystemService(Context.KEYGUARD_SERVICE);
+ return km.isKeyguardSecure();
+ }
+}
+
diff --git a/tests/androidbvt/src/com/android/androidbvt/SysUIMultiUserTests.java b/tests/androidbvt/src/com/android/androidbvt/SysUIMultiUserTests.java
new file mode 100644
index 0000000..8da0c27
--- /dev/null
+++ b/tests/androidbvt/src/com/android/androidbvt/SysUIMultiUserTests.java
@@ -0,0 +1,126 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.androidbvt;
+
+import android.app.UiAutomation;
+import android.content.Context;
+import android.os.Environment;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.uiautomator.UiDevice;
+import android.test.suitebuilder.annotation.LargeTest;
+import android.util.Log;
+
+import junit.framework.TestCase;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public class SysUIMultiUserTests extends TestCase {
+ private UiAutomation mUiAutomation = null;
+ private UiDevice mDevice;
+ private Context mContext = null;
+ private AndroidBvtHelper mABvtHelper = null;
+
+ @Override
+ public void setUp() throws Exception {
+ super.setUp();
+ mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());
+ mDevice.setOrientationNatural();
+ mContext = InstrumentationRegistry.getTargetContext();
+ mUiAutomation = InstrumentationRegistry.getInstrumentation().getUiAutomation();
+ mABvtHelper = AndroidBvtHelper.getInstance(mDevice, mContext, mUiAutomation);
+ }
+
+ @Override
+ public void tearDown() throws Exception {
+ mDevice.unfreezeRotation();
+ super.tearDown();
+ }
+
+ /**
+ * Following test creates a second user and verifies user created Also ensures owner has no
+ * access to second user's dir
+ */
+ @LargeTest
+ public void testMultiUserCreate() throws InterruptedException, IOException {
+ int secondUserId = -1;
+ List<String> cmdOut;
+ try {
+ // Ensure there are exactly 1 user
+ cmdOut = mABvtHelper.executeShellCommand("pm list users");
+ assertTrue("There aren't exatcly 1 user", (cmdOut.size() - 1) == 1);
+
+ // Create user
+ cmdOut = mABvtHelper.executeShellCommand("pm create-user test");
+ assertTrue("Output should have 1 line", cmdOut.size() == 1);
+ // output format : Success: created user id 10
+ // Find user id from output above
+ Pattern pattern = Pattern.compile(
+ "(.*)(:)(.*?)(\\d+)");
+ Matcher matcher = pattern.matcher(cmdOut.get(0));
+ if (matcher.find()) {
+ Log.i(mABvtHelper.TEST_TAG, String.format("User Name:%s UserId:%d",
+ matcher.group(1), Integer.parseInt(matcher.group(4))));
+ secondUserId = Integer.parseInt(matcher.group(4));
+ }
+ Thread.sleep(mABvtHelper.SHORT_TIMEOUT);
+
+ // Verify second user id is created
+ cmdOut = mABvtHelper.executeShellCommand("pm list users");
+ // Sample output of "pm list users"
+ // Users:
+ // UserInfo{0:Owner:13} running
+ // UserInfo{18:test:0}
+ assertTrue("There aren't exatcly 2 users", (cmdOut.size() - 1) == 2);
+ // Get Second user id from 'list users' cmd
+ // Ensure that matches with previously created user id
+ pattern = Pattern.compile(
+ "(.*\\{)(\\d+)(:)(.*?)(:)(\\d+)(\\}.*)"); // 2 = id 6 = flag
+ matcher = pattern.matcher(cmdOut.get(2));
+ if (matcher.find()) {
+ assertTrue("Second User id doesn't match",
+ Integer.parseInt(matcher.group(2)) == secondUserId);
+ }
+ Thread.sleep(mABvtHelper.SHORT_TIMEOUT);
+
+ // Ensure owner has no access to second user's directory
+ final File myPath = Environment.getExternalStorageDirectory();
+ final int myId = android.os.Process.myUid() / 100000;
+ assertEquals(String.valueOf(myId), myPath.getName());
+
+ Log.i(mABvtHelper.TEST_TAG, "My path is " + myPath);
+ final File basePath = myPath.getParentFile();
+ for (int i = 0; i < 128; i++) {
+ if (i == myId) {
+ continue;
+ }
+
+ final File otherPath = new File(basePath, String.valueOf(i));
+ assertNull("Owner have access to other user's resources!", otherPath.list());
+ assertFalse("Owner can read other user's content!", otherPath.canRead());
+ }
+ } finally {
+ cmdOut = mABvtHelper.executeShellCommand("pm remove-user " + secondUserId);
+ Log.i(mABvtHelper.TEST_TAG,
+ String.format("Second user has been removed? %s. CmdOut = %s",
+ cmdOut.get(0).equals("Success: removed user"), cmdOut.get(0)));
+ }
+ }
+}
diff --git a/tests/androidbvt/src/com/android/androidbvt/SysUIMultiWindowTests.java b/tests/androidbvt/src/com/android/androidbvt/SysUIMultiWindowTests.java
new file mode 100644
index 0000000..3c3886c
--- /dev/null
+++ b/tests/androidbvt/src/com/android/androidbvt/SysUIMultiWindowTests.java
@@ -0,0 +1,115 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.androidbvt;
+
+import android.app.UiAutomation;
+import android.content.Context;
+import android.content.Intent;
+import android.os.RemoteException;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.uiautomator.UiDevice;
+import android.test.suitebuilder.annotation.LargeTest;
+import android.view.accessibility.AccessibilityWindowInfo;
+
+import junit.framework.TestCase;
+
+import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import android.util.Log;
+public class SysUIMultiWindowTests extends TestCase {
+ private static final String CALCULATOR_PACKAGE = "com.google.android.calculator";
+ private static final String CALCULATOR_ACTIVITY = "com.android.calculator2.Calculator";
+ private static final String SETTINGS_PACKAGE = "com.android.settings";
+ private static final int FULLSCREEN = 1;
+ private static final int SPLITSCREEN = 3;
+ private UiAutomation mUiAutomation = null;
+ private UiDevice mDevice;
+ private Context mContext = null;
+ private AndroidBvtHelper mABvtHelper = null;
+
+ @Override
+ public void setUp() throws Exception {
+ super.setUp();
+ mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());
+ mDevice.setOrientationNatural();
+ mContext = InstrumentationRegistry.getTargetContext();
+ mUiAutomation = InstrumentationRegistry.getInstrumentation().getUiAutomation();
+ mABvtHelper = AndroidBvtHelper.getInstance(mDevice, mContext, mUiAutomation);
+ }
+
+ @Override
+ public void tearDown() throws Exception {
+ mDevice.unfreezeRotation();
+ super.tearDown();
+ }
+
+ /**
+ * Following test ensures any app can be docked from full-screen to split-screen, another can be
+ * launched to multiwindow mode and finally, initial app can be brought back to full-screen
+ */
+ @LargeTest
+ public void testLaunchInMultiwindow() throws InterruptedException, RemoteException {
+ // Launch calculator in full screen
+ Intent launchIntent = mContext.getPackageManager()
+ .getLaunchIntentForPackage(CALCULATOR_PACKAGE);
+ mContext.startActivity(launchIntent);
+ Thread.sleep(mABvtHelper.SHORT_TIMEOUT);
+ int taskId = -1;
+ // Find task id for launched Calculator package
+ List<String> cmdOut = mABvtHelper.executeShellCommand("am stack list");
+ for (String line : cmdOut) {
+ Pattern pattern = Pattern.compile(String.format(".*taskId=([0-9]+): %s/%s.*",CALCULATOR_PACKAGE, CALCULATOR_ACTIVITY));
+ Matcher matcher = pattern.matcher(line);
+ if (matcher.find()) {
+ taskId = Integer.parseInt(matcher.group(1));
+ break;
+ }
+ }
+ assertTrue("Taskid hasn't been found", taskId != -1);
+ // Convert calculator to multiwindow mode
+ mUiAutomation.executeShellCommand(
+ String.format("am stack movetask %d %d true", taskId, SPLITSCREEN));
+ Thread.sleep(mABvtHelper.SHORT_TIMEOUT * 2);
+ // Launch Settings
+ launchIntent = mContext.getPackageManager().getLaunchIntentForPackage(SETTINGS_PACKAGE);
+ mContext.startActivity(launchIntent);
+ Thread.sleep(mABvtHelper.SHORT_TIMEOUT * 2);
+ // Ensure settings is active window
+ List<AccessibilityWindowInfo> windows = mUiAutomation.getWindows();
+ AccessibilityWindowInfo window = windows.get(windows.size() - 1);
+ assertTrue("Settings isn't active window",
+ window.getRoot().getPackageName().equals(SETTINGS_PACKAGE));
+ // Calculate midpoint for Calculator window and click
+ mDevice.click(mDevice.getDisplayHeight() / 4, mDevice.getDisplayWidth() / 2);
+ Thread.sleep(mABvtHelper.SHORT_TIMEOUT);
+ windows = mUiAutomation.getWindows();
+ window = windows.get(windows.size() - 2);
+ assertTrue("Calcualtor isn't active window",
+ window.getRoot().getPackageName().equals(CALCULATOR_PACKAGE));
+ // Make Calculator FullWindow again and ensure Settings package isn't found on window
+ mUiAutomation.executeShellCommand(
+ String.format("am stack movetask %d %d true", taskId, FULLSCREEN));
+ Thread.sleep(mABvtHelper.SHORT_TIMEOUT * 2);
+ windows = mUiAutomation.getWindows();
+ for(int i = 0; i < windows.size() && windows.get(i).getRoot() != null; ++i) {
+ assertFalse("Settings have been found",
+ windows.get(i).getRoot().getPackageName().equals(SETTINGS_PACKAGE));
+ }
+ mDevice.pressHome();
+ }
+}
diff --git a/tests/androidbvt/src/com/android/androidbvt/SysUINotificationShadeTests.java b/tests/androidbvt/src/com/android/androidbvt/SysUINotificationShadeTests.java
new file mode 100644
index 0000000..337ef37
--- /dev/null
+++ b/tests/androidbvt/src/com/android/androidbvt/SysUINotificationShadeTests.java
@@ -0,0 +1,316 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.androidbvt;
+
+import android.app.IntentService;
+import android.app.Notification;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.app.RemoteInput;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Handler;
+import android.os.RemoteException;
+import android.provider.Settings;
+import android.service.notification.StatusBarNotification;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.uiautomator.By;
+import android.support.test.uiautomator.Direction;
+import android.support.test.uiautomator.UiDevice;
+import android.support.test.uiautomator.UiObject2;
+import android.support.test.uiautomator.Until;
+import android.view.inputmethod.InputMethodManager;
+import android.test.suitebuilder.annotation.LargeTest;
+import android.test.suitebuilder.annotation.MediumTest;
+import android.widget.EditText;
+import android.widget.Toast;
+
+import android.util.Log;
+import junit.framework.Assert;
+import junit.framework.TestCase;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+public class SysUINotificationShadeTests extends TestCase {
+ private static final String LOG_TAG = SysUINotificationShadeTests.class.getSimpleName();
+ private static final int SHORT_TIMEOUT = 200;
+ private static final int LONG_TIMEOUT = 2000;
+ private static final int GROUP_NOTIFICATION_ID = 1;
+ private static final int CHILD_NOTIFICATION_ID = 100;
+ private static final int SECOND_CHILD_NOTIFICATION_ID = 101;
+ private static final int NOTIFICATION_ID_2 = 2;
+ private static final String KEY_QUICK_REPLY_TEXT = "quick_reply";
+ private static final String INLINE_REPLY_TITLE = "INLINE REPLY TITLE";
+ private static final String RECEIVER_PKG_NAME = "com.android.systemui";
+ private static final String BUNDLE_GROUP_KEY = "group key ";
+ private UiDevice mDevice = null;
+ private Context mContext;
+ private NotificationManager mNotificationManager;
+ private ContentResolver mResolver;
+
+ private enum QuickSettingTiles {
+ WIFI("Wi-Fi"), SIM("SIM"), DND("Do not disturb"), BATTERY("Battery"),
+ FLASHLIGHT("Flashlight"), SCREEN("screen"), BLUETOOTH("Bluetooth"),
+ AIRPLANE("Airplane mode"), LOCATION("Location");
+
+ private final String name;
+
+ private QuickSettingTiles(String name) {
+ this.name = name;
+ }
+
+ public String getName() {
+ return this.name;
+ }
+ };
+
+ @Override
+ public void setUp() throws Exception {
+ super.setUp();
+ mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());
+ mContext = InstrumentationRegistry.getTargetContext();
+ mResolver = mContext.getContentResolver();
+ mDevice.setOrientationNatural();
+ mNotificationManager = (NotificationManager) mContext
+ .getSystemService(Context.NOTIFICATION_SERVICE);
+ mDevice.pressHome();
+ mNotificationManager.cancelAll();
+ }
+
+ @Override
+ public void tearDown() throws Exception {
+ mNotificationManager.cancelAll();
+ mDevice.pressHome();
+ mDevice.unfreezeRotation();
+ mDevice.waitForIdle();
+ super.tearDown();
+ }
+
+ /**
+ * Following test will create notifications, and verify notification can be expanded and
+ * redacted
+ */
+ @LargeTest
+ public void testNotifications() throws Exception {
+ // test receive notification and expand/redact notification
+ verifyReceiveAndExpandRedactNotification();
+ // test inline notification and dismiss notification
+ verifyInlineAndDimissNotification();
+ }
+
+ /**
+ * Following test will open Quick Setting shade, and verify icons in the shade
+ */
+ @MediumTest
+ public void testQuickSettings() throws Exception {
+ mDevice.openQuickSettings();
+ Thread.sleep(LONG_TIMEOUT * 2);
+ // Verify quick settings are displayed on the phone screen.
+ for (QuickSettingTiles tile : QuickSettingTiles.values()) {
+ UiObject2 quickSettingTile = mDevice.wait(
+ Until.findObject(By.descContains(tile.getName())),
+ SHORT_TIMEOUT);
+ assertNotNull(String.format("%s did not load correctly", tile.getName()),
+ quickSettingTile);
+ }
+ // Verify tapping on Settings icon in Quick settings launches Settings.
+ mDevice.wait(Until.findObject(By.descContains("Open settings.")), LONG_TIMEOUT)
+ .click();
+ UiObject2 settingHeading = mDevice.wait(Until.findObject(By.text("Settings")),
+ LONG_TIMEOUT);
+ assertNotNull("Setting menu has not loaded correctly", settingHeading);
+ }
+
+ private void verifyReceiveAndExpandRedactNotification() throws Exception {
+ List<Integer> lists = new ArrayList<Integer>(Arrays.asList(GROUP_NOTIFICATION_ID,
+ CHILD_NOTIFICATION_ID, SECOND_CHILD_NOTIFICATION_ID));
+ sendBundlingNotifications(lists, BUNDLE_GROUP_KEY);
+ Thread.sleep(LONG_TIMEOUT);
+ swipeDown();
+ UiObject2 obj = mDevice.wait(
+ Until.findObject(By.textContains(lists.get(1).toString())),
+ LONG_TIMEOUT);
+ int currentY = obj.getVisibleCenter().y;
+ mDevice.wait(Until.findObject(By.res("android:id/expand_button")), LONG_TIMEOUT * 2)
+ .click();
+ obj = mDevice.wait(Until.findObject(By.textContains(lists.get(0).toString())),
+ LONG_TIMEOUT);
+ assertFalse("The notifications has not been bundled",
+ obj.getVisibleCenter().y == currentY);
+ mDevice.wait(Until.findObject(By.res("android:id/expand_button")), LONG_TIMEOUT).click();
+ obj = mDevice.wait(Until.findObject(By.textContains(lists.get(1).toString())),
+ LONG_TIMEOUT);
+ assertTrue("The notifications can not be redacted",
+ obj.getVisibleCenter().y == currentY);
+ mNotificationManager.cancelAll();
+ }
+
+ private void verifyInlineAndDimissNotification() throws Exception {
+ sendNotificationsWithInLineReply(NOTIFICATION_ID_2, INLINE_REPLY_TITLE);
+ Thread.sleep(LONG_TIMEOUT);
+ mDevice.openNotification();
+ mDevice.wait(Until.findObject(By.text("REPLY")), LONG_TIMEOUT).click();
+ UiObject2 replyBox = mDevice.wait(
+ Until.findObject(By.res(RECEIVER_PKG_NAME, "remote_input_send")),
+ LONG_TIMEOUT);
+ InputMethodManager imm = (InputMethodManager) mContext
+ .getSystemService(Context.INPUT_METHOD_SERVICE);
+ if (!imm.isAcceptingText()) {
+ assertNotNull("Keyboard for inline reply has not loaded correctly", replyBox);
+ }
+ UiObject2 obj = mDevice.wait(Until.findObject(By.text(INLINE_REPLY_TITLE)),
+ LONG_TIMEOUT);
+ obj.swipe(Direction.LEFT, 1.0f);
+ Thread.sleep(LONG_TIMEOUT);
+ if (checkNotificationExistence(NOTIFICATION_ID_2)) {
+ fail(String.format("Notification %s has not been dismissed", NOTIFICATION_ID_2));
+ }
+ }
+
+ /**
+ * send out a group of notifications
+ * @param lists notification list for a group of notifications which includes two child
+ * notifications and one summary notification
+ * @param groupKey the group key of group notification
+ */
+ private void sendBundlingNotifications(List<Integer> lists, String groupKey) throws Exception {
+ Notification childNotification = new Notification.Builder(mContext)
+ .setContentTitle(lists.get(1).toString())
+ .setSmallIcon(R.drawable.stat_notify_email)
+ .setContentText("test1")
+ .setWhen(System.currentTimeMillis())
+ .setGroup(groupKey)
+ .build();
+ mNotificationManager.notify(lists.get(1),
+ childNotification);
+ childNotification = new Notification.Builder(mContext)
+ .setContentTitle(lists.get(2).toString())
+ .setContentText("test2")
+ .setSmallIcon(R.drawable.stat_notify_email)
+ .setWhen(System.currentTimeMillis())
+ .setGroup(groupKey)
+ .build();
+ mNotificationManager.notify(lists.get(2),
+ childNotification);
+ Notification notification = new Notification.Builder(mContext)
+ .setContentTitle(lists.get(0).toString())
+ .setSubText(groupKey)
+ .setSmallIcon(R.drawable.stat_notify_email)
+ .setGroup(groupKey)
+ .setGroupSummary(true)
+ .build();
+ mNotificationManager.notify(lists.get(0),
+ notification);
+ }
+
+ /**
+ * send out a notification with inline reply
+ *
+ * @param notificationId An identifier for this notification
+ * @param title notification title
+ */
+ private void sendNotificationsWithInLineReply(int notificationId, String title) {
+ Notification.Action action = new Notification.Action.Builder(
+ R.drawable.stat_notify_email, "Reply", ToastService.getPendingIntent(mContext,
+ title))
+ .addRemoteInput(new RemoteInput.Builder(KEY_QUICK_REPLY_TEXT)
+ .setLabel("Quick reply").build())
+ .build();
+ Notification.Builder n = new Notification.Builder(mContext)
+ .setContentTitle(Integer.toString(notificationId))
+ .setContentText(title)
+ .setWhen(System.currentTimeMillis())
+ .setSmallIcon(R.drawable.stat_notify_email)
+ .addAction(action)
+ .setPriority(Notification.PRIORITY_HIGH)
+ .setDefaults(Notification.DEFAULT_VIBRATE);
+ mNotificationManager.notify(notificationId, n.build());
+ }
+
+ private boolean checkNotificationExistence(int id) throws Exception {
+ boolean isFound = false;
+ for (int tries = 3; tries-- > 0;) {
+ isFound = false;
+ StatusBarNotification[] sbns = mNotificationManager.getActiveNotifications();
+ for (StatusBarNotification sbn : sbns) {
+ if (sbn.getId() == id) {
+ isFound = true;
+ break;
+ }
+ }
+ if (isFound) {
+ break;
+ }
+ Thread.sleep(SHORT_TIMEOUT);
+ }
+ Log.i(LOG_TAG, "checkNotificationExistence..." + isFound);
+ return isFound;
+ }
+
+ private void swipeDown() throws Exception {
+ mDevice.swipe(mDevice.getDisplayWidth() / 2, 0, mDevice.getDisplayWidth() / 2,
+ mDevice.getDisplayHeight() / 2 + 50, 20);
+ Thread.sleep(SHORT_TIMEOUT);
+ }
+
+ public static class ToastService extends IntentService {
+ private static final String TAG = "ToastService";
+ private static final String ACTION_TOAST = "toast";
+ private Handler handler;
+
+ public ToastService() {
+ super(TAG);
+ }
+
+ public ToastService(String name) {
+ super(name);
+ }
+
+ @Override
+ public int onStartCommand(Intent intent, int flags, int startId) {
+ handler = new Handler();
+ return super.onStartCommand(intent, flags, startId);
+ }
+
+ @Override
+ protected void onHandleIntent(Intent intent) {
+ if (intent.hasExtra("text")) {
+ final String text = intent.getStringExtra("text");
+ handler.post(new Runnable() {
+ @Override
+ public void run() {
+ Toast.makeText(ToastService.this, text, Toast.LENGTH_LONG).show();
+ Log.v(TAG, "toast " + text);
+ }
+ });
+ }
+ }
+
+ public static PendingIntent getPendingIntent(Context context, String text) {
+ Intent toastIntent = new Intent(context, ToastService.class);
+ toastIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ toastIntent.setAction(ACTION_TOAST + ":" + text); // one per toast message
+ toastIntent.putExtra("text", text);
+ PendingIntent pi = PendingIntent.getService(
+ context, 58, toastIntent, PendingIntent.FLAG_UPDATE_CURRENT);
+ return pi;
+ }
+ }
+}
diff --git a/tests/androidbvt/src/com/android/androidbvt/app/MediaPlaybackTestApp.java b/tests/androidbvt/src/com/android/androidbvt/app/MediaPlaybackTestApp.java
new file mode 100644
index 0000000..bfb239b
--- /dev/null
+++ b/tests/androidbvt/src/com/android/androidbvt/app/MediaPlaybackTestApp.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.androidbvt.app;
+
+import android.app.Activity;
+import android.os.Bundle;
+import android.view.SurfaceHolder;
+import android.view.SurfaceView;
+
+import com.android.androidbvt.R;
+
+public class MediaPlaybackTestApp extends Activity {
+
+ private SurfaceView mSurfaceView;
+
+ @Override
+ public void onCreate(Bundle icicle) {
+ super.onCreate(icicle);
+ setContentView(R.layout.surface_view);
+ mSurfaceView = (SurfaceView)findViewById(R.id.surface_view);
+ }
+
+ public SurfaceHolder getSurfaceHolder() {
+ return mSurfaceView.getHolder();
+ }
+}
diff --git a/tests/camera/aupt-profile/Android.mk b/tests/camera/aupt-profile/Android.mk
new file mode 100644
index 0000000..7ba85d6
--- /dev/null
+++ b/tests/camera/aupt-profile/Android.mk
@@ -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.
+#
+LOCAL_PATH := $(call my-dir)
+
+include $(CLEAR_VARS)
+LOCAL_MODULE := camera-profile-test
+LOCAL_JAVA_LIBRARIES := android.test.runner ub-uiautomator AuptLib
+LOCAL_STATIC_JAVA_LIBRARIES := app-helpers
+LOCAL_SRC_FILES := $(call all-java-files-under, src)
+LOCAL_MODULE_PATH := $(TARGET_OUT_DATA)/local/tmp
+
+include $(BUILD_JAVA_LIBRARY)
diff --git a/tests/camera/aupt-profile/src/com/android/test/uiautomator/aupt/camera/CameraStress4KTest.java b/tests/camera/aupt-profile/src/com/android/test/uiautomator/aupt/camera/CameraStress4KTest.java
new file mode 100644
index 0000000..a58b773
--- /dev/null
+++ b/tests/camera/aupt-profile/src/com/android/test/uiautomator/aupt/camera/CameraStress4KTest.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.test.uiautomator.aupt.camera;
+
+import android.support.test.aupt.AuptTestCase;
+import android.platform.test.helpers.GoogleCameraHelperImpl;
+
+/**
+ * Tests for the camera
+ */
+public class CameraStress4KTest extends AuptTestCase {
+ private GoogleCameraHelperImpl mHelper;
+ private int videoTimeMS = 5 * 1000;
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ protected void setUp() throws Exception {
+ super.setUp();
+ mHelper = new GoogleCameraHelperImpl(getInstrumentation());
+ if (getParams().containsKey("video-duration")) {
+ videoTimeMS = Integer.parseInt(getParams().getString("video-duration"));
+ }
+ mHelper.open();
+ }
+
+ public void testCameraStressVideoBack4K() {
+ mHelper.goToBackCamera();
+ mHelper.goToVideoMode();
+ mHelper.set4KMode(GoogleCameraHelperImpl.VIDEO_4K_MODE_ON);
+ mHelper.captureVideo(videoTimeMS);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void tearDown() throws Exception {
+ mHelper.exit();
+ super.tearDown();
+ }
+}
diff --git a/tests/camera/aupt-profile/src/com/android/test/uiautomator/aupt/camera/CameraStressHDRTest.java b/tests/camera/aupt-profile/src/com/android/test/uiautomator/aupt/camera/CameraStressHDRTest.java
new file mode 100644
index 0000000..01107b5
--- /dev/null
+++ b/tests/camera/aupt-profile/src/com/android/test/uiautomator/aupt/camera/CameraStressHDRTest.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.test.uiautomator.aupt.camera;
+
+import android.support.test.aupt.AuptTestCase;
+import android.platform.test.helpers.GoogleCameraHelperImpl;
+
+/**
+ * Tests for the camera
+ */
+public class CameraStressHDRTest extends AuptTestCase {
+ private GoogleCameraHelperImpl mHelper;
+ private int videoTimeMS = 5 * 1000;
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ protected void setUp() throws Exception {
+ super.setUp();
+ mHelper = new GoogleCameraHelperImpl(getInstrumentation());
+ if (getParams().containsKey("video-duration")) {
+ videoTimeMS = Integer.parseInt(getParams().getString("video-duration"));
+ }
+ mHelper.open();
+ }
+
+ public void testCameraStressStillCaptureBackHDR() {
+ mHelper.goToBackCamera();
+ mHelper.goToCameraMode();
+ mHelper.setHdrMode(GoogleCameraHelperImpl.HDR_MODE_ON);
+ mHelper.capturePhoto();
+
+ }
+
+ public void testCameraStressStillCaptureFrontHDR() {
+ mHelper.goToFrontCamera();
+ mHelper.goToCameraMode();
+ mHelper.setHdrMode(GoogleCameraHelperImpl.HDR_MODE_ON);
+ mHelper.capturePhoto();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void tearDown() throws Exception {
+ mHelper.exit();
+ super.tearDown();
+ }
+}
diff --git a/tests/camera/aupt-profile/src/com/android/test/uiautomator/aupt/camera/CameraStressHFRTest.java b/tests/camera/aupt-profile/src/com/android/test/uiautomator/aupt/camera/CameraStressHFRTest.java
new file mode 100644
index 0000000..a39fb1f
--- /dev/null
+++ b/tests/camera/aupt-profile/src/com/android/test/uiautomator/aupt/camera/CameraStressHFRTest.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.test.uiautomator.aupt.camera;
+
+import android.support.test.aupt.AuptTestCase;
+import android.platform.test.helpers.GoogleCameraHelperImpl;
+
+/**
+ * Tests for the camera
+ */
+public class CameraStressHFRTest extends AuptTestCase {
+ private GoogleCameraHelperImpl mHelper;
+ private int videoTimeMS = 5 * 1000;
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ protected void setUp() throws Exception {
+ super.setUp();
+ mHelper = new GoogleCameraHelperImpl(getInstrumentation());
+ if (getParams().containsKey("video-duration")) {
+ videoTimeMS = Integer.parseInt(getParams().getString("video-duration"));
+ }
+ mHelper.open();
+ }
+
+ public void testCameraStressVideoBackHFR120FPS() {
+ mHelper.goToBackCamera();
+ mHelper.goToVideoMode();
+ mHelper.setHFRMode(GoogleCameraHelperImpl.HFR_MODE_120_FPS);
+ mHelper.captureVideo(videoTimeMS);
+ mHelper.setHFRMode(GoogleCameraHelperImpl.HFR_MODE_OFF);
+ }
+
+ public void testCameraStressVideoBackHFR240FPS() {
+ mHelper.goToBackCamera();
+ mHelper.goToVideoMode();
+ mHelper.setHFRMode(GoogleCameraHelperImpl.HFR_MODE_240_FPS);
+ mHelper.captureVideo(videoTimeMS);
+ mHelper.setHFRMode(GoogleCameraHelperImpl.HFR_MODE_OFF);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void tearDown() throws Exception {
+ mHelper.exit();
+ super.tearDown();
+ }
+}
diff --git a/tests/camera/aupt-profile/src/com/android/test/uiautomator/aupt/camera/CameraStressSnapshotTest.java b/tests/camera/aupt-profile/src/com/android/test/uiautomator/aupt/camera/CameraStressSnapshotTest.java
new file mode 100644
index 0000000..7c40ae6
--- /dev/null
+++ b/tests/camera/aupt-profile/src/com/android/test/uiautomator/aupt/camera/CameraStressSnapshotTest.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.test.uiautomator.aupt.camera;
+
+import android.support.test.aupt.AuptTestCase;
+import android.platform.test.helpers.GoogleCameraHelperImpl;
+
+/**
+ * Tests for the camera
+ */
+public class CameraStressSnapshotTest extends AuptTestCase {
+ private GoogleCameraHelperImpl mHelper;
+ private int videoTimeMs = 5 * 1000;
+ private int snapshotStartTimeMs = 1 * 1000;
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ protected void setUp() throws Exception {
+ super.setUp();
+ mHelper = new GoogleCameraHelperImpl(getInstrumentation());
+ if (getParams().containsKey("video-duration")) {
+ videoTimeMs = Integer.parseInt(getParams().getString("video-duration"));
+ }
+ mHelper.open();
+ }
+
+ public void testCameraStressVideoBackSnapshot() {
+ mHelper.goToBackCamera();
+ mHelper.goToVideoMode();
+ mHelper.snapshotVideo(videoTimeMs, snapshotStartTimeMs);
+ }
+
+ public void testCameraStressVideoFrontSnapshot() {
+ mHelper.goToFrontCamera();
+ mHelper.goToVideoMode();
+ mHelper.snapshotVideo(videoTimeMs, snapshotStartTimeMs);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void tearDown() throws Exception {
+ mHelper.exit();
+ super.tearDown();
+ }
+}
diff --git a/tests/camera/aupt-profile/src/com/android/test/uiautomator/aupt/camera/CameraStressTest.java b/tests/camera/aupt-profile/src/com/android/test/uiautomator/aupt/camera/CameraStressTest.java
new file mode 100644
index 0000000..effb38d
--- /dev/null
+++ b/tests/camera/aupt-profile/src/com/android/test/uiautomator/aupt/camera/CameraStressTest.java
@@ -0,0 +1,80 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.test.uiautomator.aupt.camera;
+
+import android.support.test.aupt.AuptTestCase;
+import android.platform.test.helpers.GoogleCameraHelperImpl;
+
+/**
+ * Tests for the camera
+ */
+public class CameraStressTest extends AuptTestCase {
+ private GoogleCameraHelperImpl mHelper;
+ private int videoTimeMS = 5 * 1000;
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ protected void setUp() throws Exception {
+ super.setUp();
+ mHelper = new GoogleCameraHelperImpl(getInstrumentation());
+ if (getParams().containsKey("video-duration")) {
+ videoTimeMS = Integer.parseInt(getParams().getString("video-duration"));
+ }
+ mHelper.open();
+ }
+
+ public void testCameraStressStillCaptureBack() {
+ mHelper.goToBackCamera();
+ mHelper.goToCameraMode();
+ mHelper.setHdrMode(GoogleCameraHelperImpl.HDR_MODE_OFF);
+ mHelper.capturePhoto();
+ }
+
+ public void testCameraStressStillCaptureFront() {
+ mHelper.goToFrontCamera();
+ mHelper.goToCameraMode();
+ mHelper.setHdrMode(GoogleCameraHelperImpl.HDR_MODE_OFF);
+ mHelper.capturePhoto();
+ }
+
+ public void testCameraStressVideoBasicBack() {
+ mHelper.goToBackCamera();
+ mHelper.goToVideoMode();
+ //TODO: enable this once b/28723710 is fixed.
+ //mHelper.set4KMode(GoogleCameraHelperImpl.VIDEO_HD_1080);
+ mHelper.captureVideo(videoTimeMS);
+ }
+
+ public void testCameraStressVideoBasicFront() {
+ mHelper.goToFrontCamera();
+ mHelper.goToVideoMode();
+ //TODO: enable this once b/28723710 is fixed
+ //mHelper.set4KMode(GoogleCameraHelperImpl.VIDEO_HD_1080);
+ mHelper.captureVideo(videoTimeMS);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void tearDown() throws Exception {
+ mHelper.exit();
+ super.tearDown();
+ }
+}
diff --git a/tests/functional/applinktests/Android.mk b/tests/functional/applinktests/Android.mk
new file mode 100644
index 0000000..6644ac9
--- /dev/null
+++ b/tests/functional/applinktests/Android.mk
@@ -0,0 +1,29 @@
+# Copyright 2016 Google Inc. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT 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 := tests
+
+LOCAL_SDK_VERSION := current
+
+LOCAL_SRC_FILES := $(call all-subdir-java-files)
+LOCAL_JAVA_LIBRARIES := android.test.runner
+LOCAL_STATIC_JAVA_LIBRARIES := launcher-helper-lib ub-uiautomator
+
+LOCAL_PACKAGE_NAME := AppLinkFunctionalTests
+LOCAL_CERTIFICATE := platform
+
+include $(BUILD_PACKAGE)
diff --git a/tests/functional/applinktests/AndroidManifest.xml b/tests/functional/applinktests/AndroidManifest.xml
new file mode 100644
index 0000000..a277696
--- /dev/null
+++ b/tests/functional/applinktests/AndroidManifest.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Copyright 2016 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.android.functional.applinktests" >
+
+ <uses-sdk android:minSdkVersion="19"
+ android:targetSdkVersion="24" />
+ <application>
+ <uses-library android:name="android.test.runner"/>
+ </application>
+ <instrumentation
+ android:name="android.test.InstrumentationTestRunner"
+ android:targetPackage="com.android.functional.applinktests"
+ android:label="AppLink Functional Tests" />
+</manifest>
diff --git a/tests/functional/applinktests/src/com/android/functional/applinktests/AppLinkTests.java b/tests/functional/applinktests/src/com/android/functional/applinktests/AppLinkTests.java
new file mode 100644
index 0000000..7aa9141
--- /dev/null
+++ b/tests/functional/applinktests/src/com/android/functional/applinktests/AppLinkTests.java
@@ -0,0 +1,262 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.functional.applinktests;
+
+import android.app.UiAutomation;
+import android.content.Context;
+import android.content.Intent;
+import android.os.ParcelFileDescriptor;
+import android.support.test.uiautomator.By;
+import android.support.test.uiautomator.UiDevice;
+import android.support.test.uiautomator.UiObject2;
+import android.support.test.uiautomator.Until;
+import android.test.InstrumentationTestCase;
+import android.util.Log;
+import android.view.accessibility.AccessibilityWindowInfo;
+
+import java.io.BufferedReader;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.util.ArrayList;
+import java.util.List;
+
+public class AppLinkTests extends InstrumentationTestCase {
+ public final String TEST_TAG = "AppLinkFunctionalTest";
+ public final String TEST_PKG_NAME = "com.android.applinktestapp";
+ public final String TEST_APP_NAME = "AppLinkTestApp";
+ public final String YOUTUBE_PKG_NAME = "com.google.android.youtube";
+ public final String HTTP_SCHEME = "http";
+ public final String TEST_HOST = "test.com";
+ public final int TIMEOUT = 1000;
+ private UiDevice mDevice = null;
+ private Context mContext = null;
+ private UiAutomation mUiAutomation = null;
+
+ @Override
+ protected void setUp() throws Exception {
+ super.setUp();
+ mDevice = UiDevice.getInstance(getInstrumentation());
+ mContext = getInstrumentation().getContext();
+ mUiAutomation = getInstrumentation().getUiAutomation();
+ mDevice.setOrientationNatural();
+ }
+
+ // Ensures that default app link setting set to 'undefined' for 3P apps
+ public void testDefaultAppLinkSettting() throws InterruptedException {
+ String out = getAppLink(TEST_PKG_NAME);
+ assertTrue("Default app link not set to 'undefined' mode", "undefined".equals(out));
+ openLink(HTTP_SCHEME, TEST_HOST);
+ ensureDisambigPresent();
+ }
+
+ // User sets an app to open for a link 'Just Once' and disambig shows up next time too
+ // Once user set to 'always' disambig never shows up
+ public void testUserSetToJustOnceAndAlways() throws InterruptedException {
+ openLink(HTTP_SCHEME, TEST_HOST);
+ ensureDisambigPresent();
+ mDevice.wait(Until.findObject(By.text("AppLinkTestApp")), TIMEOUT).click();
+ mDevice.wait(Until.findObject(By.res("android:id/button_once")), TIMEOUT).click();
+ Thread.sleep(TIMEOUT);
+ verifyForegroundAppPackage(TEST_PKG_NAME);
+ openLink(HTTP_SCHEME, TEST_HOST);
+ assertTrue("Target app isn't the default choice",
+ mDevice.wait(Until.hasObject(By.text("Open with AppLinkTestApp")), TIMEOUT));
+ mDevice.wait(Until.findObject(By.res("android:id/button_once")), TIMEOUT)
+ .clickAndWait(Until.newWindow(), TIMEOUT);
+ Thread.sleep(TIMEOUT);
+ verifyForegroundAppPackage(TEST_PKG_NAME);
+ mDevice.pressHome();
+ // Ensure it doesn't change on second attempt
+ openLink(HTTP_SCHEME, TEST_HOST);
+ // Ensure disambig is present
+ mDevice.wait(Until.findObject(By.res("android:id/button_always")), TIMEOUT).click();
+ mDevice.pressHome();
+ // User chose to set to always and intent is opened in target direct
+ openLink(HTTP_SCHEME, TEST_HOST);
+ verifyForegroundAppPackage(TEST_PKG_NAME);
+ }
+
+ // Ensure verified app always open even candidate but unverified app set to 'always'
+ public void testVerifiedAppOpenWhenNotVerifiedSetToAlways() throws InterruptedException {
+ setAppLink(TEST_PKG_NAME, "always");
+ setAppLink(YOUTUBE_PKG_NAME, "always");
+ Thread.sleep(TIMEOUT);
+ openLink(HTTP_SCHEME, "youtube.com");
+ verifyForegroundAppPackage(YOUTUBE_PKG_NAME);
+ }
+
+ // Ensure verified app always open even one candidate but unverified app set to 'ask'
+ public void testVerifiedAppOpenWhenUnverifiedSetToAsk() throws InterruptedException {
+ setAppLink(TEST_PKG_NAME, "ask");
+ setAppLink(YOUTUBE_PKG_NAME, "always");
+ String out = getAppLink(YOUTUBE_PKG_NAME);
+ openLink(HTTP_SCHEME, "youtube.com");
+ verifyForegroundAppPackage(YOUTUBE_PKG_NAME);
+ }
+
+ // Ensure disambig is shown if verified app set to 'never' and unverified app set to 'ask'
+ public void testUserChangeVerifiedLinkHandler() throws InterruptedException {
+ setAppLink(TEST_PKG_NAME, "ask");
+ setAppLink(YOUTUBE_PKG_NAME, "never");
+ Thread.sleep(TIMEOUT);
+ openLink(HTTP_SCHEME, "youtube.com");
+ ensureDisambigPresent();
+ setAppLink(YOUTUBE_PKG_NAME, "always");
+ Thread.sleep(TIMEOUT);
+ openLink(HTTP_SCHEME, "youtube.com");
+ verifyForegroundAppPackage(YOUTUBE_PKG_NAME);
+ }
+
+ // Ensure unverified app always open when unverified app set to always but verified app set to
+ // never
+ public void testTestAppSetToAlwaysVerifiedSetToNever() throws InterruptedException {
+ setAppLink(TEST_PKG_NAME, "always");
+ setAppLink(YOUTUBE_PKG_NAME, "never");
+ Thread.sleep(TIMEOUT);
+ openLink(HTTP_SCHEME, "youtube.com");
+ verifyForegroundAppPackage(TEST_PKG_NAME);
+ }
+
+ // Test user can modify 'App Link Settings'
+ public void testSettingsChangeUI() throws InterruptedException {
+ Intent intent_as = new Intent(
+ android.provider.Settings.ACTION_APPLICATION_SETTINGS);
+ mContext.startActivity(intent_as);
+ Thread.sleep(TIMEOUT * 5);
+ mDevice.wait(Until.findObject(By.res("com.android.settings:id/advanced")), TIMEOUT)
+ .clickAndWait(Until.newWindow(), TIMEOUT);
+ mDevice.wait(Until.findObject(By.text("Opening links")), TIMEOUT)
+ .clickAndWait(Until.newWindow(), TIMEOUT);
+ mDevice.wait(Until.findObject(By.text("AppLinkTestApp")), TIMEOUT)
+ .clickAndWait(Until.newWindow(), TIMEOUT);
+ mDevice.wait(Until.findObject(By.text("Open supported links")), TIMEOUT)
+ .clickAndWait(Until.newWindow(), TIMEOUT);
+ mDevice.wait(Until.findObject(By.text("Open in this app")), TIMEOUT)
+ .clickAndWait(Until.newWindow(), TIMEOUT);
+ String out = getAppLink(TEST_PKG_NAME);
+ Thread.sleep(TIMEOUT);
+ assertTrue(String.format("Default app link not set to 'always ask' rather set to %s", out),
+ "always".equals(out));
+ mDevice.wait(Until.findObject(By.text("Open supported links")), TIMEOUT)
+ .clickAndWait(Until.newWindow(), TIMEOUT);
+ mDevice.wait(Until.findObject(By.text("Don’t open in this app")), TIMEOUT)
+ .clickAndWait(Until.newWindow(), TIMEOUT);
+ out = getAppLink(TEST_PKG_NAME);
+ Thread.sleep(TIMEOUT);
+ assertTrue(String.format("Default app link not set to 'never' rather set to %s", out),
+ "never".equals(out));
+ mDevice.wait(Until.findObject(By.text("Open supported links")), TIMEOUT)
+ .clickAndWait(Until.newWindow(), TIMEOUT);
+ mDevice.wait(Until.findObject(By.text("Ask every time")), TIMEOUT)
+ .clickAndWait(Until.newWindow(), TIMEOUT);
+ out = getAppLink(TEST_PKG_NAME);
+ Thread.sleep(TIMEOUT);
+ assertTrue(String.format("Default app link not set to 'always ask' rather set to %s", out),
+ "always ask".equals(out));
+ }
+
+ // Ensure system apps that claim to open always for set to always
+ public void testSysappAppLinkSettings() {
+ // List of system app that are set to 'Always' for certain urls
+ List<String> alwaysOpenApps = new ArrayList<String>();
+ alwaysOpenApps.add("com.google.android.apps.docs.editors.docs"); // Docs
+ alwaysOpenApps.add("com.google.android.apps.docs.editors.sheets"); // Sheets
+ alwaysOpenApps.add("com.google.android.apps.docs.editors.slides"); // Slides
+ alwaysOpenApps.add("com.google.android.apps.docs"); // Drive
+ alwaysOpenApps.add("com.google.android.youtube"); // YouTube
+ for (String alwaysOpenApp : alwaysOpenApps) {
+ String out = getAppLink(alwaysOpenApp);
+ assertTrue(String.format("App link for %s should be set to 'Always'", alwaysOpenApp),
+ "always".equalsIgnoreCase(out));
+ }
+ }
+
+ @Override
+ protected void tearDown() throws Exception {
+ executeShellCommand("pm clear " + TEST_PKG_NAME);
+ executeShellCommand("pm clear " + YOUTUBE_PKG_NAME);
+ executeShellCommand("pm set-app-link " + TEST_PKG_NAME + " undefined");
+ executeShellCommand("pm set-app-link " + YOUTUBE_PKG_NAME + " always");
+ Thread.sleep(TIMEOUT);
+ mDevice.unfreezeRotation();
+ mDevice.pressHome();
+ super.tearDown();
+ }
+
+ // Start an intent to open a test link
+ private void openLink(String scheme, String host) throws InterruptedException {
+ String out = executeShellCommand(String.format(
+ "am start -a android.intent.action.VIEW -d %s://%s/", scheme, host));
+ Thread.sleep(TIMEOUT * 2);
+ }
+
+ // If framework identifies more than one app that can handle a link intent, framework presents a
+ // window to user to choose the app to handle the intent.
+ // This is also known as 'disambig' window
+ private void ensureDisambigPresent() {
+ assertNotNull("Disambig dialog is not shown",
+ mDevice.wait(Until.hasObject(By.res("android:id/resolver_list")),
+ TIMEOUT));
+ List<UiObject2> resolverApps = mDevice.wait(Until.findObjects(By.res("android:id/text1")),
+ TIMEOUT);
+ assertTrue("There aren't exactly 2 apps to resolve", resolverApps.size() == 2);
+ assertTrue("Resolver apps aren't correct",
+ "AppLinkTestApp".equals(resolverApps.get(0).getText()) &&
+ "Chrome".equals(resolverApps.get(1).getText()));
+ }
+
+ // Verifies that a certain package is in foreground
+ private void verifyForegroundAppPackage(String pkgName) throws InterruptedException {
+ int counter = 3;
+ List<AccessibilityWindowInfo> windows = null;
+ while (--counter > 0 && windows == null) {
+ windows = mUiAutomation.getWindows();
+ Thread.sleep(TIMEOUT);
+ }
+ assertTrue(String.format("%s is not top activity", "youtube"),
+ windows.get(windows.size() - 1).getRoot().getPackageName().equals(pkgName));
+ }
+
+ // Gets app link for a package
+ private String getAppLink(String pkgName) {
+ return executeShellCommand(String.format("pm get-app-link %s", pkgName));
+ }
+
+ // Sets Openlink settings for a package to passed value
+ private void setAppLink(String pkgName, String valueToBeSet) {
+ executeShellCommand(String.format("pm set-app-link %s %s", pkgName, valueToBeSet));
+ }
+
+ // Executes 'adb shell' command. Converts ParcelFileDescriptor output to String
+ private String executeShellCommand(String command) {
+ if (command == null || command.isEmpty()) {
+ return null;
+ }
+ ParcelFileDescriptor pfd = mUiAutomation.executeShellCommand(command);
+ try (BufferedReader reader = new BufferedReader(
+ new InputStreamReader(new FileInputStream(pfd.getFileDescriptor())))) {
+ String str = reader.readLine();
+ Log.d(TEST_TAG, String.format("Executing command: %s", command));
+ return str;
+ } catch (IOException e) {
+ Log.e(TEST_TAG, e.getMessage());
+ }
+
+ return null;
+ }
+}
diff --git a/tests/functional/appsmoke/Android.mk b/tests/functional/appsmoke/Android.mk
new file mode 100644
index 0000000..17d41c9
--- /dev/null
+++ b/tests/functional/appsmoke/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_SRC_FILES := $(call all-java-files-under, src)
+LOCAL_MODULE_TAGS := tests
+LOCAL_PACKAGE_NAME := AppSmoke
+
+LOCAL_STATIC_JAVA_LIBRARIES := ub-uiautomator launcher-helper-lib android-support-test
+LOCAL_CERTIFICATE := platform
+
+include $(BUILD_PACKAGE)
diff --git a/tests/functional/appsmoke/AndroidManifest.xml b/tests/functional/appsmoke/AndroidManifest.xml
new file mode 100644
index 0000000..ebd8cb5
--- /dev/null
+++ b/tests/functional/appsmoke/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.
+-->
+
+<!-- package name must be unique so suffix with "tests" so package loader doesn't ignore us -->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="android.test.appsmoke"
+ android:sharedUserId="android.uid.system" >
+
+ <uses-sdk android:minSdkVersion="21" android:targetSdkVersion="21" />
+
+ <application>
+ <uses-library android:name="android.test.runner" />
+ </application>
+
+ <instrumentation android:name="android.support.test.runner.AndroidJUnitRunner"
+ android:targetPackage="android.test.appsmoke"
+ android:label="Prebuilt App Smoke Test"/>
+
+</manifest>
diff --git a/tests/functional/appsmoke/src/android/test/appsmoke/AppSmokeTest.java b/tests/functional/appsmoke/src/android/test/appsmoke/AppSmokeTest.java
new file mode 100644
index 0000000..d061304
--- /dev/null
+++ b/tests/functional/appsmoke/src/android/test/appsmoke/AppSmokeTest.java
@@ -0,0 +1,259 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.test.appsmoke;
+
+import android.app.ActivityManagerNative;
+import android.app.IActivityController;
+import android.app.Instrumentation;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.LauncherActivityInfo;
+import android.content.pm.LauncherApps;
+import android.os.Bundle;
+import android.os.RemoteException;
+import android.os.SystemClock;
+import android.os.UserHandle;
+import android.os.UserManager;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.launcherhelper.ILauncherStrategy;
+import android.support.test.launcherhelper.LauncherStrategyFactory;
+import android.support.test.uiautomator.UiDevice;
+import android.util.Log;
+
+import org.junit.After;
+import org.junit.AfterClass;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameter;
+import org.junit.runners.Parameterized.Parameters;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+@RunWith(Parameterized.class)
+public class AppSmokeTest {
+
+ private static final String TAG = AppSmokeTest.class.getSimpleName();
+ private static final String EXCLUDE_LIST = "exclude_apps";
+ private static final String DEBUG_LIST = "debug_apps";
+ private static final long WAIT_FOR_ANR = 6000;
+
+ @Parameter
+ public LaunchParameter mAppInfo;
+
+ private boolean mAppHasError = false;
+ private boolean mLaunchIntentDetected = false;
+ private ILauncherStrategy mLauncherStrategy = null;
+ private static UiDevice sDevice = null;
+
+ /**
+ * Convenient internal class to hold some launch specific data
+ */
+ private static class LaunchParameter implements Comparable<LaunchParameter>{
+ public String appName;
+ public String packageName;
+ public String activityName;
+
+ private LaunchParameter(String appName, String packageName, String activityName) {
+ this.appName = appName;
+ this.packageName = packageName;
+ this.activityName = activityName;
+ }
+
+ @Override
+ public int compareTo(LaunchParameter another) {
+ return appName.compareTo(another.appName);
+ }
+
+ @Override
+ public String toString() {
+ return appName;
+ }
+
+ public String toLongString() {
+ return String.format("%s [activity: %s/%s]", appName, packageName, activityName);
+ }
+ }
+
+ /**
+ * an activity controller to detect app launch crashes/ANR etc
+ */
+ private IActivityController mActivityController = new IActivityController.Stub() {
+
+ @Override
+ public int systemNotResponding(String msg) throws RemoteException {
+ // let system die
+ return -1;
+ }
+
+ @Override
+ public int appNotResponding(String processName, int pid, String processStats)
+ throws RemoteException {
+ if (processName.startsWith(mAppInfo.packageName)) {
+ mAppHasError = true;
+ }
+ // kill app
+ return -1;
+ }
+
+ @Override
+ public int appEarlyNotResponding(String processName, int pid, String annotation)
+ throws RemoteException {
+ // do nothing
+ return 0;
+ }
+
+ @Override
+ public boolean appCrashed(String processName, int pid, String shortMsg, String longMsg,
+ long timeMillis, String stackTrace) throws RemoteException {
+ if (processName.startsWith(mAppInfo.packageName)) {
+ mAppHasError = true;
+ }
+ return false;
+ }
+
+ @Override
+ public boolean activityStarting(Intent intent, String pkg) throws RemoteException {
+ Log.d(TAG, String.format("activityStarting: pkg=%s intent=%s",
+ pkg, intent.toInsecureString()));
+ // always allow starting
+ if (pkg.equals(mAppInfo.packageName)) {
+ mLaunchIntentDetected = true;
+ }
+ return true;
+ }
+
+ @Override
+ public boolean activityResuming(String pkg) throws RemoteException {
+ Log.d(TAG, String.format("activityResuming: pkg=%s", pkg));
+ // always allow resuming
+ return true;
+ }
+ };
+
+ /**
+ * Generate the list of apps to test for launches by querying package manager
+ * @return
+ */
+ @Parameters(name = "{0}")
+ public static Collection<LaunchParameter> generateAppsList() {
+ Instrumentation instr = InstrumentationRegistry.getInstrumentation();
+ Bundle args = InstrumentationRegistry.getArguments();
+ Context ctx = instr.getTargetContext();
+ List<LaunchParameter> ret = new ArrayList<>();
+ Set<String> excludedApps = new HashSet<>();
+ Set<String> debugApps = new HashSet<>();
+
+ // parse list of app names that should be execluded from launch tests
+ if (args.containsKey(EXCLUDE_LIST)) {
+ excludedApps.addAll(Arrays.asList(args.getString(EXCLUDE_LIST).split(",")));
+ }
+ // parse list of app names used for debugging (i.e. essentially a whitelist)
+ if (args.containsKey(DEBUG_LIST)) {
+ debugApps.addAll(Arrays.asList(args.getString(DEBUG_LIST).split(",")));
+ }
+ LauncherApps la = (LauncherApps)ctx.getSystemService(Context.LAUNCHER_APPS_SERVICE);
+ UserManager um = (UserManager)ctx.getSystemService(Context.USER_SERVICE);
+ List<LauncherActivityInfo> activities = new ArrayList<>();
+ for (UserHandle handle : um.getUserProfiles()) {
+ activities.addAll(la.getActivityList(null, handle));
+ }
+ for (LauncherActivityInfo info : activities) {
+ String label = info.getLabel().toString();
+ if (!debugApps.isEmpty()) {
+ if (!debugApps.contains(label)) {
+ // if debug apps non-empty, we are essentially in whitelist mode
+ // bypass any apps not on list
+ continue;
+ }
+ } else if (excludedApps.contains(label)) {
+ // if not debugging apps, bypass any excluded apps
+ continue;
+ }
+ ret.add(new LaunchParameter(label, info
+ .getApplicationInfo().packageName, info.getName()));
+ }
+ Collections.sort(ret);
+ return ret;
+ }
+
+ @Before
+ public void before() throws RemoteException {
+ ActivityManagerNative.getDefault().setActivityController(mActivityController, false);
+ mLauncherStrategy = LauncherStrategyFactory.getInstance(sDevice).getLauncherStrategy();
+ mAppHasError = false;
+ mLaunchIntentDetected = false;
+ }
+
+ @BeforeClass
+ public static void beforeClass() throws RemoteException {
+ sDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());
+ sDevice.setOrientationNatural();
+ }
+
+ @After
+ public void after() throws RemoteException {
+ sDevice.pressHome();
+ ActivityManagerNative.getDefault().forceStopPackage(
+ mAppInfo.packageName, UserHandle.USER_ALL);
+ ActivityManagerNative.getDefault().setActivityController(null, false);
+ }
+
+ @AfterClass
+ public static void afterClass() throws RemoteException {
+ sDevice.unfreezeRotation();
+ }
+
+ @Test
+ public void testAppLaunch() {
+ Log.d(TAG, "Launching: " + mAppInfo.toLongString());
+ long timestamp = mLauncherStrategy.launch(mAppInfo.appName, mAppInfo.packageName);
+ boolean launchResult = (timestamp != ILauncherStrategy.LAUNCH_FAILED_TIMESTAMP);
+ if (launchResult) {
+ // poke app to check if it's responsive
+ pokeApp();
+ SystemClock.sleep(WAIT_FOR_ANR);
+ }
+ if (mAppHasError) {
+ Assert.fail("app crash or ANR detected");
+ }
+ if (!launchResult && !mLaunchIntentDetected) {
+ Assert.fail("no app crash or ANR detected, but failed to launch via UI");
+ }
+ // if launchResult is false but mLaunchIntentDetected is true, we consider it as success
+ // this happens when an app is a trampoline activity to something else
+ }
+
+ private void pokeApp() {
+ int w = sDevice.getDisplayWidth();
+ int h = sDevice.getDisplayHeight();
+ int dY = h / 4;
+ boolean ret = sDevice.swipe(w / 2, h / 2 + dY, w / 2, h / 2 - dY, 40);
+ if (!ret) {
+ Log.w(TAG, "Failed while attempting to poke front end window with swipe");
+ }
+ }
+}
diff --git a/tests/functional/downloadapp/Android.mk b/tests/functional/downloadapp/Android.mk
new file mode 100644
index 0000000..814537b
--- /dev/null
+++ b/tests/functional/downloadapp/Android.mk
@@ -0,0 +1,15 @@
+LOCAL_PATH := $(call my-dir)
+include $(CLEAR_VARS)
+
+LOCAL_MODULE_TAGS := tests
+
+LOCAL_SDK_VERSION := current
+
+LOCAL_SRC_FILES := $(call all-subdir-java-files)
+LOCAL_JAVA_LIBRARIES := android.test.runner
+LOCAL_STATIC_JAVA_LIBRARIES := launcher-helper-lib ub-uiautomator android-support-test
+
+LOCAL_PACKAGE_NAME := DownloadAppFunctionalTests
+LOCAL_CERTIFICATE := platform
+
+include $(BUILD_PACKAGE)
diff --git a/tests/functional/downloadapp/AndroidManifest.xml b/tests/functional/downloadapp/AndroidManifest.xml
new file mode 100644
index 0000000..3e46794
--- /dev/null
+++ b/tests/functional/downloadapp/AndroidManifest.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Copyright 2016 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.android.functional.downloadapp">
+
+ <uses-sdk android:minSdkVersion="19"
+ android:targetSdkVersion="24" />
+ <uses-permission android:name="android.permission.INTERNET" />
+ <uses-permission android:name="android.permission.DOWNLOAD_WITHOUT_NOTIFICATION" />
+ <uses-permission android:name="android.permission.SET_TIME" />
+ <application>
+ <uses-library android:name="android.test.runner"/>
+ </application>
+ <instrumentation
+ android:name="android.support.test.runner.AndroidJUnitRunner"
+ android:targetPackage="com.android.functional.downloadapp"
+ android:label="DownloadApp Functional Tests" />
+</manifest>
diff --git a/tests/functional/downloadapp/src/com/android/functional/downloadapp/DownloadAppTestHelper.java b/tests/functional/downloadapp/src/com/android/functional/downloadapp/DownloadAppTestHelper.java
new file mode 100644
index 0000000..5427bc3
--- /dev/null
+++ b/tests/functional/downloadapp/src/com/android/functional/downloadapp/DownloadAppTestHelper.java
@@ -0,0 +1,328 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.functional.downloadapp;
+
+import android.app.AlarmManager;
+import android.app.DownloadManager;
+import android.app.DownloadManager.Query;
+import android.app.DownloadManager.Request;
+import android.content.ContentValues;
+import android.content.Context;
+import android.database.Cursor;
+import android.icu.util.Calendar;
+import android.net.Uri;
+import android.os.Environment;
+import android.support.test.launcherhelper.ILauncherStrategy;
+import android.support.test.launcherhelper.LauncherStrategyFactory;
+import android.support.test.uiautomator.By;
+import android.support.test.uiautomator.UiDevice;
+import android.support.test.uiautomator.UiObject2;
+import android.support.test.uiautomator.Until;
+import android.util.Log;
+
+import junit.framework.Assert;
+
+import java.util.ArrayList;
+import java.util.Hashtable;
+import java.util.List;
+import java.util.Random;
+
+public class DownloadAppTestHelper {
+ private static DownloadAppTestHelper mInstance = null;
+ public static final String[] FILE_TYPES = new String[] {
+ "pdf", "jpg", "jpeg", "doc", "xls", "txt", "rtf", "ppt", "gif", "png"
+ };
+
+ public static final String PACKAGE_NAME = "com.android.documentsui";
+ public static final String APP_NAME = "Downloads";
+ public static final String TEST_TAG = "DownloadAppTest";
+ public final int TIMEOUT = 500;
+ public final int MIN_FILENAME_LEN = 4;
+ private Context mContext = null;
+ private UiDevice mDevice = null;
+ private DownloadManager mDownloadMgr;
+ private Hashtable<String, DlObjSizeTimePair> mDownloadedItems =
+ new Hashtable<String, DlObjSizeTimePair>();
+ public ILauncherStrategy mLauncherStrategy;
+
+ private DownloadAppTestHelper(UiDevice device, Context context) {
+ mDevice = device;
+ mContext = context;
+ mLauncherStrategy = LauncherStrategyFactory.getInstance(mDevice).getLauncherStrategy();
+ }
+
+ public static DownloadAppTestHelper getInstance(UiDevice device, Context context) {
+ if (mInstance == null) {
+ mInstance = new DownloadAppTestHelper(device, context);
+ }
+ return mInstance;
+ }
+
+ public DownloadManager getDLManager() {
+ return (DownloadManager) mContext.getSystemService(Context.DOWNLOAD_SERVICE);
+ }
+
+ public void launchApp(String packageName, String appName) {
+ if (!mDevice.hasObject(By.pkg(packageName).depth(0))) {
+ mLauncherStrategy.launch(appName, packageName);
+ }
+ }
+
+ /** sort items in Download app by name, size, time */
+ public void sortByParam(String sortby) {
+ UiObject2 sortMenu = mDevice
+ .wait(Until.findObject(By.res(PACKAGE_NAME, "menu_sort")), 200);
+ if (sortMenu == null) {
+ mDevice.wait(Until.findObject(By.desc("More options")), 200).click();
+ sortMenu = mDevice.wait(Until.findObject(By.res("android:id/submenuarrow")), 200);
+ }
+ sortMenu.click();
+ mDevice.wait(Until.findObject(By.text(String.format("By %s", sortby))), 200).click();
+ mDevice.waitForIdle();
+ }
+
+ /** returns text list of items in Download app */
+ public List<String> getDownloadItemNames() {
+ List<UiObject2> itmesList = mDevice.wait(Until.findObjects(By.res("android:id/title")),
+ TIMEOUT);
+ List<String> nameList = new ArrayList<String>();
+ for (UiObject2 item : itmesList) {
+ nameList.add(item.getText());
+ }
+ return nameList;
+ }
+
+ /** verifies items in DownloadApp UI are sorted by name */
+ public Boolean verifySortedByName() {
+ List<String> nameList = getDownloadItemNames();
+ for (int i = 0; i < (nameList.size() - 1); ++i) {
+ if (nameList.get(i).compareToIgnoreCase(nameList.get(i + 1)) > 0) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /** verifies items in DownloadApp UI are sorted by size */
+ public Boolean verifySortedBySize() {
+ List<String> nameList = getDownloadItemNames();
+ for (int i = 0; i < (nameList.size() - 1); ++i) {
+ DlObjSizeTimePair firstItem = mDownloadedItems.get(nameList.get(i));
+ DlObjSizeTimePair secondItem = mDownloadedItems.get(nameList.get(i + 1));
+ if (firstItem != null && secondItem != null
+ && firstItem.dlObjSize < secondItem.dlObjSize) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /** verifies items in DownloadApp UI are sorted by time */
+ public Boolean verifySortedByTime() {
+ List<String> nameList = getDownloadItemNames();
+ for (int i = 0; i < (nameList.size() - 1); ++i) {
+ DlObjSizeTimePair firstItem = mDownloadedItems.get(nameList.get(i));
+ DlObjSizeTimePair secondItem = mDownloadedItems.get(nameList.get(i + 1));
+ if (firstItem != null && secondItem != null
+ && firstItem.dlObjTimeInMilliSec < secondItem.dlObjTimeInMilliSec) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ public void verifyDownloadViewType(UIViewType view) {
+ int counter = 5;
+ UiObject2 viewTypeObj = null;
+ while ((viewTypeObj = mDevice.wait(
+ Until.findObject(By.res(String.format("%s:id/%s",
+ DownloadAppTestHelper.PACKAGE_NAME,view.toString()))),200)) == null
+ && counter-- > 0);
+ Assert.assertNotNull(viewTypeObj);
+ }
+
+ /**
+ * Create word of random assortment of lower/upper case letters
+ */
+ /** set system time to random n[0..29] days earlier */
+ public long changeSystemTime(long timeToSet) {
+ Calendar c = Calendar.getInstance();
+ c.setTimeInMillis(timeToSet);
+ AlarmManager am = (AlarmManager) mContext.getSystemService(Context.ALARM_SERVICE);
+ am.setTime(c.getTimeInMillis());
+ return c.getTimeInMillis();
+ }
+
+ /** add some content to download DB using DownloadManager.addCompletedDownload api */
+ public void populateContentInDLApp(int count) {
+ int totalDownloaded = getTotalNumberDownloads();
+ if (totalDownloaded >= count) {
+ return;
+ }
+ Random random = new Random();
+ long currentTime = System.currentTimeMillis();
+ for (int i = 0; i < (count - totalDownloaded); ++i) {
+ String fileName = String.format("%s.%s",
+ DownloadAppTestHelper.randomWord(random.nextInt(8) + MIN_FILENAME_LEN),
+ DownloadAppTestHelper.FILE_TYPES[random.nextInt(FILE_TYPES.length)]);
+ int size = random.nextInt(1000);
+ // changing system time to simulate the usecase "downloaded items over a period of time"
+ long timeInMiliSec = changeSystemTime(
+ System.currentTimeMillis() - random.nextInt(30 * 24) * (60 * 60 * 1000));
+ long dlId = -1;
+
+ dlId = getDLManager().addCompletedDownload(
+ fileName,
+ String.format("%s Desc",
+ DownloadAppTestHelper.randomWord(random.nextInt(8) + MIN_FILENAME_LEN)),
+ Boolean.FALSE,
+ DownloadAppTestHelper.FILE_TYPES[random.nextInt(FILE_TYPES.length)],
+ Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)
+ .getAbsolutePath(),
+ size, Boolean.FALSE);
+ Assert.assertFalse("Add to DonwloadDB has failed!", dlId == -1);
+ Log.d(TEST_TAG, String.format("Adding Name = %s, size = %d, time = %d", fileName,
+ size, timeInMiliSec));
+ mDownloadedItems.put(fileName, new DlObjSizeTimePair(size, timeInMiliSec));
+ }
+ changeSystemTime(currentTime);
+ }
+
+ /** add some content to download DB directly in hacky way to bypass addCompletedDownload Api*/
+ public long addToDownloadContentDB(String title, String description,
+ boolean isMediaScannerScannable, String mimeType, String path, long length,
+ boolean showNotification) {
+
+ boolean allowWrite = Boolean.FALSE;
+ Uri uri = Uri.parse("http://blah-blah"); // just put something in url format
+ Uri referer = null;
+ Request request;
+ request = new Request(uri);
+ ContentValues values = new ContentValues();
+ /**
+ * a hacky way to insert into the Download DB direct bypassing the api with minimal data
+ * Constants have been taken from
+ * /platform/frameworks/base/+/master/core/java/android/provider/Downloads.java
+ */
+ values.put("title", title);
+ values.put("description", description);
+ values.put("mimetype", mimeType);
+ values.put("is_public_api", true);
+ values.put("scanned", isMediaScannerScannable);
+ values.put("is_visible_in_downloads_ui", Boolean.TRUE);
+ values.put("destination", 6); // 6: show the download item in app
+ values.put("_data", path); // location to save the downloaded file
+ values.put("status", 200); // 200 : STATUS_SUCCESS
+ values.put("total_bytes", length);
+ values.put("visibility", (showNotification)
+ ? Request.VISIBILITY_VISIBLE_NOTIFY_ONLY_COMPLETION : Request.VISIBILITY_HIDDEN);
+ Uri downloadUri = mContext.getContentResolver()
+ .insert(Uri.parse("content://downloads/my_downloads"), values);
+ if (downloadUri == null) {
+ return -1;
+ }
+ return Long.parseLong(downloadUri.getLastPathSegment());
+ }
+
+ /** remove downloads from download content db */
+ public void removeContentInDLApp() {
+ Cursor cursor = null;
+ try {
+ Query query = new Query();
+ cursor = getDLManager().query(query);
+ int columnIndex = cursor.getColumnIndex(DownloadManager.COLUMN_ID);
+ long[] removeIds = new long[cursor.getCount() - 1];
+ Log.d(TEST_TAG, String.format("Remove Size is = %d", cursor.getCount()));
+ for (int i = 0; i < (cursor.getCount() - 1); i++) {
+ cursor.moveToNext();
+ removeIds[i] = cursor.getLong(columnIndex);
+ }
+ if (removeIds.length > 0) {
+ Assert.assertEquals(removeIds.length, getDLManager().remove(removeIds));
+ }
+ } finally {
+ if (cursor != null) {
+ cursor.close();
+ }
+ }
+ }
+
+ public int getTotalNumberDownloads() {
+ Cursor cursor = null;
+ try {
+ Query query = new Query();
+ cursor = getDLManager().query(query);
+ return cursor.getCount();
+ } finally {
+ if (cursor != null) {
+ cursor.close();
+ }
+ }
+ }
+
+ public int getDownloadItemCountById(long[] downloadId) {
+ Cursor cursor = null;
+ int total = 0;
+ try {
+ Query query = new Query().setFilterById(downloadId);
+ cursor = getDLManager().query(query);
+ total = cursor.getCount();
+ } finally {
+ if (cursor != null) {
+ cursor.close();
+ }
+ }
+
+ return total;
+ }
+
+ public static String randomWord(int length) {
+ Random random = new Random();
+ StringBuilder result = new StringBuilder();
+ for (int j = 0; j < length; j++) {
+ int base = random.nextInt(2) == 0 ? 'A' : 'a';
+ result.append((char) (random.nextInt(26) + base));
+ }
+ return result.toString();
+ }
+
+ /**
+ * Class to hold size and time info on downloaded items
+ */
+ class DlObjSizeTimePair {
+ int dlObjSize;
+ long dlObjTimeInMilliSec;
+
+ public DlObjSizeTimePair(int size, long time) {
+ this.dlObjSize = size;
+ this.dlObjTimeInMilliSec = time;
+ }
+ }
+
+ public enum UIViewType {
+ LIST {
+ public String toString() {
+ return "menu_list";
+ }
+ },
+ GRID {
+ public String toString() {
+ return "menu_grid";
+ }
+ }
+ };
+}
diff --git a/tests/functional/downloadapp/src/com/android/functional/downloadapp/DownloadAppTests.java b/tests/functional/downloadapp/src/com/android/functional/downloadapp/DownloadAppTests.java
new file mode 100644
index 0000000..40c3fa5
--- /dev/null
+++ b/tests/functional/downloadapp/src/com/android/functional/downloadapp/DownloadAppTests.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.functional.downloadapp;
+
+import android.content.Context;
+import android.os.Environment;
+import android.os.SystemClock;
+import android.support.test.uiautomator.By;
+import android.support.test.uiautomator.Direction;
+import android.support.test.uiautomator.UiDevice;
+import android.support.test.uiautomator.UiObject2;
+import android.support.test.uiautomator.Until;
+import android.test.InstrumentationTestCase;
+import android.test.suitebuilder.annotation.MediumTest;
+import android.test.suitebuilder.annotation.SmallTest;
+import android.test.suitebuilder.annotation.Suppress;
+import android.util.Log;
+
+import com.android.functional.downloadapp.DownloadAppTestHelper.UIViewType;
+
+import junit.framework.Assert;
+
+import java.util.Random;
+
+public class DownloadAppTests extends InstrumentationTestCase {
+ private DownloadAppTestHelper mDLAppHelper = null;
+ private UiDevice mDevice = null;
+ private Context mContext = null;
+
+ @Override
+ protected void setUp() throws Exception {
+ super.setUp();
+ mDevice = UiDevice.getInstance(getInstrumentation());
+ mContext = getInstrumentation().getContext();
+ mDLAppHelper = DownloadAppTestHelper.getInstance(mDevice, mContext);
+ }
+
+ @Override
+ protected void tearDown() throws Exception {
+ super.tearDown();
+ }
+
+ @SmallTest
+ public void testAddCompletedDownload() throws Exception {
+ Random random = new Random();
+ Long dlId = mDLAppHelper.addToDownloadContentDB(
+ String.format("%s.pdf", DownloadAppTestHelper.randomWord(random.nextInt(8) + 2)),
+ String.format("%s Desc", DownloadAppTestHelper.randomWord(random.nextInt(9) + 4)),
+ Boolean.FALSE,
+ DownloadAppTestHelper.FILE_TYPES[random.nextInt(
+ DownloadAppTestHelper.FILE_TYPES.length)],
+ Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)
+ .getAbsolutePath(),
+ random.nextInt(2 * mDLAppHelper.TIMEOUT), Boolean.FALSE);
+ assertTrue("Download item <> 1",
+ 1 == mDLAppHelper.getDownloadItemCountById(new long[] {
+ dlId
+ }));
+ }
+
+ @MediumTest
+ public void testScroll() {
+ mDLAppHelper.populateContentInDLApp(20);
+ mDLAppHelper.launchApp(DownloadAppTestHelper.PACKAGE_NAME, DownloadAppTestHelper.APP_NAME);
+ UiObject2 container = mDevice.wait(Until.findObject(
+ By.res("com.android.documentsui:id/container_directory")), mDLAppHelper.TIMEOUT);
+ container.scroll(Direction.UP, 1.0f);
+ mDevice.waitForIdle();
+ container.scroll(Direction.DOWN, 1.0f);
+ }
+
+ @MediumTest
+ public void testSortByName() throws Exception {
+ Log.d(mDLAppHelper.TEST_TAG, String.format("Before sortbyname tests, total count is %d",
+ mDLAppHelper.getTotalNumberDownloads()));
+ mDLAppHelper.populateContentInDLApp(5);
+ mDLAppHelper.launchApp(DownloadAppTestHelper.PACKAGE_NAME, DownloadAppTestHelper.APP_NAME);
+ mDLAppHelper.sortByParam("name");
+ assertTrue("DL items can't be sorted by name", mDLAppHelper.verifySortedByName());
+ }
+
+ @Suppress
+ @MediumTest
+ public void testSortBySize() {
+ mDLAppHelper.populateContentInDLApp(5);
+ mDLAppHelper.launchApp(DownloadAppTestHelper.PACKAGE_NAME, DownloadAppTestHelper.APP_NAME);
+ mDLAppHelper.sortByParam("size");
+ assertTrue("DL items can't be sorted by size", mDLAppHelper.verifySortedBySize());
+ }
+
+ @MediumTest
+ public void testSortByTime() {
+ mDLAppHelper.populateContentInDLApp(5);
+ mDLAppHelper.launchApp(DownloadAppTestHelper.PACKAGE_NAME, DownloadAppTestHelper.APP_NAME);
+ mDLAppHelper.sortByParam("date modified");
+ assertTrue("DL items can't be sorted by time", mDLAppHelper.verifySortedByTime());
+ }
+
+ @MediumTest
+ public void testToggleViewTypeForDownloadItems() {
+ mDLAppHelper.populateContentInDLApp(10);
+ mDLAppHelper.launchApp(DownloadAppTestHelper.PACKAGE_NAME, DownloadAppTestHelper.APP_NAME);
+ mDevice.wait(Until.findObject(By.res(
+ String.format("%s:id/%s", DownloadAppTestHelper.PACKAGE_NAME, UIViewType.LIST))),
+ 2 * mDLAppHelper.TIMEOUT).click();
+ mDLAppHelper.verifyDownloadViewType(UIViewType.GRID);
+ mDevice.wait(Until.findObject(By.res(
+ String.format("%s:id/%s", DownloadAppTestHelper.PACKAGE_NAME, UIViewType.GRID))),
+ 2 * mDLAppHelper.TIMEOUT).click();
+ mDLAppHelper.verifyDownloadViewType(UIViewType.LIST);
+ }
+
+ @MediumTest
+ public void testCABMenuShow() {
+ mDLAppHelper.populateContentInDLApp(10);
+ mDLAppHelper.launchApp(DownloadAppTestHelper.PACKAGE_NAME, DownloadAppTestHelper.APP_NAME);
+ mDevice.wait(Until.findObject(By.res("com.android.documentsui:id/dir_list")),
+ mDLAppHelper.TIMEOUT).getChildren().get(1).click(1 * 2 * mDLAppHelper.TIMEOUT);
+ UiObject2 cabMenuObj = null;
+ int counter = 5;
+ while ((cabMenuObj = mDevice.wait(Until.findObject(By.res(String.format("%s:id/menu_share",
+ DownloadAppTestHelper.PACKAGE_NAME))), mDLAppHelper.TIMEOUT)) == null
+ && counter-- > 0);
+ Assert.assertNotNull(cabMenuObj);
+ counter = 5;
+ while ((cabMenuObj = mDevice.wait(Until.findObject(By.res(String.format("%s:id/menu_delete",
+ DownloadAppTestHelper.PACKAGE_NAME))), mDLAppHelper.TIMEOUT)) == null
+ && counter-- > 0);
+ Assert.assertNotNull(cabMenuObj);
+ counter = 5;
+ while ((cabMenuObj = mDevice.wait(Until.findObject(
+ By.desc("More options")), mDLAppHelper.TIMEOUT)) == null && counter-- > 0)
+ ;
+ Assert.assertNotNull(cabMenuObj);
+ while ((cabMenuObj = mDevice.wait(Until.findObject(
+ By.desc("Done")), mDLAppHelper.TIMEOUT)) == null && counter-- > 0)
+ ;
+ Assert.assertNotNull(cabMenuObj);
+ cabMenuObj.click();
+ SystemClock.sleep(2 * mDLAppHelper.TIMEOUT);
+ }
+
+ @MediumTest
+ public void testCABMenuDelete() {
+ mDLAppHelper.populateContentInDLApp(10);
+ mDLAppHelper.launchApp(DownloadAppTestHelper.PACKAGE_NAME, DownloadAppTestHelper.APP_NAME);
+ UiObject2 deleteObj = mDevice.wait(Until.findObject(
+ By.res("com.android.documentsui:id/dir_list")), mDLAppHelper.TIMEOUT)
+ .getChildren().get(1);
+ String deleteObjText = deleteObj.getText();
+ deleteObj.click(1 * 2 * mDLAppHelper.TIMEOUT);
+ int counter = 5;
+ UiObject2 cabMenuObj = null;
+ while ((cabMenuObj = mDevice.wait(Until.findObject(By.res(String.format("%s:id/menu_delete",
+ DownloadAppTestHelper.PACKAGE_NAME))), 2 * mDLAppHelper.TIMEOUT)) == null
+ && counter-- > 0);
+ cabMenuObj.click();
+ UiObject2 deleteBtn = mDevice.wait(Until.findObject(
+ By.textContains("Delete")), mDLAppHelper.TIMEOUT);
+ if(deleteBtn != null) {
+ mDevice.wait(Until.findObject(By.text("OK")), 2 * mDLAppHelper.TIMEOUT).click();
+ }
+ Assert.assertFalse("", mDLAppHelper.getDownloadItemNames().contains(deleteObjText));
+ }
+}
diff --git a/tests/functional/externalstorage/Android.mk b/tests/functional/externalstorage/Android.mk
new file mode 100644
index 0000000..ade11f8
--- /dev/null
+++ b/tests/functional/externalstorage/Android.mk
@@ -0,0 +1,15 @@
+LOCAL_PATH := $(call my-dir)
+include $(CLEAR_VARS)
+
+LOCAL_MODULE_TAGS := tests
+
+LOCAL_SDK_VERSION := current
+
+LOCAL_SRC_FILES := $(call all-subdir-java-files)
+LOCAL_JAVA_LIBRARIES := android.test.runner
+LOCAL_STATIC_JAVA_LIBRARIES := launcher-helper-lib ub-uiautomator app-helpers
+
+LOCAL_PACKAGE_NAME := ExternalStorageFunctionalTests
+LOCAL_CERTIFICATE := platform
+
+include $(BUILD_PACKAGE)
\ No newline at end of file
diff --git a/tests/functional/externalstorage/AndroidManifest.xml b/tests/functional/externalstorage/AndroidManifest.xml
new file mode 100644
index 0000000..8230fc3
--- /dev/null
+++ b/tests/functional/externalstorage/AndroidManifest.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Copyright 2016 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.android.functional.externalstoragetests">
+
+ <uses-sdk android:minSdkVersion="19"
+ 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.test.InstrumentationTestRunner"
+ android:targetPackage="com.android.functional.externalstoragetests"
+ android:label="External Storage Functional Tests" />
+</manifest>
\ No newline at end of file
diff --git a/tests/functional/externalstorage/src/com/android/functional/externalstoragetests/AdoptableStorageTests.java b/tests/functional/externalstorage/src/com/android/functional/externalstoragetests/AdoptableStorageTests.java
new file mode 100644
index 0000000..84d18e2
--- /dev/null
+++ b/tests/functional/externalstorage/src/com/android/functional/externalstoragetests/AdoptableStorageTests.java
@@ -0,0 +1,215 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.functional.externalstoragetests;
+
+import android.app.UiAutomation;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.provider.Settings;
+import android.support.test.uiautomator.By;
+import android.support.test.uiautomator.UiDevice;
+import android.support.test.uiautomator.UiObject2;
+import android.support.test.uiautomator.Until;
+import android.test.InstrumentationTestCase;
+import android.test.suitebuilder.annotation.LargeTest;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public class AdoptableStorageTests extends InstrumentationTestCase {
+ private UiDevice mDevice = null;
+ private Context mContext = null;
+ private UiAutomation mUiAutomation = null;
+ private ExternalStorageHelper storageHelper;
+
+ @Override
+ protected void setUp() throws Exception {
+ super.setUp();
+ mDevice = UiDevice.getInstance(getInstrumentation());
+ mContext = getInstrumentation().getContext();
+ mUiAutomation = getInstrumentation().getUiAutomation();
+ storageHelper = ExternalStorageHelper.getInstance(mDevice, mContext, mUiAutomation,
+ getInstrumentation());
+ mDevice.setOrientationNatural();
+ }
+
+ /**
+ * Tests external storage adoption and move data later flow via UI
+ */
+ @LargeTest
+ public void testAdoptAsAdoptableMoveDataLaterUIFlow() throws InterruptedException {
+ // ensure there is a storage to be adopted
+ storageHelper.partitionDisk("public");
+ initiateAdoption();
+ Pattern pattern = Pattern.compile("Move later", Pattern.CASE_INSENSITIVE);
+ mDevice.wait(Until.findObject(By.text(pattern)), storageHelper.TIMEOUT).click();
+ pattern = Pattern.compile("Next", Pattern.CASE_INSENSITIVE);
+ mDevice.wait(Until.findObject(By.text(pattern)), storageHelper.TIMEOUT).clickAndWait(
+ Until.newWindow(), storageHelper.TIMEOUT);
+ pattern = Pattern.compile("Done", Pattern.CASE_INSENSITIVE);
+ mDevice.wait(Until.findObject(By.text(pattern)), storageHelper.TIMEOUT).clickAndWait(
+ Until.newWindow(), storageHelper.TIMEOUT);
+ assertNotNull(storageHelper.getAdoptionVolumeId("private"));
+ // ensure data dirs have not moved
+ Intent intent = new Intent(Settings.ACTION_INTERNAL_STORAGE_SETTINGS);
+ intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ mContext.startActivity(intent);
+ mDevice.wait(Until.findObject(By.textContains("SD card")), 2 * storageHelper.TIMEOUT)
+ .clickAndWait(Until.newWindow(), storageHelper.TIMEOUT);
+ assertTrue(mDevice.wait(Until.hasObject(By.res("android:id/title").text("Apps")),
+ storageHelper.TIMEOUT));
+ }
+
+ // Adoptable storage settings
+ /**
+ * tests to ensure that adoptable storage has setting options rename, eject, format as portable
+ */
+ @LargeTest
+ public void testAdoptableOverflowSettings() throws InterruptedException {
+ storageHelper.partitionDisk("private");
+ storageHelper.openSDCard();
+ Pattern pattern = Pattern.compile("More options", Pattern.CASE_INSENSITIVE);
+ UiObject2 moreOptions = mDevice.wait(Until.findObject(By.desc(pattern)),
+ storageHelper.TIMEOUT);
+ assertNotNull("Over flow menu options shouldn't be null", moreOptions);
+ moreOptions.click();
+ pattern = Pattern.compile("Rename", Pattern.CASE_INSENSITIVE);
+ assertTrue(mDevice.wait(Until.hasObject(By.text(pattern)), storageHelper.TIMEOUT));
+ pattern = Pattern.compile("Eject", Pattern.CASE_INSENSITIVE);
+ assertTrue(mDevice.wait(Until.hasObject(By.text(pattern)), storageHelper.TIMEOUT));
+ pattern = Pattern.compile("Format as portable", Pattern.CASE_INSENSITIVE);
+ assertTrue(mDevice.wait(Until.hasObject(By.text(pattern)), storageHelper.TIMEOUT));
+ }
+
+ /**
+ * tests to ensure that adoptable storage can be renamed
+ */
+ @LargeTest
+ public void testRenameAdoptable() throws InterruptedException {
+ storageHelper.partitionDisk("private");
+ storageHelper.openSDCard();
+ Pattern pattern = Pattern.compile("More options", Pattern.CASE_INSENSITIVE);
+ mDevice.wait(Until.findObject(By.desc(pattern)), storageHelper.TIMEOUT).click();
+ pattern = Pattern.compile("Rename", Pattern.CASE_INSENSITIVE);
+ mDevice.wait(Until.findObject(By.text(pattern)), storageHelper.TIMEOUT).click();
+ mDevice.wait(Until.findObject(By.res(storageHelper.SETTINGS_PKG, "edittext")),
+ storageHelper.TIMEOUT).setText("My SD card");
+ pattern = Pattern.compile("Save", Pattern.CASE_INSENSITIVE);
+ mDevice.wait(Until.findObject(By.text(pattern)), storageHelper.TIMEOUT).clickAndWait(
+ Until.newWindow(), storageHelper.TIMEOUT);
+ assertTrue(mDevice.wait(Until.hasObject(By.text("My SD card")), storageHelper.TIMEOUT));
+ }
+
+ /**
+ * tests to ensure that adoptable storage can be ejected
+ */
+ @LargeTest
+ public void testEjectAdoptable() throws InterruptedException {
+ storageHelper.partitionDisk("private");
+ storageHelper.openSDCard();
+ Pattern pattern = Pattern.compile("More options", Pattern.CASE_INSENSITIVE);
+ mDevice.wait(Until.findObject(By.desc(pattern)), storageHelper.TIMEOUT).click();
+ pattern = Pattern.compile("Eject", Pattern.CASE_INSENSITIVE);
+ mDevice.wait(Until.findObject(By.text(pattern)), storageHelper.TIMEOUT).click();
+ assertTrue(mDevice.wait(Until.hasObject(By.res(storageHelper.SETTINGS_PKG, "body")),
+ storageHelper.TIMEOUT));
+ mDevice.wait(Until.findObject(By.res(storageHelper.SETTINGS_PKG, "confirm").text(pattern)),
+ storageHelper.TIMEOUT).clickAndWait(Until.newWindow(), storageHelper.TIMEOUT);
+ pattern = Pattern.compile("Ejected", Pattern.CASE_INSENSITIVE);
+ assertTrue(mDevice.wait(Until.hasObject(By.res("android:id/summary").text(pattern)),
+ storageHelper.TIMEOUT));
+ mDevice.wait(Until.findObject(By.textContains("SD card")), storageHelper.TIMEOUT).click();
+ pattern = Pattern.compile("Mount", Pattern.CASE_INSENSITIVE);
+ mDevice.wait(Until.findObject(By.res("android:id/button1").text(pattern)),
+ 2 * storageHelper.TIMEOUT).clickAndWait(Until.newWindow(), storageHelper.TIMEOUT);
+ }
+
+ /**
+ * tests to ensure that adoptable storage can be formated back as portable from settings
+ */
+ @LargeTest
+ public void testFormatAdoptableAsPortable() throws InterruptedException {
+ storageHelper.partitionDisk("private");
+ storageHelper.openSDCard();
+ Pattern pattern = Pattern.compile("More options", Pattern.CASE_INSENSITIVE);
+ mDevice.wait(Until.findObject(By.desc(pattern)), storageHelper.TIMEOUT).click();
+ pattern = Pattern.compile("Format as portable", Pattern.CASE_INSENSITIVE);
+ mDevice.wait(Until.findObject(By.text(pattern)), storageHelper.TIMEOUT)
+ .clickAndWait(Until.newWindow(), storageHelper.TIMEOUT);
+ mDevice.wait(Until.hasObject(
+ By.textContains("After formatting, you can use this")), storageHelper.TIMEOUT);
+ mDevice.wait(Until.findObject(By.text("FORMAT")), 2 * storageHelper.TIMEOUT)
+ .clickAndWait(Until.newWindow(), storageHelper.TIMEOUT);
+ pattern = Pattern.compile("Done", Pattern.CASE_INSENSITIVE);
+ mDevice.wait(Until.findObject(By.text(pattern)), 5 * storageHelper.TIMEOUT)
+ .clickAndWait(Until.newWindow(), storageHelper.TIMEOUT);
+ }
+
+ public void initiateAdoption() throws InterruptedException {
+ storageHelper.openSdCardSetUpNotification().clickAndWait(Until.newWindow(),
+ storageHelper.TIMEOUT);
+ UiObject2 adoptFlowUi = mDevice.wait(Until.findObject(
+ By.res(storageHelper.SETTINGS_PKG, "storage_wizard_init_internal_title")),
+ storageHelper.TIMEOUT);
+ adoptFlowUi.click();
+ Pattern pattern = Pattern.compile("NEXT", Pattern.CASE_INSENSITIVE);
+ adoptFlowUi = mDevice.wait(Until.findObject(
+ By.res(storageHelper.SETTINGS_PKG, "suw_navbar_next").text(pattern)),
+ storageHelper.TIMEOUT);
+ adoptFlowUi.clickAndWait(Until.newWindow(), storageHelper.TIMEOUT);
+ pattern = Pattern.compile("ERASE & FORMAT", Pattern.CASE_INSENSITIVE);
+ adoptFlowUi = mDevice.wait(Until.findObject(By.text(pattern)),
+ storageHelper.TIMEOUT);
+ adoptFlowUi.clickAndWait(Until.newWindow(), storageHelper.TIMEOUT);
+ adoptFlowUi = mDevice.wait(
+ Until.findObject(By.res(storageHelper.SETTINGS_PKG, "storage_wizard_progress")),
+ storageHelper.TIMEOUT);
+ assertNotNull(adoptFlowUi);
+ if ((mDevice.wait(Until.findObject(By.res("android:id/message")),
+ 60 * storageHelper.TIMEOUT)) != null) {
+ mDevice.wait(Until.findObject(By.text("OK")), storageHelper.TIMEOUT).clickAndWait(
+ Until.newWindow(), storageHelper.TIMEOUT);
+ }
+ }
+
+ /**
+ * System apps can't be moved to adopted storage
+ */
+ @LargeTest
+ public void testTransferSystemApp() throws InterruptedException, NameNotFoundException {
+ storageHelper.partitionDisk("private");
+ storageHelper.executeShellCommand("pm move-package " + storageHelper.SETTINGS_PKG + " "
+ + storageHelper.getAdoptionVolumeId("private"));
+ assertTrue(storageHelper.getInstalledLocation(storageHelper.SETTINGS_PKG)
+ .startsWith("/data/user_de/0"));
+ }
+
+ @Override
+ protected void tearDown() throws Exception {
+ // Convert sdcard to public
+ storageHelper.executeShellCommand(String.format("sm partition %s %s",
+ storageHelper.getAdoptionDisk(), "public"));
+ Thread.sleep(storageHelper.TIMEOUT);
+ storageHelper.executeShellCommand("sm forget all");
+ Thread.sleep(storageHelper.TIMEOUT);
+ // move back to homescreen
+ mDevice.unfreezeRotation();
+ mDevice.pressBack();
+ mDevice.pressHome();
+ super.tearDown();
+ }
+}
diff --git a/tests/functional/externalstorage/src/com/android/functional/externalstoragetests/ExternalStorageHelper.java b/tests/functional/externalstorage/src/com/android/functional/externalstoragetests/ExternalStorageHelper.java
new file mode 100644
index 0000000..9a67ea4
--- /dev/null
+++ b/tests/functional/externalstorage/src/com/android/functional/externalstoragetests/ExternalStorageHelper.java
@@ -0,0 +1,288 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.functional.externalstoragetests;
+
+import android.app.Instrumentation;
+import android.app.UiAutomation;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.os.ParcelFileDescriptor;
+import android.os.StatFs;
+import android.os.SystemClock;
+import android.provider.Settings;
+import android.support.test.uiautomator.By;
+import android.support.test.uiautomator.UiDevice;
+import android.support.test.uiautomator.UiObject2;
+import android.support.test.uiautomator.Until;
+import android.util.Log;
+
+import android.platform.test.helpers.PlayStoreHelperImpl;
+
+import junit.framework.Assert;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.util.HashMap;
+import java.util.Hashtable;
+import java.util.List;
+import java.util.Map;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public class ExternalStorageHelper {
+ public static final String TEST_TAG = "StorageFunctionalTest";
+ public final String SETTINGS_PKG = "com.android.settings";
+ public final String PLAYSTORE_PKG = "com.android.vending";
+ public final String DOCUMENTS_PKG = "com.android.documentsui";
+ public static final Map<String, String> APPLIST = new HashMap<String, String>();
+ static {
+ APPLIST.put("w35location1", "com.test.w35location1");
+ APPLIST.put("w35location2", "com.test.w35location2");
+ APPLIST.put("w35location3", "com.test.w35location3");
+ }
+ public final int TIMEOUT = 2000;
+ public static ExternalStorageHelper mInstance = null;
+ public UiDevice mDevice;
+ public Context mContext;
+ public static UiAutomation mUiAutomation;
+ public static Instrumentation mInstrumentation;
+ public static Hashtable<String, List<String>> mPermissionGroupInfo = null;
+
+ public ExternalStorageHelper(UiDevice device, Context context, UiAutomation uiAutomation,
+ Instrumentation instrumentation) {
+ mDevice = device;
+ mContext = context;
+ mUiAutomation = uiAutomation;
+ mInstrumentation = instrumentation;
+ }
+
+ public static ExternalStorageHelper getInstance(UiDevice device, Context context,
+ UiAutomation uiAutomation, Instrumentation instrumentation) {
+ if (mInstance == null) {
+ mInstance = new ExternalStorageHelper(device, context, uiAutomation, instrumentation);
+ }
+ return mInstance;
+ }
+
+ /**
+ * Opens SD card setup notification from homescreen
+ */
+ public UiObject2 openSdCardSetUpNotification() throws InterruptedException {
+ boolean success = mDevice.openNotification();
+ Thread.sleep(TIMEOUT);
+ UiObject2 sdCardDetected = mDevice
+ .wait(Until.findObject(By.textContains("SD card detected")), TIMEOUT);
+ Assert.assertNotNull(sdCardDetected);
+ return sdCardDetected;
+ }
+
+ /**
+ * Open Storage settings, then SD Card
+ */
+ public void openStorageSettings() throws InterruptedException {
+ Intent intent = new Intent(Settings.ACTION_INTERNAL_STORAGE_SETTINGS);
+ intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ mContext.startActivity(intent);
+ Thread.sleep(TIMEOUT * 2);
+ }
+
+ /**
+ * Open Storage settings, then SD Card
+ */
+ public void openSDCard() throws InterruptedException {
+ openStorageSettings();
+ mDevice.wait(Until.findObject(By.textContains("SD card")), TIMEOUT)
+ .clickAndWait(Until.newWindow(), TIMEOUT);
+ }
+
+ public String executeShellCommand(String command) {
+ ParcelFileDescriptor pfd = mUiAutomation.executeShellCommand(command);
+ try (BufferedReader reader = new BufferedReader(
+ new InputStreamReader(new FileInputStream(pfd.getFileDescriptor())))) {
+ String str = reader.readLine();
+ Log.d(TEST_TAG, String.format("Executing command: %s", command));
+ return str;
+ } catch (IOException e) {
+ Log.e(TEST_TAG, e.getMessage());
+ }
+
+ return null;
+ }
+
+ /**
+ * Create # of files in a given dir
+ */
+ public void createFiles(int numberOfFiles, String dir) {
+ for (int i = 0; i < numberOfFiles; ++i) {
+ if (!new File(String.format("%s/Test_%d", dir, i)).exists()) {
+ fillInStorage(dir, String.format("Test_%d", i), 1);
+ }
+ }
+ }
+
+ public void fillInStorage(String location, String filename, int sizeInKb) {
+ executeShellCommand(String.format("dd if=/dev/zero of=%s/%s bs=1024 count=%d",
+ location, filename, sizeInKb));
+ }
+
+ public int getFreeSpaceSize(File path) {
+ StatFs stat = new StatFs(path.getPath());
+ long blockSize = stat.getBlockSize();
+ long availableBlocks = stat.getAvailableBlocks();
+ return (int) ((availableBlocks * blockSize) / (1024 * 1024));
+ }
+
+ public boolean hasAdoptable() {
+ return Boolean.parseBoolean(executeShellCommand("sm has-adoptable").trim());
+ }
+
+ public String getAdoptionDisk() throws InterruptedException {
+ int counter = 10;
+ String disks = null;
+ while (((disks == null || disks.length() == 0)) && counter > 0) {
+ disks = executeShellCommand("sm list-disks adoptable");
+ Thread.sleep(TIMEOUT);
+ --counter;
+ }
+ if (counter == 0) {
+ throw new AssertionError("Devices must have adoptable media inserted");
+ }
+ return disks.split("\n")[0].trim();
+ }
+
+ public Boolean hasPublicVolume() {
+ return (null != executeShellCommand("sm list-volumes public"));
+ }
+
+ public String getAdoptionVolumeId(String volType) throws InterruptedException {
+ return getAdoptionVolumeInfo(volType).volId;
+ }
+
+ public String getAdoptionVolumeUuid(String volType) throws InterruptedException {
+ return getAdoptionVolumeInfo(volType).uuid;
+ }
+
+ public LocalVolumeInfo getAdoptionVolumeInfo(String volType) throws InterruptedException {
+ String[] lines = null;
+ int attempt = 0;
+ while (attempt++ < 5) {
+ if (null != (lines = executeShellCommand("sm list-volumes " + volType).split("\n"))) {
+ for (String line : lines) {
+ final LocalVolumeInfo info = new LocalVolumeInfo(line.trim());
+ if (info.volId.startsWith(volType) && "mounted".equals(info.state)) {
+ return info;
+ }
+ }
+ Thread.sleep(TIMEOUT);
+ }
+ }
+ return null;
+ }
+
+ public void partitionDisk(String type) throws InterruptedException {
+ if (type.equals("private")) {
+ executeShellCommand(String.format("sm partition %s %s", getAdoptionDisk(), type));
+ Thread.sleep(2 * TIMEOUT);
+ } else if (!hasPublicVolume() && type.equals("public")) {
+ executeShellCommand("sm forget all");
+ executeShellCommand(String.format("sm partition %s %s", getAdoptionDisk(), type));
+ Thread.sleep(2 * TIMEOUT);
+ setupAsPortableUiFlow();
+ }
+ }
+
+ public void setupAsPortableUiFlow() throws InterruptedException {
+ openSdCardSetUpNotification();
+ Thread.sleep(TIMEOUT);
+ Pattern pattern = Pattern.compile("Set up", Pattern.CASE_INSENSITIVE);
+ UiObject2 adoptFlowUi = mDevice.wait(Until.findObject(By.desc(pattern)), TIMEOUT);
+ adoptFlowUi.clickAndWait(Until.newWindow(), TIMEOUT);
+ adoptFlowUi = mDevice.wait(Until.findObject(
+ By.res(SETTINGS_PKG, "storage_wizard_init_external_title")),
+ TIMEOUT);
+ adoptFlowUi.click();
+ pattern = Pattern.compile("Next", Pattern.CASE_INSENSITIVE);
+ adoptFlowUi = mDevice.wait(Until.findObject(By.text(pattern)),
+ TIMEOUT);
+ adoptFlowUi.clickAndWait(Until.newWindow(), TIMEOUT);
+ pattern = Pattern.compile("Done", Pattern.CASE_INSENSITIVE);
+ mDevice.wait(Until.findObject(By.text(pattern)), TIMEOUT).clickAndWait(
+ Until.newWindow(), TIMEOUT);
+ hasPublicVolume();
+
+ }
+
+ public void installFromPlayStore(String appName) {
+ PlayStoreHelperImpl mHelper = new PlayStoreHelperImpl(mInstrumentation);
+ mHelper.open();
+ mHelper.doSearch(appName);
+ mHelper.selectFirstResult();
+ mDevice.wait(Until.findObject(By.res(PLAYSTORE_PKG, "buy_button").text("INSTALL")),
+ TIMEOUT).clickAndWait(Until.newWindow(), 2 * TIMEOUT);
+ SystemClock.sleep(2 * TIMEOUT);
+ mDevice.wait(Until.findObject(By.res(PLAYSTORE_PKG, "launch_button").text("OPEN")),
+ 5 * TIMEOUT);
+ }
+
+ public PackageInfo getPackageInfo(String packageName) throws NameNotFoundException {
+ return mContext.getPackageManager().getPackageInfo(packageName, 0);
+ }
+
+ public Boolean doesPackageExist(String packageName) throws NameNotFoundException {
+ try {
+ mContext.getPackageManager().getPackageInfo(packageName, 0);
+ } catch (NameNotFoundException nex) {
+ throw nex;
+ }
+
+ return Boolean.TRUE;
+ }
+
+ public String getInstalledLocation(String packageName) throws NameNotFoundException {
+ Assert.assertTrue(String.format("%s doesn't exist!", packageName),
+ doesPackageExist(packageName));
+ return getPackageInfo(packageName).applicationInfo.dataDir;
+ }
+
+ public void settingsUiCleanUp() {
+ executeShellCommand("pm clear " + SETTINGS_PKG);
+ executeShellCommand("pm clear " + DOCUMENTS_PKG);
+ }
+
+ private static class LocalVolumeInfo {
+ public String volId;
+ public String state;
+ public String uuid;
+
+ public LocalVolumeInfo(String line) {
+ final String[] split = line.split(" ");
+ volId = split[0];
+ state = split[1];
+ uuid = split[2];
+ }
+ }
+
+ public PackageManager getPackageManager() {
+ return mContext.getPackageManager();
+ }
+}
diff --git a/tests/functional/externalstorage/src/com/android/functional/externalstoragetests/PortableStorageTests.java b/tests/functional/externalstorage/src/com/android/functional/externalstoragetests/PortableStorageTests.java
new file mode 100644
index 0000000..79cdfe2
--- /dev/null
+++ b/tests/functional/externalstorage/src/com/android/functional/externalstoragetests/PortableStorageTests.java
@@ -0,0 +1,258 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.functional.externalstoragetests;
+
+import android.app.UiAutomation;
+import android.content.Context;
+import android.content.Intent;
+import android.provider.Settings;
+import android.support.test.uiautomator.By;
+import android.support.test.uiautomator.UiDevice;
+import android.support.test.uiautomator.UiObject2;
+import android.support.test.uiautomator.Until;
+import android.test.InstrumentationTestCase;
+import android.test.suitebuilder.annotation.LargeTest;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import junit.framework.Assert;
+
+public class PortableStorageTests extends InstrumentationTestCase {
+ private UiDevice mDevice = null;
+ private Context mContext = null;
+ private UiAutomation mUiAutomation = null;
+ private ExternalStorageHelper storageHelper;
+
+ @Override
+ protected void setUp() throws Exception {
+ super.setUp();
+ mDevice = UiDevice.getInstance(getInstrumentation());
+ mContext = getInstrumentation().getContext();
+ mUiAutomation = getInstrumentation().getUiAutomation();
+ storageHelper = ExternalStorageHelper.getInstance(mDevice, mContext, mUiAutomation,
+ getInstrumentation());
+ mDevice.setOrientationNatural();
+ }
+
+ /**
+ * Test to ensure sd card can be adopted as portable storage
+ */
+ @LargeTest
+ public void testAdoptAsPortableViaUI() throws InterruptedException {
+ // ensure notification
+ storageHelper.executeShellCommand(String.format(
+ "sm partition %s %s", storageHelper.getAdoptionDisk(), "public"));
+ Thread.sleep(storageHelper.TIMEOUT);
+ storageHelper.setupAsPortableUiFlow();
+ storageHelper.executeShellCommand(String.format("sm forget all"));
+ Thread.sleep(storageHelper.TIMEOUT);
+ }
+
+ /**
+ * tests to ensure copy option is visible for items on portable storage
+ */
+ @LargeTest
+ public void testCopyFromPortable() throws InterruptedException {
+ ensureHasPortable();
+ storageHelper.createFiles(2,
+ String.format("/storage/%s", storageHelper.getAdoptionVolumeUuid("public")));
+ storageHelper.openSDCard();
+ mDevice.wait(Until.findObject(By.res("android:id/title").text("Test_0")),
+ storageHelper.TIMEOUT).click(storageHelper.TIMEOUT);
+ mDevice.wait(Until.findObject(By.desc(Pattern.compile("More options",
+ Pattern.CASE_INSENSITIVE))), storageHelper.TIMEOUT).click();
+ assertNotNull(mDevice.wait(Until.findObject(By.res("android:id/title").text("Copy to…")),
+ 2 * storageHelper.TIMEOUT));
+ mDevice.wait(Until.findObject(By.res("android:id/title").text("Copy to…")),
+ storageHelper.TIMEOUT).clickAndWait(Until.newWindow(), storageHelper.TIMEOUT);
+ mDevice.pressBack();
+ }
+
+ /**
+ * tests to ensure that resources on portable storage can be deleted via UI
+ */
+ @LargeTest
+ public void testDeleteFromPortable() throws InterruptedException {
+ ensureHasPortable();
+ storageHelper.createFiles(2,
+ String.format("/storage/%s", storageHelper.getAdoptionVolumeUuid("public")));
+ storageHelper.openSDCard();
+ mDevice.wait(Until.findObject(By.res("android:id/title").text("Test_0")),
+ storageHelper.TIMEOUT).click(storageHelper.TIMEOUT);
+ mDevice.wait(Until.findObject(By.res("com.android.documentsui:id/menu_sort")),
+ storageHelper.TIMEOUT).clickAndWait(Until.newWindow(), storageHelper.TIMEOUT);
+ assertNull(mDevice.wait(Until.findObject(By.res("android:id/title").text("Test_0")),
+ 2 * storageHelper.TIMEOUT));
+ }
+
+ /**
+ * tests to ensure that external storage is explorable via UI
+ */
+ @LargeTest
+ public void testExplorePortable() throws InterruptedException {
+ ensureHasPortable();
+ // Create 2 random files on SDCard
+ storageHelper.createFiles(2,
+ String.format("/storage/%s", storageHelper.getAdoptionVolumeUuid("public")));
+ Intent intent = new Intent(Settings.ACTION_INTERNAL_STORAGE_SETTINGS);
+ intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ mContext.startActivity(intent);
+ Thread.sleep(storageHelper.TIMEOUT * 2);
+ mDevice.wait(Until.findObject(By.textContains("SD card")), storageHelper.TIMEOUT)
+ .clickAndWait(Until.newWindow(), storageHelper.TIMEOUT);
+ for (int i = 0; i < 2; ++i) {
+ Assert.assertTrue(mDevice.wait(Until.hasObject(By.res("android:id/title")
+ .text(String.format("Test_%d", i))), storageHelper.TIMEOUT));
+ }
+ }
+
+ /**
+ * tests to ensure that resources on portable storage can be shared via UI
+ */
+ @LargeTest
+ public void testShareableFromPortable() throws InterruptedException {
+ ensureHasPortable();
+ storageHelper.createFiles(2,
+ String.format("/storage/%s", storageHelper.getAdoptionVolumeUuid("public")));
+ storageHelper.openSDCard();
+ mDevice.wait(Until.findObject(By.res("android:id/title").text("Test_0")),
+ storageHelper.TIMEOUT).click(storageHelper.TIMEOUT);
+ mDevice.wait(Until.findObject(By.res("com.android.documentsui:id/menu_list")),
+ storageHelper.TIMEOUT).click();
+ assertNotNull(mDevice.wait(Until.findObject(By.res("android:id/resolver_list")),
+ storageHelper.TIMEOUT));
+ // click and ensure intent is sent to share? or actual share?
+ mDevice.pressBack();
+ }
+
+ /**
+ * tests to ensure that portable overflow menu contain all setting options
+ */
+ @LargeTest
+ public void testPortableOverflowSettings() throws InterruptedException {
+ ensureHasPortable();
+ storageHelper.createFiles(2,
+ String.format("/storage/%s", storageHelper.getAdoptionVolumeUuid("public")));
+ storageHelper.openSDCard();
+ mDevice.wait(Until.findObject(By.res("android:id/title").text("Test_0")),
+ storageHelper.TIMEOUT).click(storageHelper.TIMEOUT);
+ assertTrue(mDevice.wait(Until.hasObject(By.res(storageHelper.DOCUMENTS_PKG, "menu_search")),
+ storageHelper.TIMEOUT));
+ assertTrue(mDevice.wait(Until.hasObject(By.res(storageHelper.DOCUMENTS_PKG, "menu_sort")),
+ storageHelper.TIMEOUT));
+ assertTrue(mDevice.wait(Until.hasObject(By.text("1 selected")), storageHelper.TIMEOUT));
+ }
+
+ /**
+ * tests to ensure that portable storage has setting options format, format as internal, eject
+ */
+ @LargeTest
+ public void testPortableSettings() throws InterruptedException {
+ ensureHasPortable();
+ storageHelper.openSDCard();
+ Pattern pattern = Pattern.compile("More options", Pattern.CASE_INSENSITIVE);
+ mDevice.wait(Until.findObject(By.desc(pattern)), storageHelper.TIMEOUT).click();
+ pattern = Pattern.compile("Storage settings", Pattern.CASE_INSENSITIVE);
+ mDevice.wait(Until.findObject(By.text(pattern)), storageHelper.TIMEOUT)
+ .clickAndWait(
+ Until.newWindow(), storageHelper.TIMEOUT);
+ pattern = Pattern.compile("Eject", Pattern.CASE_INSENSITIVE);
+ assertTrue(mDevice.wait(Until.hasObject(By.text(pattern)), storageHelper.TIMEOUT));
+ pattern = Pattern.compile("Format", Pattern.CASE_INSENSITIVE);
+ assertTrue(mDevice.wait(Until.hasObject(By.text(pattern)), storageHelper.TIMEOUT));
+ pattern = Pattern.compile("Format as internal", Pattern.CASE_INSENSITIVE);
+ assertTrue(mDevice.wait(Until.hasObject(By.text(pattern)),
+ storageHelper.TIMEOUT));
+ }
+
+ /**
+ * tests to ensure that portable storage can be ejected from settings
+ */
+ @LargeTest
+ public void testEjectPortable() throws InterruptedException {
+ ensureHasPortable();
+ storageHelper.openStorageSettings();
+ mDevice.wait(Until.findObject(By.res(storageHelper.SETTINGS_PKG, "unmount")),
+ storageHelper.TIMEOUT).click();
+ assertTrue(mDevice.wait(Until.hasObject(By.res("android:id/summary").text("Ejected")),
+ 4 * storageHelper.TIMEOUT));
+ mDevice.wait(Until.findObject(By.textContains("SD card")), 2 * storageHelper.TIMEOUT)
+ .click();
+ Pattern pattern = Pattern.compile("Mount", Pattern.CASE_INSENSITIVE);
+ mDevice.wait(Until.findObject(By.res("android:id/button1").text(pattern)),
+ 2 * storageHelper.TIMEOUT).clickAndWait(Until.newWindow(), storageHelper.TIMEOUT);
+ ;
+ }
+
+ /**
+ * tests to ensure that portable storage can be erased and formated from settings
+ */
+ @LargeTest
+ public void testFormatPortable() throws InterruptedException {
+ ensureHasPortable();
+ storageHelper.openSDCard();
+ Pattern pattern = Pattern.compile("More options", Pattern.CASE_INSENSITIVE);
+ mDevice.wait(Until.findObject(By.desc(pattern)), storageHelper.TIMEOUT).click();
+ pattern = Pattern.compile("Storage settings", Pattern.CASE_INSENSITIVE);
+ mDevice.wait(Until.findObject(By.text(pattern)), storageHelper.TIMEOUT)
+ .clickAndWait(
+ Until.newWindow(), storageHelper.TIMEOUT);
+ UiObject2 format = mDevice.wait(Until.findObject(By.text("Format")), storageHelper.TIMEOUT);
+ format.clickAndWait(Until.newWindow(), storageHelper.TIMEOUT);
+ pattern = Pattern.compile("Erase & Format", Pattern.CASE_INSENSITIVE);
+ mDevice.wait(Until.findObject(By.text(pattern)), storageHelper.TIMEOUT).click();
+ pattern = Pattern.compile("Done", Pattern.CASE_INSENSITIVE);
+ mDevice.wait(Until.findObject(By.text(pattern)), 20 * storageHelper.TIMEOUT).click();
+ }
+
+ /**
+ * tests to ensure that portable storage can be erased and formated as internal from settings
+ */
+ @LargeTest
+ public void testFormatPortableAsAdoptable() throws InterruptedException {
+ try {
+ ensureHasPortable();
+ storageHelper.openSDCard();
+ Pattern pattern = Pattern.compile("More options", Pattern.CASE_INSENSITIVE);
+ mDevice.wait(Until.findObject(By.desc(pattern)), storageHelper.TIMEOUT).click();
+ pattern = Pattern.compile("Storage settings", Pattern.CASE_INSENSITIVE);
+ mDevice.wait(Until.findObject(By.text(pattern)), storageHelper.TIMEOUT)
+ .clickAndWait(
+ Until.newWindow(), storageHelper.TIMEOUT);
+ pattern = Pattern.compile("Format", Pattern.CASE_INSENSITIVE);
+ assertTrue(mDevice.wait(Until.hasObject(By.text(pattern)), storageHelper.TIMEOUT));
+ pattern = Pattern.compile("Format as internal", Pattern.CASE_INSENSITIVE);
+ assertTrue(mDevice.wait(Until.hasObject(By.text(pattern)),
+ storageHelper.TIMEOUT));
+ // Next flow is same as adoption, so no need to test
+ } finally {
+ storageHelper.partitionDisk("public");
+ }
+ }
+
+ private void ensureHasPortable() throws InterruptedException {
+ storageHelper.partitionDisk("public");
+ storageHelper.settingsUiCleanUp();
+ }
+
+ @Override
+ protected void tearDown() throws Exception {
+ mDevice.unfreezeRotation();
+ mDevice.pressBack();
+ mDevice.pressHome();
+ super.tearDown();
+ }
+}
diff --git a/tests/functional/launchertests/Android.mk b/tests/functional/launchertests/Android.mk
new file mode 100644
index 0000000..5166049
--- /dev/null
+++ b/tests/functional/launchertests/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_PACKAGE_NAME := LauncherFunctionalTests
+LOCAL_SRC_FILES := $(call all-java-files-under, src)
+LOCAL_MODULE_TAGS := tests
+
+LOCAL_STATIC_JAVA_LIBRARIES := ub-uiautomator timeresult-helper-lib launcher-helper-lib android-support-test
+
+LOCAL_SDK_VERSION := current
+
+include $(BUILD_PACKAGE)
diff --git a/tests/functional/launchertests/AndroidManifest.xml b/tests/functional/launchertests/AndroidManifest.xml
new file mode 100644
index 0000000..86b75f3
--- /dev/null
+++ b/tests/functional/launchertests/AndroidManifest.xml
@@ -0,0 +1,31 @@
+<?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="android.launcher.functional">
+
+ <application>
+ <uses-library android:name="android.test.runner" />
+ </application>
+
+ <uses-sdk android:minSdkVersion="19"
+ android:targetSdkVersion="23"/>
+
+ <instrumentation
+ android:name="android.support.test.runner.AndroidJUnitRunner"
+ android:targetPackage="android.launcher.functional"
+ android:label="Platform Android Launcher Functional Tests" />
+</manifest>
diff --git a/tests/functional/launchertests/src/com/android/launcher/functional/HomeScreenTests.java b/tests/functional/launchertests/src/com/android/launcher/functional/HomeScreenTests.java
new file mode 100644
index 0000000..9f31232
--- /dev/null
+++ b/tests/functional/launchertests/src/com/android/launcher/functional/HomeScreenTests.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 android.launcher.functional;
+
+import java.io.File;
+import java.io.IOException;
+
+import android.app.UiAutomation;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.Context;
+import android.graphics.Point;
+import android.os.PowerManager;
+import android.os.RemoteException;
+import android.os.SystemClock;
+import android.support.test.launcherhelper.ILauncherStrategy;
+import android.support.test.launcherhelper.LauncherStrategyFactory;
+import android.support.test.uiautomator.By;
+import android.support.test.uiautomator.Direction;
+import android.support.test.uiautomator.UiDevice;
+import android.support.test.uiautomator.UiObject2;
+import android.support.test.uiautomator.UiObjectNotFoundException;
+import android.support.test.uiautomator.Until;
+import android.test.InstrumentationTestCase;
+import android.test.suitebuilder.annotation.MediumTest;
+import android.util.Log;
+import android.view.KeyEvent;
+
+public class HomeScreenTests extends InstrumentationTestCase {
+
+ private static final int TIMEOUT = 3000;
+ private static final String HOTSEAT = "hotseat";
+ private UiDevice mDevice;
+ private PackageManager mPackageManager;
+ private ILauncherStrategy mLauncherStrategy = null;
+
+ @Override
+ public void setUp() throws Exception {
+ super.setUp();
+ mDevice = UiDevice.getInstance(getInstrumentation());
+ mPackageManager = getInstrumentation().getContext().getPackageManager();
+ mDevice.setOrientationNatural();
+ mLauncherStrategy = LauncherStrategyFactory.getInstance(mDevice).getLauncherStrategy();
+ }
+
+ @Override
+ protected void tearDown() throws Exception {
+ mDevice.pressHome();
+ mDevice.unfreezeRotation();
+ mDevice.waitForIdle();
+ super.tearDown();
+ }
+
+ public String getLauncherPackage() {
+ return mDevice.getLauncherPackageName();
+ }
+
+ public void launchAppWithIntent(String appPackageName) {
+ Intent appIntent = mPackageManager.getLaunchIntentForPackage(appPackageName);
+ appIntent.addCategory(Intent.CATEGORY_LAUNCHER);
+ appIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ getInstrumentation().getContext().startActivity(appIntent);
+ SystemClock.sleep(TIMEOUT);
+ }
+
+ @MediumTest
+ public void testGoHome() {
+ launchAppWithIntent("com.android.chrome");
+ mDevice.pressHome();
+ UiObject2 hotseat = mDevice.findObject(By.res(getLauncherPackage(), HOTSEAT));
+ assertNotNull("Hotseat could not be found", hotseat);
+ }
+
+ @MediumTest
+ public void testHomeToRecentsNavigation() throws Exception {
+ mDevice.pressRecentApps();
+ assertNotNull("Recents not found when navigating from hotseat",
+ mDevice.wait(Until.hasObject(By.res("com.android.systemui:id/recents_view")),
+ TIMEOUT));
+ }
+
+ @MediumTest
+ public void testCreateAndDeleteShortcutOnHome() throws Exception {
+ createShortcutOnHome("Calculator");
+ // Verify presence of shortcut on Home screen
+ UiObject2 hotseat = mDevice.findObject(By.res(getLauncherPackage(), HOTSEAT));
+ assertNotNull("Not on Home page; hotseat could not be found", hotseat);
+ UiObject2 calculatorIcon = mDevice.wait(Until.findObject(By.text("Calculator")), TIMEOUT);
+ assertNotNull("Calculator shortcut not found on Home screen", calculatorIcon);
+ removeObjectFromHomeScreen(calculatorIcon, "text", "Calculator");
+ }
+
+ // Screen on with power button press
+ @MediumTest
+ public void testScreenOffOnUsingPowerButton() throws Exception {
+ Context currentContext = getInstrumentation().getContext();
+ PowerManager pm = (PowerManager) currentContext
+ .getSystemService(currentContext.POWER_SERVICE);
+ mDevice.pressKeyCode(KeyEvent.KEYCODE_POWER);
+ Thread.sleep(TIMEOUT);
+ assertFalse("Screen wasn't turned off", pm.isInteractive());
+ mDevice.pressKeyCode(KeyEvent.KEYCODE_POWER);
+ Thread.sleep(TIMEOUT);
+ assertTrue("Screen wasn't turned on by pressing Power Key", pm.isInteractive());
+ // Unlock screen since this ends up putting the device in Swpe lock mode.
+ mDevice.wakeUp();
+ mDevice.pressMenu();
+ }
+
+ // Wallpaper menu from home page
+ @MediumTest
+ public void testLongPressFromHomeToWallpaperMenu() {
+ String wallpaperResourceId = getLauncherPackage() + ":id/wallpaper_image";
+ verifyHomeLongPressMenu("Wallpaper", "wallpaper_button", wallpaperResourceId);
+ }
+
+ // Widget menu from home page
+ @MediumTest
+ public void testLongPressFromHomeToWidgetMenu() {
+ String widgetResourceId = getLauncherPackage() + ":id/widgets_list_view";
+ verifyHomeLongPressMenu("Widgets", "widget_button", widgetResourceId);
+ }
+
+ // Settings menu from home page
+ @MediumTest
+ public void testLongPressFromHomeToGoogleSettingsMenu() {
+ verifyHomeLongPressMenu("Google settings", "settings_button",
+ "android:id/action_bar");
+ }
+
+ // Home screen long press display menu
+ private void verifyHomeLongPressMenu(String longPressElementName,
+ String longPressElementResourceId, String pageLoadResourceId) {
+ mDevice.pressHome();
+ UiObject2 workspace = mDevice.findObject(By.res(getLauncherPackage(), "workspace"));
+ workspace.longClick();
+ UiObject2 longPressElementButton = mDevice.findObject(By.res(getLauncherPackage(),
+ longPressElementResourceId));
+ assertNotNull(longPressElementName +
+ " element is not visible on long press", longPressElementButton);
+ longPressElementButton.click();
+ mDevice.waitForIdle();
+ assertNotNull(longPressElementName + " page hasn't loaded correctly on clicking",
+ mDevice.wait(Until.hasObject(By.res(pageLoadResourceId)), TIMEOUT));
+ }
+
+ // Home screen add widget
+ @MediumTest
+ public void testAddRemoveWidgetOnHome() throws Exception {
+ mDevice.pressHome();
+ mDevice.waitForIdle();
+ UiObject2 workspace = mDevice.findObject(By.res(getLauncherPackage(), "workspace"));
+ workspace.longClick();
+ UiObject2 widgetButton = mDevice.findObject(By.res(getLauncherPackage(),
+ "widget_button"));
+ widgetButton.click();
+ mDevice.waitForIdle();
+ UiObject2 analogClock = mDevice.wait(Until.findObject(By.text("Analog clock")), TIMEOUT);
+ analogClock.click(2000L);
+
+ // Verify presence of shortcut on Home screen
+ UiObject2 hotseat = mDevice.findObject(By.res(getLauncherPackage(), HOTSEAT));
+ assertNotNull("Not on Home page; hotseat could not be found", hotseat);
+ UiObject2 analogClockWidget = mDevice.findObject
+ (By.res("com.google.android.deskclock:id/analog_appwidget"));
+ assertNotNull("Clock widget not found on Home screen", analogClockWidget);
+ removeObjectFromHomeScreen(analogClockWidget, "res",
+ "com.google.android.deskclock:id/analog_appwidget");
+ }
+
+ @MediumTest
+ public void testCreateRenameRemoveFolderOnHome() throws Exception {
+ // Create two shortcuts on the home screen
+ createShortcutOnHome("Calculator");
+ createShortcutOnHome("Clock");
+ mDevice.pressHome();
+ mDevice.waitForIdle();
+
+ // Drag and drop the calculator shortcut onto
+ // the clock shortcut to create a folder.
+ UiObject2 calculatorIcon = mDevice.wait
+ (Until.findObject(By.text("Calculator")), TIMEOUT);
+ UiObject2 clockIcon = mDevice.wait
+ (Until.findObject(By.text("Clock")), TIMEOUT);
+ calculatorIcon.drag(clockIcon.getVisibleCenter(), 1000);
+
+ // Verify that there is a new unnamed folder at this point
+ UiObject2 customFolder = mDevice.wait
+ (Until.findObject(By.desc("Folder: ")), TIMEOUT);
+ customFolder.click();
+ UiObject2 unnamedFolder = mDevice.wait
+ (Until.findObject(By.text("Unnamed Folder")), TIMEOUT);
+ assertNotNull("Custom folder not created", unnamedFolder);
+
+ // Rename the unnamed folder to 'Snowflake'
+ unnamedFolder.click();
+ unnamedFolder.setText("Snowflake");
+
+ // Dismiss the IME and then collapse the folder.
+ mDevice.pressBack();
+ mDevice.pressBack();
+ mDevice.waitForIdle();
+ UiObject2 workspace = mDevice.findObject(By.res(getLauncherPackage(), "workspace"));
+ workspace.click();
+
+ // Verify the newly renamed Snowflake folder
+ UiObject2 snowflakeFolder = mDevice.wait
+ (Until.findObject(By.text("Snowflake")), TIMEOUT);
+ assertNotNull("Custom folder not created", snowflakeFolder);
+
+ // Verify that the Snowflake folder can be removed
+ removeObjectFromHomeScreen(snowflakeFolder, "text", "Snowflake");
+ }
+
+ // Folders - opening an app from folder
+ @MediumTest
+ public void testOpenAppFromFolderOnHome() {
+ mDevice.pressHome();
+ mDevice.waitForIdle();
+ UiObject2 googleFolder = mDevice.wait
+ (Until.findObject(By.desc("Folder: Google")), TIMEOUT);
+ googleFolder.click();
+ UiObject2 youTubeButton = mDevice.wait
+ (Until.findObject(By.text("YouTube")), TIMEOUT);
+ youTubeButton.click();
+ assertTrue("Youtube wasn't opened from the Google folder",
+ mDevice.wait(Until.hasObject
+ (By.pkg("com.google.android.youtube")), TIMEOUT));
+ mDevice.pressHome();
+ mDevice.waitForIdle();
+ }
+
+ /* This method takes in an object to be drag/dropped onto the
+ * Remove button hiding behind the search bar
+ *
+ * @param objectToRemove The UI object to be removed from the
+ * Home screen
+ * @param searchCategory String of value text, res or desc, based on which
+ * the By selector is chosen to find the object
+ * @param searchContent String to be searched in the searchCategory
+ */
+ private void removeObjectFromHomeScreen(UiObject2 objectToRemove,
+ String searchCategory, String searchContent) {
+ // Find the center of the Google Search Bar that the Remove button
+ // is hidden behind.
+ // Note: We're using this hacky way of locating the Remove button
+ // because today, UIAutomator doesn't allow us to search for an element
+ // while a touchdown has been executed, but before the touch up.
+ // FYI: A click is a combination of a touch down and a touch up motion.
+ UiObject2 removeButton = mDevice.wait(Until.findObject(By.desc("Google Search")),
+ TIMEOUT);
+ // Drag the calculator icon to the 'Remove' button to remove it
+ objectToRemove.drag(new Point(mDevice.getDisplayWidth() / 2,
+ removeButton.getVisibleCenter().y), 1000);
+
+ UiObject2 checkForObject = null;
+ // Refetch the calculator icon
+ if (searchCategory.equals("text")) {
+ checkForObject = mDevice.findObject(By.text(searchContent));
+ }
+ else if (searchCategory.equals("res")) {
+ checkForObject = mDevice.findObject(By.res(searchContent));
+ }
+ else if (searchCategory.equals("desc")) {
+ checkForObject = mDevice.findObject(By.desc(searchContent));
+ }
+ else {
+ Log.d(null, "Your search category doesn't match common use cases.");
+ }
+ assertNull(searchContent + " is present on the Home screen after removal attempt",
+ checkForObject);
+ }
+
+ /* Creates a shortcut for the given app name on the Home screen
+ *
+ * @param appName text of the app name as seen in 'All Apps'
+ */
+ private void createShortcutOnHome(String appName) throws Exception {
+ // Navigate to All Apps
+ mDevice.pressHome();
+ UiObject2 allApps = mDevice.findObject(By.desc("Apps"));
+ allApps.click();
+ mDevice.waitForIdle();
+
+ // Long press on the Calculator app for two seconds and release on home screen
+ // to create a shortcut
+ UiObject2 appIcon = mDevice.wait(Until.findObject
+ (By.res(getLauncherPackage(), "icon").text(appName)), TIMEOUT);
+ appIcon.click(2000L);
+ }
+}
diff --git a/tests/functional/launchertests/src/com/android/launcher/functional/HotseatHelper.java b/tests/functional/launchertests/src/com/android/launcher/functional/HotseatHelper.java
new file mode 100644
index 0000000..c0b2c51
--- /dev/null
+++ b/tests/functional/launchertests/src/com/android/launcher/functional/HotseatHelper.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.launcher.functional;
+
+import java.io.IOException;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.os.Environment;
+import android.os.RemoteException;
+import android.os.SystemClock;
+import android.support.test.uiautomator.By;
+import android.support.test.uiautomator.UiDevice;
+import android.support.test.uiautomator.UiObject2;
+import android.support.test.uiautomator.UiObjectNotFoundException;
+import android.support.test.uiautomator.Until;
+
+import junit.framework.Assert;
+
+
+public class HotseatHelper {
+
+ private static final int TIMEOUT = 3000;
+ private UiDevice mDevice;
+ private PackageManager pm;
+ private Context mContext;
+ public static HotseatHelper mInstance = null;
+
+ private HotseatHelper(UiDevice device, Context context) {
+ mDevice = device;
+ mContext = context;
+ }
+
+ public static HotseatHelper getInstance(UiDevice device, Context context) {
+ if (mInstance == null) {
+ mInstance = new HotseatHelper(device, context);
+ }
+ return mInstance;
+ }
+
+ public void launchAppFromHotseat(String textAppName, String appPackage) {
+ mDevice.pressHome();
+ UiObject2 appOnHotseat = mDevice.findObject(By.clazz("android.widget.TextView")
+ .desc(textAppName));
+ Assert.assertNotNull(textAppName + " app couldn't be found on hotseat", appOnHotseat);
+ appOnHotseat.click();
+ UiObject2 appLoaded = mDevice.wait(Until.findObject(By.pkg(appPackage)), TIMEOUT*2);
+ Assert.assertNotNull(textAppName + "app did not load on tapping from hotseat", appLoaded);
+ }
+}
diff --git a/tests/functional/launchertests/src/com/android/launcher/functional/PhoneHotseatTests.java b/tests/functional/launchertests/src/com/android/launcher/functional/PhoneHotseatTests.java
new file mode 100644
index 0000000..9a987f3
--- /dev/null
+++ b/tests/functional/launchertests/src/com/android/launcher/functional/PhoneHotseatTests.java
@@ -0,0 +1,87 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.launcher.functional;
+
+import java.io.IOException;
+import android.content.Intent;
+import android.os.RemoteException;
+import android.os.SystemClock;
+import android.support.test.uiautomator.By;
+import android.support.test.uiautomator.UiDevice;
+import android.support.test.uiautomator.UiObject2;
+import android.support.test.uiautomator.UiObjectNotFoundException;
+import android.support.test.uiautomator.Until;
+import android.test.InstrumentationTestCase;
+import android.test.suitebuilder.annotation.MediumTest;
+
+public class PhoneHotseatTests extends InstrumentationTestCase {
+
+ private static final int TIMEOUT = 3000;
+ private static final String HOTSEAT = "hotseat";
+ private UiDevice mDevice;
+ private HotseatHelper hotseatHelper = null;
+
+ @Override
+ public void setUp() throws Exception {
+ super.setUp();
+ mDevice = UiDevice.getInstance(getInstrumentation());
+ try {
+ mDevice.setOrientationNatural();
+ } catch (RemoteException e) {
+ throw new RuntimeException("failed to freeze device orientaion", e);
+ }
+ hotseatHelper = HotseatHelper.getInstance(mDevice, getInstrumentation().getContext());
+ }
+
+ @Override
+ protected void tearDown() throws Exception {
+ mDevice.pressHome();
+ super.tearDown();
+ }
+
+ public String getLauncherPackage() {
+ return mDevice.getLauncherPackageName();
+ }
+
+ @MediumTest
+ public void testOpenPhoneFromHotseat() {
+ hotseatHelper.launchAppFromHotseat("Phone", "com.google.android.dialer");
+ }
+
+ @MediumTest
+ public void testOpenMessengerFromHotseat() {
+ hotseatHelper.launchAppFromHotseat("Messenger", "com.google.android.apps.messaging");
+ }
+
+ @MediumTest
+ public void testOpenChromeFromHotseat() {
+ hotseatHelper.launchAppFromHotseat("Chrome", "com.android.chrome");
+ }
+
+ @MediumTest
+ public void testOpenCameraFromHotseat() {
+ hotseatHelper.launchAppFromHotseat("Camera", "com.google.android.GoogleCamera");
+ }
+
+ @MediumTest
+ public void testHomeToAllAppsNavigation() {
+ hotseatHelper.launchAppFromHotseat("Apps", getLauncherPackage());
+ assertNotNull("All apps page not found when navigating from hotseat",
+ mDevice.wait(Until.hasObject(By.res(getLauncherPackage(), "apps_view")), TIMEOUT));
+ }
+
+}
diff --git a/tests/functional/launchertests/src/com/android/launcher/functional/TabletHotseatTests.java b/tests/functional/launchertests/src/com/android/launcher/functional/TabletHotseatTests.java
new file mode 100644
index 0000000..7b16a86
--- /dev/null
+++ b/tests/functional/launchertests/src/com/android/launcher/functional/TabletHotseatTests.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 android.launcher.functional;
+
+import java.io.IOException;
+import android.content.Intent;
+import android.os.RemoteException;
+import android.os.SystemClock;
+import android.support.test.uiautomator.By;
+import android.support.test.uiautomator.UiDevice;
+import android.support.test.uiautomator.UiObject2;
+import android.support.test.uiautomator.UiObjectNotFoundException;
+import android.support.test.uiautomator.Until;
+import android.test.InstrumentationTestCase;
+import android.test.suitebuilder.annotation.MediumTest;
+import android.test.suitebuilder.annotation.Suppress;
+
+public class TabletHotseatTests extends InstrumentationTestCase {
+
+ private static final int TIMEOUT = 3000;
+ private static final String HOTSEAT = "hotseat";
+ private UiDevice mDevice;
+ private HotseatHelper hotseatHelper = null;
+
+ @Override
+ public void setUp() throws Exception {
+ super.setUp();
+ mDevice = UiDevice.getInstance(getInstrumentation());
+ try {
+ mDevice.setOrientationNatural();
+ } catch (RemoteException e) {
+ throw new RuntimeException("failed to freeze device orientaion", e);
+ }
+ hotseatHelper = HotseatHelper.getInstance(mDevice, getInstrumentation().getContext());
+ }
+
+ @Override
+ protected void tearDown() throws Exception {
+ mDevice.pressHome();
+ super.tearDown();
+ }
+
+ public String getLauncherPackage() {
+ return mDevice.getLauncherPackageName();
+ }
+
+ @Suppress
+ @MediumTest
+ public void testOpenChromeFromHotseat() {
+ hotseatHelper.launchAppFromHotseat("Chrome", "com.android.chrome");
+ }
+
+ @Suppress
+ @MediumTest
+ public void testOpenCameraFromHotseat() {
+ hotseatHelper.launchAppFromHotseat("Camera", "com.google.android.GoogleCamera");
+ }
+
+ @Suppress
+ @MediumTest
+ public void testOpenGMailFromHotseat() {
+ hotseatHelper.launchAppFromHotseat("Gmail", "com.google.android.gm");
+ }
+
+ @Suppress
+ @MediumTest
+ public void testOpenHangoutsFromHotseat() {
+ hotseatHelper.launchAppFromHotseat("Hangouts", "com.google.android.gms");
+ }
+
+ @Suppress
+ @MediumTest
+ public void testOpenPhotosFromHotseat() {
+ hotseatHelper.launchAppFromHotseat("Photos", "com.google.android.apps.photos");
+ }
+
+ @Suppress
+ @MediumTest
+ public void testOpenYoutubeFromHotseat() {
+ hotseatHelper.launchAppFromHotseat("YouTube", "com.google.android.youtube");
+ }
+
+ @Suppress
+ @MediumTest
+ public void testHomeToAllAppsNavigation() {
+ hotseatHelper.launchAppFromHotseat("Apps", getLauncherPackage());
+ assertNotNull("All apps page not found when navigating from hotseat",
+ mDevice.wait(Until.hasObject(By.res(getLauncherPackage(), "apps_view")), TIMEOUT));
+ }
+}
diff --git a/tests/functional/notificationtests/Android.mk b/tests/functional/notificationtests/Android.mk
new file mode 100644
index 0000000..de2ea8b
--- /dev/null
+++ b/tests/functional/notificationtests/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_PACKAGE_NAME := NotificationFunctionalTests
+LOCAL_SRC_FILES := $(call all-java-files-under, src)
+LOCAL_MODULE_TAGS := tests
+LOCAL_STATIC_JAVA_LIBRARIES := \
+ android-support-test \
+ launcher-helper-lib \
+ ub-uiautomator \
+ services.core
+
+#LOCAL_SDK_VERSION := current
+
+include $(BUILD_PACKAGE)
diff --git a/tests/functional/notificationtests/AndroidManifest.xml b/tests/functional/notificationtests/AndroidManifest.xml
new file mode 100644
index 0000000..970ac39
--- /dev/null
+++ b/tests/functional/notificationtests/AndroidManifest.xml
@@ -0,0 +1,37 @@
+<?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.notification.functional" >
+
+ <application>
+ <uses-library android:name="android.test.runner" />
+ </application>
+
+ <uses-sdk
+ android:minSdkVersion="21"
+ android:targetSdkVersion="24" />
+
+ <uses-permission android:name="android.permission.WRITE_SECURE_SETTINGS" />
+ <uses-permission android:name="android.permission.ACCESS_NOTIFICATION_POLICY" />
+ <uses-permission android:name="com.android.alarm.permission.SET_ALARM" />
+
+ <instrumentation
+ android:name="android.support.test.runner.AndroidJUnitRunner"
+ android:label="Android Notifications Functional Tests"
+ android:targetPackage="com.android.notification.functional" />
+
+</manifest>
diff --git a/tests/functional/notificationtests/res/drawable-xhdpi/stat_notify_email.png b/tests/functional/notificationtests/res/drawable-xhdpi/stat_notify_email.png
new file mode 100644
index 0000000..23c4672
--- /dev/null
+++ b/tests/functional/notificationtests/res/drawable-xhdpi/stat_notify_email.png
Binary files differ
diff --git a/tests/functional/notificationtests/src/com/android/notification/functional/HeadsUpNotificationTests.java b/tests/functional/notificationtests/src/com/android/notification/functional/HeadsUpNotificationTests.java
new file mode 100644
index 0000000..31b4c35
--- /dev/null
+++ b/tests/functional/notificationtests/src/com/android/notification/functional/HeadsUpNotificationTests.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.notification.functional;
+
+import android.app.NotificationManager;
+import android.content.Context;
+import android.content.Intent;
+import android.provider.AlarmClock;
+import android.support.test.uiautomator.By;
+import android.support.test.uiautomator.Direction;
+import android.support.test.uiautomator.UiDevice;
+import android.support.test.uiautomator.UiObject2;
+import android.support.test.uiautomator.Until;
+import android.test.InstrumentationTestCase;
+import android.test.suitebuilder.annotation.LargeTest;
+import android.test.suitebuilder.annotation.MediumTest;
+import android.view.inputmethod.InputMethodManager;
+
+import java.util.Calendar;
+import java.util.GregorianCalendar;
+
+public class HeadsUpNotificationTests extends InstrumentationTestCase {
+ private static final int SHORT_TIMEOUT = 1000;
+ private static final int LONG_TIMEOUT = 2000;
+ private static final int NOTIFICATION_ID_1 = 1;
+ private static final int NOTIFICATION_ID_2 = 2;
+ private static final String NOTIFICATION_CONTENT_TEXT = "INLINE REPLY TEST";
+ private NotificationManager mNotificationManager;
+ private UiDevice mDevice = null;
+ private Context mContext;
+ private NotificationHelper mHelper;
+
+ @Override
+ public void setUp() throws Exception {
+ super.setUp();
+ mDevice = UiDevice.getInstance(getInstrumentation());
+ mContext = getInstrumentation().getContext();
+ mNotificationManager = (NotificationManager) mContext
+ .getSystemService(Context.NOTIFICATION_SERVICE);
+ mHelper = new NotificationHelper(mDevice, getInstrumentation(), mNotificationManager);
+ mDevice.setOrientationNatural();
+ mHelper.unlockScreen();
+ mDevice.pressHome();
+ mNotificationManager.cancelAll();
+ }
+
+ @Override
+ public void tearDown() throws Exception {
+ mNotificationManager.cancelAll();
+ mDevice.pressHome();
+ mDevice.unfreezeRotation();
+ super.tearDown();
+ }
+
+ @MediumTest
+ public void testHeadsUpNotificationInlineReply() throws Exception {
+ mHelper.sendNotificationsWithInLineReply(NOTIFICATION_ID_1, true);
+ Thread.sleep(SHORT_TIMEOUT);
+ mDevice.wait(Until.findObject(By.text("REPLY")), LONG_TIMEOUT).click();
+ try {
+ UiObject2 replyBox = mDevice.wait(
+ Until.findObject(By.res("com.android.systemui:id/remote_input_send")),
+ LONG_TIMEOUT);
+ InputMethodManager imm = (InputMethodManager) mContext
+ .getSystemService(Context.INPUT_METHOD_SERVICE);
+ if (!imm.isAcceptingText()) {
+ assertNotNull("Keyboard for inline reply has not loaded correctly", replyBox);
+ }
+ } finally {
+ mDevice.pressBack();
+ }
+ }
+
+ @MediumTest
+ public void testHeadsUpNotificationManualDismiss() throws Exception {
+ mHelper.sendNotificationsWithInLineReply(NOTIFICATION_ID_1, true);
+ Thread.sleep(SHORT_TIMEOUT);
+ UiObject2 obj = mDevice.wait(Until.findObject(By.text(NOTIFICATION_CONTENT_TEXT)),
+ LONG_TIMEOUT);
+ obj.swipe(Direction.LEFT, 1.0f);
+ Thread.sleep(SHORT_TIMEOUT);
+ if (mHelper.checkNotificationExistence(NOTIFICATION_ID_1, true)) {
+ fail(String.format("Notification %s has not been auto dismissed", NOTIFICATION_ID_1));
+ }
+ }
+
+ @LargeTest
+ public void testHeadsUpNotificationAutoDismiss() throws Exception {
+ mHelper.sendNotificationsWithInLineReply(NOTIFICATION_ID_1, true);
+ Thread.sleep(LONG_TIMEOUT * 3);
+ UiObject2 obj = mDevice.wait(Until.findObject(By.text(NOTIFICATION_CONTENT_TEXT)),
+ LONG_TIMEOUT);
+ assertNull(String.format("Notification %s has not been auto dismissed", NOTIFICATION_ID_1),
+ obj);
+ }
+
+ @MediumTest
+ public void testHeadsUpNotificationInlineReplyMulti() throws Exception {
+ mHelper.sendNotificationsWithInLineReply(NOTIFICATION_ID_1, true);
+ Thread.sleep(LONG_TIMEOUT);
+ mDevice.wait(Until.findObject(By.text("REPLY")), LONG_TIMEOUT).click();
+ UiObject2 replyBox = mDevice.wait(
+ Until.findObject(By.res("com.android.systemui:id/remote_input_send")),
+ LONG_TIMEOUT);
+ InputMethodManager imm = (InputMethodManager) mContext
+ .getSystemService(Context.INPUT_METHOD_SERVICE);
+ if (!imm.isAcceptingText()) {
+ assertNotNull("Keyboard for inline reply has not loaded correctly", replyBox);
+ }
+ mHelper.sendNotificationsWithInLineReply(NOTIFICATION_ID_2, true);
+ Thread.sleep(LONG_TIMEOUT);
+ UiObject2 obj = mDevice.wait(Until.findObject(By.text(NOTIFICATION_CONTENT_TEXT)),
+ LONG_TIMEOUT);
+ if (obj == null) {
+ assertNull(String.format("Notification %s can not be found", NOTIFICATION_ID_1),
+ obj);
+ }
+ }
+
+ @LargeTest
+ public void testAlarm() throws Exception {
+ try {
+ setAlarmNow();
+ UiObject2 obj = mDevice.wait(Until.findObject(By.text("test")), 60000);
+ if (obj == null) {
+ fail("Alarm heads up notifcation is not working");
+ }
+ } finally {
+ mDevice.wait(Until.findObject(By.text("DISMISS")), LONG_TIMEOUT).click();
+ }
+ }
+
+ private void setAlarmNow() throws InterruptedException {
+ GregorianCalendar cal = new GregorianCalendar();
+ cal.setTimeInMillis(System.currentTimeMillis());
+ int hour = cal.get(Calendar.HOUR_OF_DAY);
+ int minute = cal.get(Calendar.MINUTE) + 1;// to make sure it won't be set at the next day
+ Intent intent = new Intent(AlarmClock.ACTION_SET_ALARM);
+ intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ intent.putExtra(AlarmClock.EXTRA_HOUR, hour);
+ intent.putExtra(AlarmClock.EXTRA_MINUTES, minute);
+ intent.putExtra(AlarmClock.EXTRA_SKIP_UI, true);
+ intent.putExtra(AlarmClock.EXTRA_MESSAGE, "test");
+ mContext.startActivity(intent);
+ Thread.sleep(LONG_TIMEOUT * 2);
+ }
+}
diff --git a/tests/functional/notificationtests/src/com/android/notification/functional/NotificationBundlingTests.java b/tests/functional/notificationtests/src/com/android/notification/functional/NotificationBundlingTests.java
new file mode 100644
index 0000000..84e3f57
--- /dev/null
+++ b/tests/functional/notificationtests/src/com/android/notification/functional/NotificationBundlingTests.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.notification.functional;
+
+import android.app.NotificationManager;
+import android.content.Context;
+import android.os.RemoteException;
+import android.support.test.uiautomator.By;
+import android.support.test.uiautomator.UiDevice;
+import android.support.test.uiautomator.UiObject2;
+import android.support.test.uiautomator.Until;
+import android.test.InstrumentationTestCase;
+import android.test.suitebuilder.annotation.MediumTest;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+public class NotificationBundlingTests extends InstrumentationTestCase {
+ private static final int SHORT_TIMEOUT = 200;
+ private static final int LONG_TIMEOUT = 2000;
+ private static final int GROUP_NOTIFICATION_ID = 1;
+ private static final int CHILD_NOTIFICATION_ID = 100;
+ private static final int SECOND_CHILD_NOTIFICATION_ID = 101;
+ private static final String BUNDLE_GROUP_KEY = "group_key ";
+ private NotificationManager mNotificationManager;
+ private UiDevice mDevice = null;
+ private Context mContext;
+ private NotificationHelper mHelper;
+
+ @Override
+ public void setUp() throws Exception {
+ super.setUp();
+ mDevice = UiDevice.getInstance(getInstrumentation());
+ mContext = getInstrumentation().getContext();
+ mNotificationManager = (NotificationManager) mContext
+ .getSystemService(Context.NOTIFICATION_SERVICE);
+ mHelper = new NotificationHelper(mDevice, getInstrumentation(), mNotificationManager);
+ mDevice.pressHome();
+ mNotificationManager.cancelAll();
+ try {
+ mDevice.setOrientationNatural();
+ } catch (RemoteException e) {
+ throw new RuntimeException("failed to freeze device orientaion", e);
+ }
+ }
+
+ @Override
+ public void tearDown() throws Exception {
+ mNotificationManager.cancelAll();
+ mDevice.pressHome();
+ super.tearDown();
+ }
+
+ @MediumTest
+ public void testBundlingNotification() throws Exception {
+ List<Integer> lists = new ArrayList<Integer>(Arrays.asList(GROUP_NOTIFICATION_ID,
+ CHILD_NOTIFICATION_ID, SECOND_CHILD_NOTIFICATION_ID));
+ mHelper.sendBundlingNotifications(lists, BUNDLE_GROUP_KEY);
+ Thread.sleep(SHORT_TIMEOUT);
+ mHelper.swipeDown();
+ UiObject2 obj = mDevice.wait(
+ Until.findObject(By.res("com.android.systemui", "notification_title")),
+ LONG_TIMEOUT);
+ int currentY = obj.getVisibleCenter().y;
+ mDevice.wait(Until.findObject(By.res("android:id/expand_button")), LONG_TIMEOUT).click();
+ obj = mDevice.wait(Until.findObject(By.textContains(lists.get(1).toString())),
+ LONG_TIMEOUT);
+ assertFalse("The notifications have not been bundled",
+ obj.getVisibleCenter().y == currentY);
+ }
+
+ @MediumTest
+ public void testDismissBundlingNotification() throws Exception {
+ List<Integer> lists = new ArrayList<Integer>(Arrays.asList(GROUP_NOTIFICATION_ID,
+ CHILD_NOTIFICATION_ID, SECOND_CHILD_NOTIFICATION_ID));
+ mHelper.sendBundlingNotifications(lists, BUNDLE_GROUP_KEY);
+ mHelper.swipeDown();
+ dismissObject(Integer.toString(CHILD_NOTIFICATION_ID));
+ Thread.sleep(LONG_TIMEOUT);
+ for (int n : lists) {
+ if (mHelper.checkNotificationExistence(n, true)) {
+ fail(String.format("Notification %s has not been dismissed", n));
+ }
+ }
+ }
+
+ @MediumTest
+ public void testDismissIndividualNotification() throws Exception {
+ List<Integer> lists = new ArrayList<Integer>(Arrays.asList(GROUP_NOTIFICATION_ID,
+ CHILD_NOTIFICATION_ID, SECOND_CHILD_NOTIFICATION_ID));
+ mHelper.sendBundlingNotifications(lists, BUNDLE_GROUP_KEY);
+ Thread.sleep(SHORT_TIMEOUT);
+ mDevice.openNotification();
+ mDevice.wait(Until.findObject(By.res("android:id/expand_button")), LONG_TIMEOUT).click();
+ dismissObject(Integer.toString(CHILD_NOTIFICATION_ID));
+ Thread.sleep(LONG_TIMEOUT);
+ if (mHelper.checkNotificationExistence(CHILD_NOTIFICATION_ID, true)) {
+ fail(String.format("Notification %s has not been dismissed", CHILD_NOTIFICATION_ID));
+ }
+ if (mHelper.checkNotificationExistence(GROUP_NOTIFICATION_ID, false)) {
+ fail(String.format("Notification %s has been dismissed ", GROUP_NOTIFICATION_ID));
+ }
+ }
+
+ private void dismissObject(String text) {
+ UiObject2 obj = mDevice.wait(
+ Until.findObject(By.textContains(text)),
+ LONG_TIMEOUT);
+ int y = obj.getVisibleBounds().centerY();
+ mDevice.swipe(0, y, mDevice.getDisplayWidth(),
+ y, 5);
+ }
+}
diff --git a/tests/functional/notificationtests/src/com/android/notification/functional/NotificationDNDTests.java b/tests/functional/notificationtests/src/com/android/notification/functional/NotificationDNDTests.java
new file mode 100644
index 0000000..16fd608
--- /dev/null
+++ b/tests/functional/notificationtests/src/com/android/notification/functional/NotificationDNDTests.java
@@ -0,0 +1,178 @@
+
+package com.android.notification.functional;
+
+import android.app.Notification;
+import android.app.NotificationManager;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.Intent;
+import android.media.AudioManager;
+import android.net.Uri;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.RemoteException;
+import android.provider.Settings;
+import android.provider.Settings.Global;
+import android.provider.Settings.SettingNotFoundException;
+import android.service.notification.StatusBarNotification;
+import android.service.notification.ZenModeConfig;
+import android.support.test.uiautomator.By;
+import android.support.test.uiautomator.UiDevice;
+import android.support.test.uiautomator.UiObject;
+import android.support.test.uiautomator.UiObject2;
+import android.support.test.uiautomator.Until;
+import android.test.InstrumentationTestCase;
+import android.test.suitebuilder.annotation.LargeTest;
+import android.test.suitebuilder.annotation.MediumTest;
+import android.util.Log;
+
+import com.android.server.notification.NotificationManagerService;
+import com.android.server.notification.ConditionProviders;
+import com.android.server.notification.ManagedServices.UserProfiles;
+
+import java.util.concurrent.Callable;
+import java.util.concurrent.FutureTask;
+
+import com.android.server.notification.ZenModeHelper;
+import com.android.server.notification.NotificationRecord;
+import com.android.server.notification.ZenModeFiltering;
+
+public class NotificationDNDTests extends InstrumentationTestCase {
+ private static final String LOG_TAG = NotificationDNDTests.class.getSimpleName();
+ private static final int SHORT_TIMEOUT = 1000;
+ private static final int LONG_TIMEOUT = 2000;
+ private static final int NOTIFICATION_ID = 1;
+ private static final String NOTIFICATION_CONTENT_TEXT = "INLINE REPLY TEST";
+ private NotificationManager mNotificationManager;
+ private UiDevice mDevice = null;
+ private Context mContext;
+ private NotificationHelper mHelper;
+ private ContentResolver mResolver;
+ private ZenModeHelper mZenHelper;
+ private boolean isGranted = false;
+
+ @Override
+ public void setUp() throws Exception {
+ super.setUp();
+ mDevice = UiDevice.getInstance(getInstrumentation());
+ mContext = getInstrumentation().getTargetContext();
+ mNotificationManager = (NotificationManager) mContext
+ .getSystemService(Context.NOTIFICATION_SERVICE);
+ mHelper = new NotificationHelper(mDevice, getInstrumentation(), mNotificationManager);
+ mResolver = mContext.getContentResolver();
+ ConditionProviders cps = new ConditionProviders(
+ mContext, new Handler(Looper.getMainLooper()),
+ new UserProfiles());
+ Callable<ZenModeHelper> callable = new Callable<ZenModeHelper>() {
+ @Override
+ public ZenModeHelper call() throws Exception {
+ return new ZenModeHelper(mContext, Looper.getMainLooper(), cps);
+ }
+ };
+ FutureTask<ZenModeHelper> task = new FutureTask<>(callable);
+ getInstrumentation().runOnMainSync(task);
+ mZenHelper = task.get();
+ mDevice.setOrientationNatural();
+ mHelper.unlockScreen();
+ mDevice.pressHome();
+ mNotificationManager.cancelAll();
+ isGranted = mNotificationManager.isNotificationPolicyAccessGranted();
+ }
+
+ @Override
+ public void tearDown() throws Exception {
+ mNotificationManager.cancelAll();
+ mDevice.pressHome();
+ mDevice.unfreezeRotation();
+ super.tearDown();
+ }
+
+ @LargeTest
+ public void testDND() throws Exception {
+ if (!isGranted) {
+ grantPolicyAccess(true);
+ }
+ int setting = mNotificationManager.getCurrentInterruptionFilter();
+ try {
+ mNotificationManager
+ .setInterruptionFilter(NotificationManager.INTERRUPTION_FILTER_PRIORITY);
+ mHelper.sendNotificationsWithInLineReply(NOTIFICATION_ID, true);
+ Thread.sleep(LONG_TIMEOUT);
+ NotificationRecord nr = new NotificationRecord(mContext,
+ mHelper.getStatusBarNotification(NOTIFICATION_ID));
+ ZenModeConfig mConfig = mZenHelper.getConfig();
+ ZenModeFiltering zF = new ZenModeFiltering(mContext);
+ assertTrue(zF.shouldIntercept(mNotificationManager.getZenMode(), mConfig, nr));
+ } finally {
+ mNotificationManager.setInterruptionFilter(setting);
+ if (!isGranted) {
+ grantPolicyAccess(false);
+ }
+ }
+ }
+
+ @LargeTest
+ public void testPriority() throws Exception {
+ int setting = mNotificationManager.getCurrentInterruptionFilter();
+ if (!isGranted) {
+ grantPolicyAccess(true);
+ }
+ mNotificationManager
+ .setInterruptionFilter(NotificationManager.INTERRUPTION_FILTER_PRIORITY);
+ try {
+ mHelper.showInstalledAppDetails(mContext, "com.android.notification.functional");
+ mDevice.wait(Until.findObject(By.textContains("Override Do Not Disturb")), LONG_TIMEOUT)
+ .click();
+ Thread.sleep(LONG_TIMEOUT);
+ mHelper.sendNotificationsWithInLineReply(NOTIFICATION_ID, true);
+ Thread.sleep(LONG_TIMEOUT);
+ NotificationRecord nr = new NotificationRecord(mContext,
+ mHelper.getStatusBarNotification(NOTIFICATION_ID));
+ ZenModeConfig mConfig = mZenHelper.getConfig();
+ ZenModeFiltering zF = new ZenModeFiltering(mContext);
+ assertFalse(zF.shouldIntercept(mZenHelper.getZenMode(), mConfig, nr));
+ } finally {
+ mHelper.showInstalledAppDetails(mContext, "com.android.notification.functional");
+ mDevice.wait(Until.findObject(By.textContains("Override Do Not Disturb")), LONG_TIMEOUT)
+ .click();
+ mNotificationManager.setInterruptionFilter(setting);
+ if (!isGranted) {
+ grantPolicyAccess(false);
+ }
+ }
+ }
+
+ @LargeTest
+ public void testBlockNotification() throws Exception {
+ try {
+ mHelper.showInstalledAppDetails(mContext, "com.android.notification.functional");
+ mDevice.wait(Until.findObject(By.textContains("Block all")), LONG_TIMEOUT).click();
+ Thread.sleep(LONG_TIMEOUT);
+ mHelper.sendNotificationsWithInLineReply(NOTIFICATION_ID, true);
+ Thread.sleep(LONG_TIMEOUT);
+ if (mHelper.checkNotificationExistence(NOTIFICATION_ID, true)) {
+ fail(String.format("Notification %s has not benn blocked", NOTIFICATION_ID));
+ }
+ } finally {
+ mHelper.showInstalledAppDetails(mContext, "com.android.notification.functional");
+ mDevice.wait(Until.findObject(By.textContains("Block all")), LONG_TIMEOUT).click();
+ }
+ }
+
+ private void grantPolicyAccess(boolean isGranted) throws Exception {
+ NotificationHelper.launchSettingsPage(mContext,
+ android.provider.Settings.ACTION_NOTIFICATION_POLICY_ACCESS_SETTINGS);
+ Thread.sleep(LONG_TIMEOUT);
+ if (isGranted) {
+ mDevice.wait(Until.findObject(By.text("OFF")), LONG_TIMEOUT).click();
+ Thread.sleep(SHORT_TIMEOUT);
+ mDevice.wait(Until.findObject(By.text("ALLOW")), LONG_TIMEOUT).click();
+ } else {
+ mDevice.wait(Until.findObject(By.text("ON")), LONG_TIMEOUT).click();
+ Thread.sleep(SHORT_TIMEOUT);
+ mDevice.wait(Until.findObject(By.text("OK")), LONG_TIMEOUT).click();
+ }
+ Thread.sleep(LONG_TIMEOUT);
+ mDevice.pressHome();
+ }
+}
diff --git a/tests/functional/notificationtests/src/com/android/notification/functional/NotificationHelper.java b/tests/functional/notificationtests/src/com/android/notification/functional/NotificationHelper.java
new file mode 100644
index 0000000..7e36fa8
--- /dev/null
+++ b/tests/functional/notificationtests/src/com/android/notification/functional/NotificationHelper.java
@@ -0,0 +1,383 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.notification.functional;
+
+import android.app.AlarmManager;
+import android.app.Instrumentation;
+import android.app.IntentService;
+import android.app.KeyguardManager;
+import android.app.Notification;
+import android.app.Notification.Builder;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.app.RemoteInput;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.graphics.Typeface;
+import android.net.Uri;
+import android.os.Handler;
+import android.os.RemoteException;
+import android.os.SystemClock;
+import android.provider.Settings;
+import android.service.notification.StatusBarNotification;
+import android.support.test.uiautomator.By;
+import android.support.test.uiautomator.Direction;
+import android.support.test.uiautomator.UiDevice;
+import android.support.test.uiautomator.UiObject;
+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.text.SpannableStringBuilder;
+import android.text.style.StyleSpan;
+import android.util.Log;
+import android.widget.EditText;
+import android.widget.ListView;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import com.android.notification.functional.R;
+
+import java.lang.InterruptedException;
+import java.util.List;
+import java.util.Map;
+
+public class NotificationHelper {
+
+ private static final String LOG_TAG = NotificationHelper.class.getSimpleName();
+ private static final int LONG_TIMEOUT = 2000;
+ private static final int SHORT_TIMEOUT = 200;
+ private static final String KEY_QUICK_REPLY_TEXT = "quick_reply";
+ private static final UiSelector LIST_VIEW = new UiSelector().className(ListView.class);
+ private static final UiSelector LIST_ITEM_VALUE = new UiSelector().className(TextView.class);
+
+ private UiDevice mDevice;
+ private Instrumentation mInst;
+ private NotificationManager mNotificationManager = null;
+ private Context mContext = null;
+
+ public NotificationHelper(UiDevice device, Instrumentation inst, NotificationManager nm) {
+ this.mDevice = device;
+ mInst = inst;
+ mNotificationManager = nm;
+ mContext = inst.getContext();
+ }
+
+ public void sleepAndWakeUpDevice() throws RemoteException, InterruptedException {
+ mDevice.sleep();
+ Thread.sleep(LONG_TIMEOUT);
+ mDevice.wakeUp();
+ }
+
+ public static void launchSettingsPage(Context ctx, String pageName) throws Exception {
+ Intent intent = new Intent(pageName);
+ intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ ctx.startActivity(intent);
+ Thread.sleep(LONG_TIMEOUT * 2);
+ }
+
+ /**
+ * Sets the screen lock pin
+ * @param pin 4 digits
+ * @return false if a pin is already set or pin value is not 4 digits
+ * @throws UiObjectNotFoundException
+ */
+ public boolean setScreenLockPin(int pin) throws Exception {
+ if (pin >= 0 && pin <= 9999) {
+ navigateToScreenLock();
+ if (new UiObject(new UiSelector().text("Confirm your PIN")).exists()) {
+ UiObject pinField = new UiObject(
+ new UiSelector().className(EditText.class.getName()));
+ pinField.setText(String.format("%04d", pin));
+ mDevice.pressEnter();
+ }
+ new UiObject(new UiSelector().text("PIN")).click();
+ clickText("No thanks");
+ UiObject pinField = new UiObject(new UiSelector().className(EditText.class.getName()));
+ pinField.setText(String.format("%04d", pin));
+ mDevice.pressEnter();
+ pinField.setText(String.format("%04d", pin));
+ mDevice.pressEnter();
+ clickText("Hide sensitive notification content");
+ clickText("DONE");
+ return true;
+ }
+ return false;
+ }
+
+ public boolean removeScreenLock(int pin, String mode) throws Exception {
+ navigateToScreenLock();
+ if (new UiObject(new UiSelector().text("Confirm your PIN")).exists()) {
+ UiObject pinField = new UiObject(new UiSelector().className(EditText.class.getName()));
+ pinField.setText(String.format("%04d", pin));
+ mDevice.pressEnter();
+ clickText(mode);
+ clickText("YES, REMOVE");
+ } else {
+ clickText(mode);
+ }
+ return true;
+ }
+
+ public void unlockScreenByPin(int pin) throws Exception {
+ String command = String.format(" %s %s %s", "input", "text", Integer.toString(pin));
+ executeAdbCommand(command);
+ Thread.sleep(SHORT_TIMEOUT);
+ mDevice.pressEnter();
+ }
+
+ public void enableNotificationViaAdb(boolean isShow) {
+ String command = String.format(" %s %s %s %s %s", "settings", "put", "secure",
+ "lock_screen_show_notifications",
+ isShow ? "1" : "0");
+ executeAdbCommand(command);
+ }
+
+ public void executeAdbCommand(String command) {
+ Log.i(LOG_TAG, String.format("executing - %s", command));
+ mInst.getUiAutomation().executeShellCommand(command);
+ mDevice.waitForIdle();
+ }
+
+ private void navigateToScreenLock() throws Exception {
+ launchSettingsPage(mInst.getContext(), Settings.ACTION_SECURITY_SETTINGS);
+ new UiObject(new UiSelector().text("Screen lock")).click();
+ }
+
+ private void clickText(String text) throws UiObjectNotFoundException {
+ mDevice.wait(Until.findObject(By.text(text)), LONG_TIMEOUT).click();
+ }
+
+ public void sendNotification(int id, int visibility, String title) throws Exception {
+ Log.v(LOG_TAG, "Sending out notification...");
+ Intent intent = new Intent(Intent.ACTION_VIEW);
+ PendingIntent pendingIntent = PendingIntent.getActivity(mContext, 0, intent, 0);
+ CharSequence subtitle = String.valueOf(System.currentTimeMillis());
+ Notification notification = new Notification.Builder(mContext)
+ .setSmallIcon(R.drawable.stat_notify_email)
+ .setWhen(System.currentTimeMillis()).setContentTitle(title).setContentText(subtitle)
+ .setContentIntent(pendingIntent).setVisibility(visibility)
+ .setPriority(Notification.PRIORITY_HIGH)
+ .build();
+ mNotificationManager.notify(id, notification);
+ Thread.sleep(LONG_TIMEOUT);
+ }
+
+ public void sendNotifications(Map<Integer, String> lists) throws Exception {
+ Log.v(LOG_TAG, "Sending out notification...");
+ Intent intent = new Intent(Intent.ACTION_VIEW);
+ PendingIntent pendingIntent = PendingIntent.getActivity(mContext, 0, intent, 0);
+ CharSequence subtitle = String.valueOf(System.currentTimeMillis());
+ for (Map.Entry<Integer, String> l : lists.entrySet()) {
+ Notification notification = new Notification.Builder(mContext)
+ .setSmallIcon(R.drawable.stat_notify_email)
+ .setWhen(System.currentTimeMillis()).setContentTitle(l.getValue())
+ .setContentText(subtitle)
+ .build();
+ mNotificationManager.notify(l.getKey(), notification);
+ }
+ Thread.sleep(LONG_TIMEOUT);
+ }
+
+ public void sendBundlingNotifications(List<Integer> lists, String groupKey) throws Exception {
+ Notification childNotification = new Notification.Builder(mContext)
+ .setContentTitle(lists.get(1).toString())
+ .setSmallIcon(R.drawable.stat_notify_email)
+ .setGroup(groupKey)
+ .build();
+ mNotificationManager.notify(lists.get(1),
+ childNotification);
+ childNotification = new Notification.Builder(mContext)
+ .setContentText(lists.get(2).toString())
+ .setSmallIcon(R.drawable.stat_notify_email)
+ .setGroup(groupKey)
+ .build();
+ mNotificationManager.notify(lists.get(2),
+ childNotification);
+ Notification notification = new Notification.Builder(mContext)
+ .setContentTitle(lists.get(0).toString())
+ .setSubText(groupKey)
+ .setSmallIcon(R.drawable.stat_notify_email)
+ .setGroup(groupKey)
+ .setGroupSummary(true)
+ .build();
+ mNotificationManager.notify(lists.get(0),
+ notification);
+ }
+
+ static SpannableStringBuilder BOLD(CharSequence str) {
+ final SpannableStringBuilder ssb = new SpannableStringBuilder(str);
+ ssb.setSpan(new StyleSpan(Typeface.BOLD), 0, ssb.length(), 0);
+ return ssb;
+ }
+
+ public boolean checkNotificationExistence(int id, boolean exists) throws Exception {
+ boolean isFound = false;
+ for (int tries = 3; tries-- > 0;) {
+ isFound = false;
+ StatusBarNotification[] sbns = mNotificationManager.getActiveNotifications();
+ for (StatusBarNotification sbn : sbns) {
+ if (sbn.getId() == id) {
+ isFound = true;
+ break;
+ }
+ }
+ if (isFound == exists) {
+ break;
+ }
+ Thread.sleep(SHORT_TIMEOUT);
+ }
+ Log.i(LOG_TAG, "checkNotificationExistence..." + isFound);
+ return isFound == exists;
+ }
+
+ public StatusBarNotification getStatusBarNotification(int id) {
+ StatusBarNotification[] sbns = mNotificationManager.getActiveNotifications();
+ StatusBarNotification n = null;
+ for (StatusBarNotification sbn : sbns) {
+ if (sbn.getId() == id) {
+ n = sbn;
+ break;
+ }
+ }
+ return n;
+ }
+
+ public void swipeUp() throws Exception {
+ mDevice.swipe(mDevice.getDisplayWidth() / 2, mDevice.getDisplayHeight(),
+ mDevice.getDisplayWidth() / 2, 0, 30);
+ Thread.sleep(SHORT_TIMEOUT);
+ }
+
+ public void swipeDown() throws Exception {
+ mDevice.swipe(mDevice.getDisplayWidth() / 2, 0, mDevice.getDisplayWidth() / 2,
+ mDevice.getDisplayHeight() / 2 + 50, 20);
+ Thread.sleep(SHORT_TIMEOUT);
+ }
+
+ public void unlockScreen() throws Exception {
+ KeyguardManager myKM = (KeyguardManager) mContext
+ .getSystemService(Context.KEYGUARD_SERVICE);
+ if (myKM.inKeyguardRestrictedInputMode()) {
+ // it is locked
+ swipeUp();
+ }
+ }
+
+ public void showInstalledAppDetails(Context context, String packageName) throws Exception {
+ Intent intent = new Intent();
+ intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ Uri uri = Uri.fromParts("package", packageName, null);
+ intent.setData(uri);
+ intent.setClassName("com.android.settings",
+ "com.android.settings.Settings$AppNotificationSettingsActivity");
+ intent.putExtra("app_package", mContext.getPackageName());
+ intent.putExtra("app_uid", mContext.getApplicationInfo().uid);
+ context.startActivity(intent);
+ Thread.sleep(LONG_TIMEOUT * 2);
+ }
+
+ /**
+ * This is the main list view containing the items that settings are possible for
+ */
+ public static class SettingsListView {
+ public static boolean selectSettingsFor(String name) throws UiObjectNotFoundException {
+ UiScrollable settingsList = new UiScrollable(
+ new UiSelector().resourceId("android:id/content"));
+ UiObject appSettings = settingsList.getChildByText(LIST_ITEM_VALUE, name);
+ if (appSettings != null) {
+ return appSettings.click();
+ }
+ return false;
+ }
+
+ public boolean checkSettingsExists(String name) {
+ try {
+ UiScrollable settingsList = new UiScrollable(LIST_VIEW);
+ UiObject appSettings = settingsList.getChildByText(LIST_ITEM_VALUE, name);
+ return appSettings.exists();
+ } catch (UiObjectNotFoundException e) {
+ return false;
+ }
+ }
+ }
+
+ public void sendNotificationsWithInLineReply(int notificationId, boolean isHeadsUp) {
+ Notification.Action action = new Notification.Action.Builder(
+ R.drawable.stat_notify_email, "Reply", ToastService.getPendingIntent(mContext,
+ "inline reply test"))
+ .addRemoteInput(new RemoteInput.Builder(KEY_QUICK_REPLY_TEXT)
+ .setLabel("Quick reply").build())
+ .build();
+ Notification.Builder n = new Notification.Builder(mContext)
+ .setContentTitle(Integer.toString(notificationId))
+ .setContentText("INLINE REPLY TEST")
+ .setWhen(System.currentTimeMillis())
+ .setSmallIcon(R.drawable.stat_notify_email)
+ .addAction(action);
+ if (isHeadsUp) {
+ n.setPriority(Notification.PRIORITY_HIGH)
+ .setDefaults(Notification.DEFAULT_VIBRATE);
+ }
+ mNotificationManager.notify(notificationId, n.build());
+ }
+
+ public static class ToastService extends IntentService {
+ private static final String TAG = "ToastService";
+ private static final String ACTION_TOAST = "toast";
+ private Handler handler;
+
+ public ToastService() {
+ super(TAG);
+ }
+
+ public ToastService(String name) {
+ super(name);
+ }
+
+ @Override
+ public int onStartCommand(Intent intent, int flags, int startId) {
+ handler = new Handler();
+ return super.onStartCommand(intent, flags, startId);
+ }
+
+ @Override
+ protected void onHandleIntent(Intent intent) {
+ if (intent.hasExtra("text")) {
+ final String text = intent.getStringExtra("text");
+ handler.post(new Runnable() {
+ @Override
+ public void run() {
+ Toast.makeText(ToastService.this, text, Toast.LENGTH_LONG).show();
+ Log.v(TAG, "toast " + text);
+ }
+ });
+ }
+ }
+
+ public static PendingIntent getPendingIntent(Context context, String text) {
+ Intent toastIntent = new Intent(context, ToastService.class);
+ toastIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ toastIntent.setAction(ACTION_TOAST + ":" + text); // one per toast message
+ toastIntent.putExtra("text", text);
+ PendingIntent pi = PendingIntent.getService(
+ context, 58, toastIntent, PendingIntent.FLAG_UPDATE_CURRENT);
+ return pi;
+ }
+ }
+}
diff --git a/tests/functional/notificationtests/src/com/android/notification/functional/NotificationInlineReplyTests.java b/tests/functional/notificationtests/src/com/android/notification/functional/NotificationInlineReplyTests.java
new file mode 100644
index 0000000..84f3c49
--- /dev/null
+++ b/tests/functional/notificationtests/src/com/android/notification/functional/NotificationInlineReplyTests.java
@@ -0,0 +1,103 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.notification.functional;
+
+import android.app.NotificationManager;
+import android.content.Context;
+import android.os.RemoteException;
+import android.support.test.uiautomator.By;
+import android.support.test.uiautomator.UiDevice;
+import android.support.test.uiautomator.UiObject2;
+import android.support.test.uiautomator.Until;
+import android.test.InstrumentationTestCase;
+import android.test.suitebuilder.annotation.MediumTest;
+import android.view.inputmethod.InputMethodManager;
+
+public class NotificationInlineReplyTests extends InstrumentationTestCase {
+ private static final int SHORT_TIMEOUT = 200;
+ private static final int LONG_TIMEOUT = 2000;
+ private static final int NOTIFICATION_ID = 1;
+ private NotificationManager mNotificationManager;
+ private UiDevice mDevice = null;
+ private Context mContext;
+ private NotificationHelper mHelper;
+
+ @Override
+ public void setUp() throws Exception {
+ super.setUp();
+ mDevice = UiDevice.getInstance(getInstrumentation());
+ mContext = getInstrumentation().getContext();
+ mNotificationManager = (NotificationManager) mContext
+ .getSystemService(Context.NOTIFICATION_SERVICE);
+ mHelper = new NotificationHelper(mDevice, getInstrumentation(), mNotificationManager);
+ try {
+ mDevice.setOrientationNatural();
+ } catch (RemoteException e) {
+ throw new RuntimeException("failed to freeze device orientaion", e);
+ }
+ mDevice.pressHome();
+ mNotificationManager.cancelAll();
+ }
+
+ @Override
+ public void tearDown() throws Exception {
+ mNotificationManager.cancelAll();
+ mDevice.pressHome();
+ super.tearDown();
+ }
+
+ @MediumTest
+ public void testInLineNotificationWithLockScreen() throws Exception {
+ try {
+ mHelper.sendNotificationsWithInLineReply(NOTIFICATION_ID,false);
+ mHelper.sleepAndWakeUpDevice();
+ UiObject2 obj = mDevice.wait(Until.findObject(By.text("INLINE REPLY TEST")),
+ LONG_TIMEOUT);
+ mDevice.swipe(obj.getVisibleBounds().centerX(), obj.getVisibleBounds().centerY(),
+ obj.getVisibleBounds().centerX(),
+ mDevice.getDisplayHeight(), 5);
+ mDevice.wait(Until.findObject(By.text("REPLY")), LONG_TIMEOUT).click();
+ UiObject2 replyBox = mDevice.wait(
+ Until.findObject(By.res("com.android.systemui:id/remote_input_send")),
+ LONG_TIMEOUT);
+ InputMethodManager imm = (InputMethodManager) mContext
+ .getSystemService(Context.INPUT_METHOD_SERVICE);
+ if (!imm.isAcceptingText()) {
+ assertNotNull("Keyboard for inline reply has not loaded correctly", replyBox);
+ }
+ } finally {
+ mHelper.swipeUp();
+ }
+ }
+
+ @MediumTest
+ public void testInLineNotificationsWithQuickSetting() throws Exception {
+ mHelper.sendNotificationsWithInLineReply(NOTIFICATION_ID,false);
+ Thread.sleep(SHORT_TIMEOUT);
+ mDevice.openQuickSettings();
+ mDevice.openNotification();
+ mDevice.wait(Until.findObject(By.text("REPLY")), LONG_TIMEOUT).click();
+ UiObject2 replyBox = mDevice.wait(
+ Until.findObject(By.res("com.android.systemui:id/remote_input_send")),
+ LONG_TIMEOUT);
+ InputMethodManager imm = (InputMethodManager) mContext
+ .getSystemService(Context.INPUT_METHOD_SERVICE);
+ if (!imm.isAcceptingText()) {
+ assertNotNull("Keyboard for inline reply has not loaded correctly", replyBox);
+ }
+ }
+}
diff --git a/tests/functional/notificationtests/src/com/android/notification/functional/NotificationInteractionTests.java b/tests/functional/notificationtests/src/com/android/notification/functional/NotificationInteractionTests.java
new file mode 100644
index 0000000..83aae52
--- /dev/null
+++ b/tests/functional/notificationtests/src/com/android/notification/functional/NotificationInteractionTests.java
@@ -0,0 +1,123 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.notification.functional;
+
+import android.app.NotificationManager;
+import android.content.Context;
+import android.service.notification.StatusBarNotification;
+import android.support.test.uiautomator.By;
+import android.support.test.uiautomator.Direction;
+import android.support.test.uiautomator.UiDevice;
+import android.support.test.uiautomator.UiObject2;
+import android.support.test.uiautomator.Until;
+import android.test.InstrumentationTestCase;
+import android.test.suitebuilder.annotation.MediumTest;
+import android.util.Log;
+
+import java.util.HashMap;
+import java.util.Map;
+
+public class NotificationInteractionTests extends InstrumentationTestCase {
+ private static final String LOG_TAG = NotificationInteractionTests.class.getSimpleName();
+ private static final int LONG_TIMEOUT = 2000;
+ private final boolean DEBUG = false;
+ private NotificationManager mNotificationManager;
+ private UiDevice mDevice = null;
+ private Context mContext;
+ private NotificationHelper mHelper;
+ private static final int CUSTOM_NOTIFICATION_ID = 1;
+ private static final int NOTIFICATIONS_COUNT = 3;
+
+ @Override
+ public void setUp() throws Exception {
+ super.setUp();
+ mDevice = UiDevice.getInstance(getInstrumentation());
+ mContext = getInstrumentation().getContext();
+ mNotificationManager = (NotificationManager) mContext
+ .getSystemService(Context.NOTIFICATION_SERVICE);
+ mHelper = new NotificationHelper(mDevice, getInstrumentation(), mNotificationManager);
+ mDevice.setOrientationNatural();
+ mNotificationManager.cancelAll();
+ }
+
+ @Override
+ public void tearDown() throws Exception {
+ super.tearDown();
+ mDevice.unfreezeRotation();
+ mDevice.pressHome();
+ mNotificationManager.cancelAll();
+ }
+
+ @MediumTest
+ public void testNonDismissNotification() throws Exception {
+ String text = "USB debugging connected";
+ mDevice.openNotification();
+ Thread.sleep(LONG_TIMEOUT);
+ UiObject2 obj = findByText(text);
+ assertNotNull(String.format("Couldn't find %s notification", text), obj);
+ obj.swipe(Direction.LEFT, 1.0f);
+ Thread.sleep(LONG_TIMEOUT);
+ obj = mDevice.wait(Until.findObject(By.text(text)),
+ LONG_TIMEOUT);
+ assertNotNull("USB debugging notification has been dismissed", obj);
+ }
+
+ /** send out multiple notifications in order to test CLEAR ALL function */
+ @MediumTest
+ public void testDismissAll() throws Exception {
+ String text = "CLEAR ALL";
+ Map<Integer, String> lists = new HashMap<Integer, String>();
+ StatusBarNotification[] sbns = mNotificationManager.getActiveNotifications();
+ int currentSbns = sbns.length;
+ for (int i = 0; i < NOTIFICATIONS_COUNT; i++) {
+ lists.put(CUSTOM_NOTIFICATION_ID + i, Integer.toString(CUSTOM_NOTIFICATION_ID + i));
+ }
+ mHelper.sendNotifications(lists);
+ if (DEBUG) {
+ Log.d(LOG_TAG,
+ String.format("posted %s notifications, here they are: ", NOTIFICATIONS_COUNT));
+ sbns = mNotificationManager.getActiveNotifications();
+ for (StatusBarNotification sbn : sbns) {
+ Log.d(LOG_TAG, " " + sbn);
+ }
+ }
+ if (mDevice.openNotification()) {
+ Thread.sleep(LONG_TIMEOUT);
+ UiObject2 clearAll = findByText(text);
+ clearAll.click();
+ }
+ Thread.sleep(LONG_TIMEOUT);
+ sbns = mNotificationManager.getActiveNotifications();
+ assertTrue(String.format("%s notifications have not been cleared", sbns.length),
+ sbns.length == currentSbns);
+ }
+
+ private UiObject2 findByText(String text) throws Exception {
+ int maxAttempt = 5;
+ UiObject2 item = null;
+ while (maxAttempt-- > 0) {
+ item = mDevice.wait(Until.findObject(By.text(text)), LONG_TIMEOUT);
+ if (item == null) {
+ mDevice.swipe(mDevice.getDisplayWidth() / 2, mDevice.getDisplayHeight() / 2,
+ mDevice.getDisplayWidth() / 2, 0, 30);
+ } else {
+ return item;
+ }
+ }
+ return null;
+ }
+}
diff --git a/tests/functional/notificationtests/src/com/android/notification/functional/NotificationSecurityLargeTests.java b/tests/functional/notificationtests/src/com/android/notification/functional/NotificationSecurityLargeTests.java
new file mode 100644
index 0000000..f9a36b0
--- /dev/null
+++ b/tests/functional/notificationtests/src/com/android/notification/functional/NotificationSecurityLargeTests.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.notification.functional;
+
+import android.app.Notification;
+import android.app.NotificationManager;
+import android.content.Context;
+import android.support.test.uiautomator.UiDevice;
+import android.support.test.uiautomator.By;
+import android.support.test.uiautomator.Until;
+import android.test.InstrumentationTestCase;
+import android.test.suitebuilder.annotation.LargeTest;
+import android.util.Log;
+
+public class NotificationSecurityLargeTests extends InstrumentationTestCase {
+
+ private static final String LOG_TAG = NotificationSecurityLargeTests.class.getSimpleName();
+ private static final int SHORT_TIMEOUT = 200;
+ private static final int NOTIFICATION_ID_SECRET = 1;
+ private static final int NOTIFICATION_ID_PUBLIC = 2;
+ private static final int PIN = 1234;
+ private NotificationManager mNotificationManager = null;
+ private UiDevice mDevice = null;
+ private Context mContext;
+ private NotificationHelper mHelper;
+
+ @Override
+ public void setUp() throws Exception {
+ super.setUp();
+ mDevice = UiDevice.getInstance(getInstrumentation());
+ mContext = getInstrumentation().getContext();
+ mNotificationManager = (NotificationManager) mContext
+ .getSystemService(Context.NOTIFICATION_SERVICE);
+ Log.v(LOG_TAG, "set up notification...");
+ mHelper = new NotificationHelper(mDevice, getInstrumentation(), mNotificationManager);
+ mHelper.setScreenLockPin(PIN);
+ mHelper.sleepAndWakeUpDevice();
+ }
+
+ @Override
+ public void tearDown() throws Exception {
+ mHelper.swipeUp();
+ Thread.sleep(SHORT_TIMEOUT);
+ mHelper.unlockScreenByPin(PIN);
+ mHelper.removeScreenLock(PIN, "Swipe");
+ mNotificationManager.cancelAll();
+ super.tearDown();
+ }
+
+ @LargeTest
+ public void testVisibilitySecret() throws Exception {
+ String title = "Secret Title";
+ Log.i(LOG_TAG, "Begin test visibility equals VISIBILITY_SECRET ");
+ mHelper.sendNotification(NOTIFICATION_ID_SECRET, Notification.VISIBILITY_SECRET, title);
+ if (!mHelper.checkNotificationExistence(NOTIFICATION_ID_SECRET, true)) {
+ fail("couldn't find posted notification id=" + NOTIFICATION_ID_PUBLIC);
+ }
+ assertFalse(mDevice.wait(Until.hasObject(By.res("android:id/title").text(title)),
+ SHORT_TIMEOUT));
+ }
+
+ @LargeTest
+ public void testVisibilityPrivate() throws Exception {
+ Log.i(LOG_TAG, "Begin test visibility equals VISIBILITY_PRIVATE ");
+ mHelper.sendNotification(NOTIFICATION_ID_PUBLIC, Notification.VISIBILITY_PRIVATE, "");
+ if (!mHelper.checkNotificationExistence(NOTIFICATION_ID_PUBLIC, true)) {
+ fail("couldn't find posted notification id=" + NOTIFICATION_ID_PUBLIC);
+ }
+ assertNotNull(
+ Until.findObject(By.res("android:id/title").text("Contents hidden")));
+ }
+}
diff --git a/tests/functional/notificationtests/src/com/android/notification/functional/NotificationSecurityTests.java b/tests/functional/notificationtests/src/com/android/notification/functional/NotificationSecurityTests.java
new file mode 100644
index 0000000..80cdfd2
--- /dev/null
+++ b/tests/functional/notificationtests/src/com/android/notification/functional/NotificationSecurityTests.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.notification.functional;
+
+import android.app.Notification;
+import android.app.NotificationManager;
+import android.content.Context;
+import android.service.notification.StatusBarNotification;
+import android.support.test.uiautomator.UiDevice;
+import android.test.InstrumentationTestCase;
+import android.test.suitebuilder.annotation.MediumTest;
+import android.util.Log;
+
+public class NotificationSecurityTests extends InstrumentationTestCase {
+
+ private static final String LOG_TAG = NotificationSecurityTests.class.getSimpleName();
+ private static final int NOTIFICATION_ID_PUBLIC = 1;
+ private NotificationManager mNotificationManager = null;
+ private UiDevice mDevice = null;
+ private Context mContext;
+ private NotificationHelper mHelper;
+
+ @Override
+ public void setUp() throws Exception {
+ super.setUp();
+ mDevice = UiDevice.getInstance(getInstrumentation());
+ mContext = getInstrumentation().getContext();
+ mNotificationManager = (NotificationManager) mContext
+ .getSystemService(Context.NOTIFICATION_SERVICE);
+ Log.i(LOG_TAG, "set up notification...");
+ mHelper = new NotificationHelper(mDevice, getInstrumentation(), mNotificationManager);
+ mDevice.freezeRotation();
+ mHelper.sleepAndWakeUpDevice();
+ }
+
+ @Override
+ public void tearDown() throws Exception {
+ mNotificationManager.cancelAll();
+ mHelper.swipeUp();
+ super.tearDown();
+ }
+
+ @MediumTest
+ public void testVisibilityPublic() throws Exception {
+ mHelper.enableNotificationViaAdb(true);
+ String title = "Public Notification";
+ Log.i(LOG_TAG, "Begin test visibility equals VISIBILITY_PUBLIC ");
+ mHelper.sendNotification(NOTIFICATION_ID_PUBLIC, Notification.VISIBILITY_PUBLIC, title);
+ if (mHelper.checkNotificationExistence(NOTIFICATION_ID_PUBLIC, false)) {
+ fail("couldn't find posted notification id=" + NOTIFICATION_ID_PUBLIC);
+ }
+ StatusBarNotification[] sbns = mNotificationManager.getActiveNotifications();
+ for (StatusBarNotification sbn : sbns) {
+ Log.i(LOG_TAG, sbn.getNotification().extras.getString(Notification.EXTRA_TITLE));
+ if (sbn.getId() == NOTIFICATION_ID_PUBLIC) {
+ String sentTitle = sbn.getNotification().extras.getString(Notification.EXTRA_TITLE);
+ assertEquals(sentTitle, title);
+ }
+ }
+ }
+}
diff --git a/tests/functional/otatests/Android.mk b/tests/functional/otatests/Android.mk
new file mode 100644
index 0000000..110ea6a
--- /dev/null
+++ b/tests/functional/otatests/Android.mk
@@ -0,0 +1,14 @@
+
+LOCAL_PATH := $(call my-dir)
+
+include $(CLEAR_VARS)
+
+LOCAL_PACKAGE_NAME := OtaFunctionalTests
+LOCAL_SRC_FILES := $(call all-java-files-under, src)
+LOCAL_MODULE_TAGS := tests
+LOCAL_STATIC_JAVA_LIBRARIES := \
+ easymocklib \
+ objenesis-target \
+ ub-uiautomator
+
+include $(BUILD_PACKAGE)
diff --git a/tests/functional/otatests/AndroidManifest.xml b/tests/functional/otatests/AndroidManifest.xml
new file mode 100644
index 0000000..54cccad
--- /dev/null
+++ b/tests/functional/otatests/AndroidManifest.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.android.functional.otatests">
+ <application>
+ <uses-library android:name="android.test.runner" />
+ </application>
+
+ <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
+
+ <uses-sdk android:minSdkVersion="19"
+ android:targetSdkVersion="24" />
+ <instrumentation
+ android:name="android.test.InstrumentationTestRunner"
+ android:targetPackage="com.android.functional.otatests"
+ android:label="Android System Update Functional Tests" />
+</manifest>
diff --git a/tests/functional/otatests/src/com/android/functional/otatests/PackageProcessTest.java b/tests/functional/otatests/src/com/android/functional/otatests/PackageProcessTest.java
new file mode 100644
index 0000000..cf74375
--- /dev/null
+++ b/tests/functional/otatests/src/com/android/functional/otatests/PackageProcessTest.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.functional.otatests;
+
+import static org.easymock.EasyMock.createNiceMock;
+
+import android.content.Context;
+import android.content.ContextWrapper;
+import android.os.IPowerManager;
+import android.os.PowerManager;
+import android.os.RecoverySystem;
+import android.test.InstrumentationTestCase;
+
+import java.io.File;
+
+public class PackageProcessTest extends InstrumentationTestCase {
+
+ private static final String PACKAGE_DATA_PATH =
+ "/data/data/com.google.android.gms/app_download/update.zip";
+ private static final String BLOCK_MAP = "/cache/recovery/block.map";
+ private static final String UNCRYPT_FILE = "/cache/recovery/uncrypt_file";
+
+ private Context mMockContext;
+ private Context mContext;
+ private PowerManager mMockPowerManager;
+
+ private class PackageProcessMockContext extends ContextWrapper {
+
+ private Context mInternal;
+
+ public PackageProcessMockContext(Context base) {
+ super(base);
+ mInternal = base;
+ }
+
+ @Override
+ public Object getSystemService(String name) {
+ if (name.equals(Context.POWER_SERVICE)) {
+ return mMockPowerManager;
+ }
+ return mInternal.getSystemService(name);
+ }
+ }
+
+ @Override
+ public void setUp() throws Exception {
+ super.setUp();
+ mContext = getInstrumentation().getContext();
+ mMockContext = new PackageProcessMockContext(mContext);
+ // Set a mocked out power manager into the mocked context, so the device
+ // won't reboot at the end of installPackage
+ IPowerManager mockIPowerManager = createNiceMock(IPowerManager.class);
+ mMockPowerManager = new PowerManager(mContext, mockIPowerManager, null);
+ }
+
+ public void testPackageProcessOnly() throws Exception {
+ File pkg = new File(PACKAGE_DATA_PATH);
+ RecoverySystem.verifyPackage(pkg, null, null);
+ RecoverySystem.processPackage(mMockContext, pkg, null);
+ // uncrypt will push block.map to this location if and only if it finishes successfully
+ assertTrue(new File(BLOCK_MAP).exists());
+ }
+
+ public void testPackageProcessViaInstall() throws Exception {
+ File pkg = new File(PACKAGE_DATA_PATH);
+ RecoverySystem.verifyPackage(pkg, null, null);
+ RecoverySystem.installPackage(mMockContext, pkg);
+ // uncrypt will push block.map to this location if and only if it finishes successfully
+ assertTrue(new File(UNCRYPT_FILE).exists());
+ }
+}
diff --git a/tests/functional/otatests/src/com/android/functional/otatests/SystemUpdateTest.java b/tests/functional/otatests/src/com/android/functional/otatests/SystemUpdateTest.java
new file mode 100644
index 0000000..065ccb1
--- /dev/null
+++ b/tests/functional/otatests/src/com/android/functional/otatests/SystemUpdateTest.java
@@ -0,0 +1,11 @@
+package com.android.functional.otatests;
+
+/**
+ * A basic test case to assert that the system was updated to the expected version.
+ */
+public class SystemUpdateTest extends VersionCheckingTest {
+
+ public void testIsUpdated() throws Exception {
+ assertUpdated();
+ }
+}
diff --git a/tests/functional/otatests/src/com/android/functional/otatests/VersionCheckingTest.java b/tests/functional/otatests/src/com/android/functional/otatests/VersionCheckingTest.java
new file mode 100644
index 0000000..174786c
--- /dev/null
+++ b/tests/functional/otatests/src/com/android/functional/otatests/VersionCheckingTest.java
@@ -0,0 +1,57 @@
+package com.android.functional.otatests;
+
+import android.test.InstrumentationTestCase;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStreamReader;
+
+public class VersionCheckingTest extends InstrumentationTestCase {
+
+ protected static final String OLD_VERSION = "/sdcard/otatest/version.old";
+ protected static final String NEW_VERSION = "/sdcard/otatest/version.new";
+ protected static final String KEY_BUILD_ID = "ro.build.version.incremental";
+ protected static final String KEY_BOOTLOADER = "ro.bootloader";
+ protected static final String KEY_BASEBAND = "ro.build.expect.baseband";
+ protected static final String KEY_BASEBAND_GSM = "gsm.version.baseband";
+
+ protected VersionInfo mOldVersion;
+ protected VersionInfo mNewVersion;
+
+ @Override
+ public void setUp() throws Exception {
+ try {
+ mOldVersion = VersionInfo.parseFromFile(OLD_VERSION);
+ mNewVersion = VersionInfo.parseFromFile(NEW_VERSION);
+ } catch (IOException e) {
+ throw new RuntimeException(
+ "Couldn't find version file; was this test run with VersionCachePreparer?", e);
+ }
+ }
+
+ protected void assertNotUpdated() throws IOException {
+ assertEquals(mOldVersion.getBuildId(), getProp(KEY_BUILD_ID));
+ assertEquals(mOldVersion.getBootloaderVersion(), getProp(KEY_BOOTLOADER));
+ assertTrue(mOldVersion.getBasebandVersion().equals(getProp(KEY_BASEBAND))
+ || mOldVersion.getBasebandVersion().equals(getProp(KEY_BASEBAND_GSM)));
+ }
+
+ protected void assertUpdated() throws IOException {
+ assertEquals(mNewVersion.getBuildId(), getProp(KEY_BUILD_ID));
+ assertEquals(mNewVersion.getBootloaderVersion(), getProp(KEY_BOOTLOADER));
+ // Due to legacy property names (an old meaning to gsm.version.baseband),
+ // the KEY_BASEBAND and KEY_BASEBAND_GSM properties may not match each other.
+ // At least one of them will always match the baseband version recorded by
+ // NEW_VERSION.
+ assertTrue(mNewVersion.getBasebandVersion().equals(getProp(KEY_BASEBAND))
+ || mNewVersion.getBasebandVersion().equals(getProp(KEY_BASEBAND_GSM)));
+ }
+
+ private String getProp(String key) throws IOException {
+ Process p = Runtime.getRuntime().exec("getprop " + key);
+ BufferedReader r = new BufferedReader(new InputStreamReader(p.getInputStream()));
+ String ret = r.readLine().trim();
+ r.close();
+ return ret;
+ }
+}
diff --git a/tests/functional/otatests/src/com/android/functional/otatests/VersionInfo.java b/tests/functional/otatests/src/com/android/functional/otatests/VersionInfo.java
new file mode 100644
index 0000000..448464f
--- /dev/null
+++ b/tests/functional/otatests/src/com/android/functional/otatests/VersionInfo.java
@@ -0,0 +1,48 @@
+package com.android.functional.otatests;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStreamReader;
+
+public class VersionInfo {
+ private final String mBuildId;
+ private final String mBootloaderVersion;
+ private final String mBasebandVersion;
+
+ private VersionInfo(String buildId, String bootVersion, String radioVersion) {
+ mBuildId = buildId;
+ mBootloaderVersion = bootVersion;
+ mBasebandVersion = radioVersion;
+ }
+
+ public String getBuildId() {
+ return mBuildId;
+ }
+
+ public String getBootloaderVersion() {
+ return mBootloaderVersion;
+ }
+
+ public String getBasebandVersion() {
+ return mBasebandVersion;
+ }
+
+ public static VersionInfo parseFromFile(String fileName) throws IOException {
+ BufferedReader r = new BufferedReader(
+ new InputStreamReader(new FileInputStream(new File(fileName))));
+ try {
+ return new VersionInfo(
+ denull(r.readLine()),
+ denull(r.readLine()),
+ denull(r.readLine()));
+ } finally {
+ r.close();
+ }
+ }
+
+ private static String denull(String s) {
+ return s == null || s.equals("null") ? "" : s;
+ }
+}
diff --git a/tests/functional/permission/Android.mk b/tests/functional/permission/Android.mk
new file mode 100644
index 0000000..0ecdeb2
--- /dev/null
+++ b/tests/functional/permission/Android.mk
@@ -0,0 +1,15 @@
+LOCAL_PATH := $(call my-dir)
+include $(CLEAR_VARS)
+
+LOCAL_MODULE_TAGS := tests
+
+LOCAL_SDK_VERSION := current
+
+LOCAL_SRC_FILES := $(call all-subdir-java-files)
+LOCAL_JAVA_LIBRARIES := android.test.runner
+LOCAL_STATIC_JAVA_LIBRARIES := ub-uiautomator launcher-helper-lib
+
+LOCAL_PACKAGE_NAME := PermissionFunctionalTests
+LOCAL_CERTIFICATE := platform
+
+include $(BUILD_PACKAGE)
diff --git a/tests/functional/permission/AndroidManifest.xml b/tests/functional/permission/AndroidManifest.xml
new file mode 100644
index 0000000..b13b888
--- /dev/null
+++ b/tests/functional/permission/AndroidManifest.xml
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Copyright 2016 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.android.functional.permissiontests">
+
+ <uses-sdk android:minSdkVersion="23"
+ android:targetSdkVersion="23" />
+ <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
+ <uses-permission android:name="android.permission.WRITE_CONTACTS" />
+ <uses-permission android:name="android.permission.SEND_SMS" />
+ <uses-permission android:name="android.permission.RECEIVE_SMS" />
+ <application>
+ <uses-library android:name="android.test.runner"/>
+ </application>
+ <instrumentation
+ android:name="android.test.InstrumentationTestRunner"
+ android:targetPackage="com.android.functional.permissiontests"
+ android:label="Permission Functional Tests" />
+</manifest>
diff --git a/tests/functional/permission/src/com/android/functional/permissiontests/GenericAppPermissionTests.java b/tests/functional/permission/src/com/android/functional/permissiontests/GenericAppPermissionTests.java
new file mode 100644
index 0000000..1923e3c
--- /dev/null
+++ b/tests/functional/permission/src/com/android/functional/permissiontests/GenericAppPermissionTests.java
@@ -0,0 +1,139 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.functional.permissiontests;
+
+import android.app.UiAutomation;
+import android.content.Context;
+import android.os.RemoteException;
+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.Until;
+import android.test.InstrumentationTestCase;
+import android.test.suitebuilder.annotation.MediumTest;
+import android.test.suitebuilder.annotation.SmallTest;
+
+import com.android.functional.permissiontests.PermissionHelper.PermissionOp;
+import com.android.functional.permissiontests.PermissionHelper.PermissionStatus;
+
+import java.util.Arrays;
+import java.util.List;
+
+public class GenericAppPermissionTests extends InstrumentationTestCase {
+ protected final String PACKAGE_INSTALLER = "com.android.packageinstaller";
+ protected final String TARGET_APP_PKG = "com.android.functional.permissiontests";
+ private final String PERMISSION_TEST_APP_PKG = "com.android.permissiontestappmv1";
+ private final String PERMISSION_TEST_APP = "PermissionTestAppMV1";
+ private UiDevice mDevice = null;
+ private Context mContext = null;
+ private UiAutomation mUiAutomation = null;
+ private PermissionHelper pHelper;
+ private final String[] mDefaultPermittedGroups = new String[] {
+ "CONTACTS", "SMS", "STORAGE"
+ };
+
+ private final String[] mDefaultPermittedDangerousPermissions = new String[] {
+ "android.permission.WRITE_EXTERNAL_STORAGE", "android.permission.WRITE_CONTACTS",
+ "android.permission.SEND_SMS", "android.permission.RECEIVE_SMS"
+ };
+
+ private List<String> mDefaultGrantedPermissions;
+
+ @Override
+ protected void setUp() throws Exception {
+ super.setUp();
+ mDevice = UiDevice.getInstance(getInstrumentation());
+ mContext = getInstrumentation().getContext();
+ mUiAutomation = getInstrumentation().getUiAutomation();
+ mDevice.setOrientationNatural();
+ pHelper = PermissionHelper.getInstance(mDevice, mContext, mUiAutomation);
+ mDefaultGrantedPermissions = pHelper.getPermissionByPackage(TARGET_APP_PKG, Boolean.TRUE);
+ }
+
+ @SmallTest
+ public void testDefaultDangerousPermissionGranted() {
+ pHelper.verifyDefaultDangerousPermissionGranted(TARGET_APP_PKG,
+ mDefaultPermittedDangerousPermissions, Boolean.FALSE);
+ }
+
+ @SmallTest
+ public void testExtraDangerousPermissionNotGranted() {
+ pHelper.verifyExtraDangerousPermissionNotGranted(TARGET_APP_PKG, mDefaultPermittedGroups);
+ }
+
+ @SmallTest
+ public void testNormalPermissionsAutoGranted() {
+ pHelper.verifyNormalPermissionsAutoGranted(TARGET_APP_PKG);
+ }
+
+ public void testToggleAppPermisssionOFF() {
+ pHelper.togglePermissionSetting(PERMISSION_TEST_APP, "Contacts", Boolean.FALSE);
+ pHelper.verifyPermissionSettingStatus(
+ PERMISSION_TEST_APP, "Contacts", PermissionStatus.OFF);
+ }
+
+ public void testToggleAppPermisssionON() {
+ pHelper.togglePermissionSetting(PERMISSION_TEST_APP, "Contacts", Boolean.TRUE);
+ pHelper.verifyPermissionSettingStatus(PERMISSION_TEST_APP, "Contacts", PermissionStatus.ON);
+ }
+
+ @MediumTest
+ public void testViewPermissionDescription() {
+ List<String> permissionDescGrpNamesList = pHelper
+ .getPermissionDescGroupNames(TARGET_APP_PKG);
+ permissionDescGrpNamesList.removeAll(Arrays.asList(mDefaultPermittedGroups));
+ assertTrue("Still there are more", permissionDescGrpNamesList.isEmpty());
+ }
+
+ public void testPermissionDialogAllow() {
+ pHelper.cleanPackage(PERMISSION_TEST_APP_PKG);
+ pHelper.launchApp(PERMISSION_TEST_APP_PKG, PERMISSION_TEST_APP);
+ mDevice.wait(Until.findObject(By.text("GET CONTACT PERMISSION")), pHelper.TIMEOUT).click();
+ mDevice.wait(Until.findObject(
+ By.res(PACKAGE_INSTALLER, "permission_allow_button")), pHelper.TIMEOUT).click();
+ assertTrue("Permission hasn't been granted",
+ pHelper.getAllDangerousPermissionsByPermGrpNames(
+ new String[] {"Contacts"} ).size() > 0);
+ }
+
+ public void testPermissionDialogDenyFlow() {
+ pHelper.cleanPackage(PERMISSION_TEST_APP_PKG);
+ pHelper.launchApp(PERMISSION_TEST_APP_PKG, PERMISSION_TEST_APP);
+ pHelper.grantOrRevokePermissionViaAdb(
+ PERMISSION_TEST_APP_PKG, "android.permission.READ_CONTACTS", PermissionOp.REVOKE);
+ BySelector getContactSelector = By.text("GET CONTACT PERMISSION");
+ BySelector dontAskChkSelector = By.res(PACKAGE_INSTALLER, "do_not_ask_checkbox");
+ BySelector denySelctor = By.res(PACKAGE_INSTALLER, "permission_deny_button");
+
+ mDevice.wait(Until.findObject(getContactSelector), pHelper.TIMEOUT).click();
+ UiObject2 dontAskChk = mDevice.wait(Until.findObject(dontAskChkSelector), pHelper.TIMEOUT);
+ assertNull(dontAskChk);
+ mDevice.wait(Until.findObject(denySelctor), pHelper.TIMEOUT).click();
+ mDevice.wait(Until.findObject(getContactSelector), pHelper.TIMEOUT).click();
+ mDevice.wait(Until.findObject(dontAskChkSelector), pHelper.TIMEOUT).click();
+ mDevice.wait(Until.findObject(denySelctor), pHelper.TIMEOUT).click();
+ mDevice.wait(Until.findObject(getContactSelector), pHelper.TIMEOUT).click();;
+ assertNull(mDevice.wait(Until.findObject(denySelctor), pHelper.TIMEOUT));
+ }
+
+ @Override
+ protected void tearDown() throws Exception {
+ mDevice.unfreezeRotation();
+ super.tearDown();
+ }
+}
diff --git a/tests/functional/permission/src/com/android/functional/permissiontests/PermissionHelper.java b/tests/functional/permission/src/com/android/functional/permissiontests/PermissionHelper.java
new file mode 100644
index 0000000..d6edd8a
--- /dev/null
+++ b/tests/functional/permission/src/com/android/functional/permissiontests/PermissionHelper.java
@@ -0,0 +1,430 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.functional.permissiontests;
+
+import android.app.UiAutomation;
+import android.content.Context;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.os.ParcelFileDescriptor;
+import android.os.SystemClock;
+import android.support.test.launcherhelper.ILauncherStrategy;
+import android.support.test.launcherhelper.LauncherStrategyFactory;
+import android.support.test.uiautomator.By;
+import android.support.test.uiautomator.Direction;
+import android.support.test.uiautomator.UiDevice;
+import android.support.test.uiautomator.UiObject2;
+import android.support.test.uiautomator.Until;
+import android.util.Log;
+
+import junit.framework.Assert;
+
+import java.io.BufferedReader;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Hashtable;
+import java.util.List;
+
+public class PermissionHelper {
+ public static final String TEST_TAG = "PermissionTest";
+ public static final String SETTINGS_PACKAGE = "com.android.settings";
+ public final int TIMEOUT = 500;
+ public static PermissionHelper mInstance = null;
+ private UiDevice mDevice;
+ private Context mContext;
+ private static UiAutomation mUiAutomation;
+ public static Hashtable<String, List<String>> mPermissionGroupInfo = null;
+ ILauncherStrategy mLauncherStrategy;
+
+ private PermissionHelper(UiDevice device, Context context, UiAutomation uiAutomation) {
+ mDevice = device;
+ mContext = context;
+ mUiAutomation = uiAutomation;
+ mLauncherStrategy = LauncherStrategyFactory.getInstance(mDevice).getLauncherStrategy();
+ }
+
+ public static PermissionHelper getInstance(UiDevice device, Context context,
+ UiAutomation uiAutomation) {
+ if (mInstance == null) {
+ mInstance = new PermissionHelper(device, context, uiAutomation);
+ PermissionHelper.populateDangerousPermissionGroupInfo();
+ }
+ return mInstance;
+ }
+
+ /**
+ * Returns list of all dangerous permission of the system
+ */
+ private static void populateDangerousPermissionGroupInfo() {
+ ParcelFileDescriptor pfd = mUiAutomation.executeShellCommand("pm list permissions -g -d");
+ try (BufferedReader reader = new BufferedReader(
+ new InputStreamReader(new FileInputStream(pfd.getFileDescriptor())))) {
+ String line;
+ List<String> permissions = new ArrayList<String>();
+ String groupName = null;
+ while ((line = reader.readLine()) != null) {
+ if (line.isEmpty()) {
+ // Do nothing
+ } else if (line.startsWith("group")) {
+ if (mPermissionGroupInfo == null) {
+ mPermissionGroupInfo = new Hashtable<String, List<String>>();
+ } else {
+ mPermissionGroupInfo.put(groupName, permissions);
+ permissions = new ArrayList<String>();
+ }
+ groupName = line.split(":")[1];
+ } else if (line.startsWith(" permission:")) {
+ permissions.add(line.split(":")[1]);
+ }
+ }
+ mPermissionGroupInfo.put(groupName, permissions);
+ } catch (IOException e) {
+ Log.e(TEST_TAG, e.getMessage());
+ }
+ }
+
+ /**
+ * Returns list of permission asked by package
+ * @param packageName : PackageName for which permission list to be returned
+ * @param permitted : set 'true' for normal and default granted dangerous permissions, 'false'
+ * otherwise
+ * @return
+ */
+ public List<String> getPermissionByPackage(String packageName, Boolean permitted) {
+ List<String> selectedPermissions = new ArrayList<String>();
+ String[] requestedPermissions = null;
+ int[] requestedPermissionFlags = null;
+ PackageInfo packageInfo = null;
+ try {
+ packageInfo = getPackageManager().getPackageInfo(packageName,
+ PackageManager.GET_PERMISSIONS);
+ } catch (NameNotFoundException e) {
+ throw new RuntimeException(String.format("%s package isn't found", packageName));
+ }
+
+ requestedPermissions = packageInfo.requestedPermissions;
+ requestedPermissionFlags = packageInfo.requestedPermissionsFlags;
+ for (int i = 0; i < requestedPermissions.length; ++i) {
+ // requestedPermissionFlags 1 = Denied, 3 = Granted
+ if (permitted && requestedPermissionFlags[i] == 3) {
+ selectedPermissions.add(requestedPermissions[i]);
+ } else if (!permitted && requestedPermissionFlags[i] == 1) {
+ selectedPermissions.add(requestedPermissions[i]);
+ }
+ }
+ return selectedPermissions;
+ }
+
+ /**
+ * Verify any dangerous permission not mentioned in manifest aren't granted
+ * @param packageName
+ * @param permittedGroups
+ */
+ public void verifyExtraDangerousPermissionNotGranted(String packageName,
+ String[] permittedGroups) {
+ List<String> allPermittedDangerousPermsList = getAllDangerousPermissionsByPermGrpNames(
+ permittedGroups);
+ List<String> allPermissionsForPackageList = getPermissionByPackage(packageName,
+ Boolean.TRUE);
+ List<String> allPlatformDangerousPermissionList = getPlatformDangerousPermissionGroupNames();
+ allPermissionsForPackageList.retainAll(allPlatformDangerousPermissionList);
+ allPermissionsForPackageList.removeAll(allPermittedDangerousPermsList);
+ Assert.assertTrue(
+ String.format("For package %s some extra dangerous permissions have been granted",
+ packageName),
+ allPermissionsForPackageList.isEmpty());
+ }
+
+ /**
+ * Verify any dangerous permission mentioned in manifest that is not default for privileged app
+ * isn't granted. Example: Location permission for Camera app
+ * @param packageName
+ * @param notPermittedGroups
+ */
+ public void verifyNotPermittedDangerousPermissionDenied(String packageName,
+ String[] notPermittedGroups) {
+ List<String> allNotPermittedDangerousPermsList = getAllDangerousPermissionsByPermGrpNames(
+ notPermittedGroups);
+ List<String> allPermissionsForPackageList = getPermissionByPackage(packageName,
+ Boolean.TRUE);
+ int allNotPermittedDangerousPermsCount = allNotPermittedDangerousPermsList.size();
+ allNotPermittedDangerousPermsList.removeAll(allPermissionsForPackageList);
+ Assert.assertTrue(
+ String.format("For package %s not permissible dangerous permissions been granted",
+ packageName),
+ allNotPermittedDangerousPermsList.size() == allNotPermittedDangerousPermsCount);
+ }
+
+ /**
+ * Verify any normal permission mentioned in manifest is auto granted
+ * @param packageName
+ */
+ public void verifyNormalPermissionsAutoGranted(String packageName) {
+ List<String> allDeniedPermissionsForPackageList = getPermissionByPackage(packageName,
+ Boolean.FALSE);
+ List<String> allPlatformDangerousPermissionList = getPlatformDangerousPermissionGroupNames();
+ allDeniedPermissionsForPackageList.removeAll(allPlatformDangerousPermissionList);
+ if (!allDeniedPermissionsForPackageList.isEmpty()) {
+ for (int i = 0; i < allDeniedPermissionsForPackageList.size(); ++i) {
+ Log.d(TEST_TAG, String.format("%s should have been auto granted",
+ allDeniedPermissionsForPackageList.get(i)));
+ }
+ }
+ Assert.assertTrue(
+ String.format("For package %s few normal permission have been denied", packageName),
+ allDeniedPermissionsForPackageList.isEmpty());
+ }
+
+ /**
+ * Verifies via UI that a permission is set/unset for an app
+ * @param appName
+ * @param permission
+ * @param expected : 'ON' or 'OFF'
+ * @return
+ */
+ public Boolean verifyPermissionSettingStatus(String appName, String permission,
+ PermissionStatus expected) {
+ if (!expected.equals(PermissionStatus.ON) && !expected.equals(PermissionStatus.OFF)) {
+ throw new RuntimeException(String.format("%s isn't valid permission status", expected));
+ }
+ openAppPermissionView(appName);
+ UiObject2 permissionView = mDevice
+ .wait(Until.findObject(By.res("android:id/list_container")), TIMEOUT);
+ List<UiObject2> permissionsList = permissionView.getChildren().get(0).getChildren();
+ for (UiObject2 permDesc : permissionsList) {
+ if (permDesc.getChildren().get(1).getChildren().get(0).getText().equals(permission)) {
+ String status = permDesc.getChildren().get(2).getChildren().get(0).getText();
+ return status.equals(expected.toString().toUpperCase());
+ }
+ }
+ Assert.fail("Permission is not found");
+ return Boolean.FALSE;
+ }
+
+ /**
+ * Verify default dangerous permission mentioned in manifest for system privileged apps are auto
+ * permitted Example: Camera permission for Camera app
+ * @param packageName
+ * @param permittedGroups
+ */
+ public void verifyDefaultDangerousPermissionGranted(String packageName,
+ String[] permittedGroups,
+ Boolean byGroup) {
+ List<String> allPermittedDangerousPermsList = new ArrayList<String>();
+ if (byGroup) {
+ allPermittedDangerousPermsList = getAllDangerousPermissionsByPermGrpNames(
+ permittedGroups);
+ } else {
+ allPermittedDangerousPermsList.addAll(Arrays.asList(permittedGroups));
+ }
+
+ List<String> allPermissionsForPackageList = getPermissionByPackage(packageName,
+ Boolean.TRUE);
+ allPermittedDangerousPermsList.removeAll(allPermissionsForPackageList);
+ for (String permission : allPermittedDangerousPermsList) {
+ Log.d(TEST_TAG,
+ String.format("%s - > %s hasn't been granted yet", packageName, permission));
+ }
+ Assert.assertTrue(String.format("For %s some Permissions aren't granted yet", packageName),
+ allPermittedDangerousPermsList.isEmpty());
+ }
+
+ /**
+ * For a given app, opens the permission settings window settings -> apps -> permissions
+ * @param appName
+ */
+ public void openAppPermissionView(String appName) {
+ mDevice.pressHome();
+ launchApp(SETTINGS_PACKAGE, "Settings");
+ UiObject2 app = null;
+ UiObject2 view = null;
+ int maxAttempt = 5;
+ while ((maxAttempt-- > 0)
+ && ((app = mDevice.wait(Until.findObject(By.res("android:id/title").text("Apps")),
+ TIMEOUT)) == null)) {
+ view = mDevice.wait(Until.findObject(By.res(SETTINGS_PACKAGE, "main_content")),
+ TIMEOUT);
+ // todo scroll may be different for device and build
+ view.scroll(Direction.DOWN, 1.0f);
+ }
+
+ mDevice.wait(Until.findObject(By.res("android:id/title").text("Apps")),
+ TIMEOUT)
+ .clickAndWait(Until.newWindow(), TIMEOUT);
+ app = null;
+ view = null;
+ maxAttempt = 10;
+ while ((maxAttempt-- > 0)
+ && ((app = mDevice.wait(Until.findObject(By.res("android:id/title").text(appName)),
+ TIMEOUT)) == null)) {
+ view = mDevice.wait(Until.findObject(By.res("com.android.settings:id/main_content")),
+ TIMEOUT);
+ // todo scroll may be different for device and build
+ view.scroll(Direction.DOWN, 1.0f);
+ }
+ app.clickAndWait(Until.newWindow(), TIMEOUT);
+ mDevice.wait(Until.findObject(By.res("android:id/title").text("Permissions")),
+ TIMEOUT).clickAndWait(Until.newWindow(), TIMEOUT);
+ }
+
+ /**
+ * Toggles permission for an app via UI
+ * @param appName
+ * @param permission
+ * @param toBeSet
+ */
+ public void togglePermissionSetting(String appName, String permission, Boolean toBeSet) {
+ openAppPermissionView(appName);
+ UiObject2 permissionView = mDevice
+ .wait(Until.findObject(By.res("android:id/list_container")), TIMEOUT);
+ List<UiObject2> permissionsList = permissionView.getChildren().get(0).getChildren();
+ for (UiObject2 obj : permissionsList) {
+ if (obj.getChildren().get(1).getChildren().get(0).getText().equals(permission)) {
+ String status = obj.getChildren().get(2).getChildren().get(0).getText();
+ if ((toBeSet && !status.equals(PermissionStatus.ON.toString()))
+ || (!toBeSet && status.equals(PermissionStatus.ON.toString()))) {
+ obj.getChildren().get(2).getChildren().get(0).click();
+ mDevice.waitForIdle();
+ }
+ break;
+ }
+ }
+ }
+
+ /**
+ * Grant or revoke permission via adb command
+ * @param packageName
+ * @param permissionName
+ * @param permissionOp : Accepted values are 'grant' and 'revoke'
+ */
+ public void grantOrRevokePermissionViaAdb(String packageName, String permissionName,
+ PermissionOp permissionOp) {
+ if (permissionOp == null) {
+ throw new RuntimeException("null operation can't be executed");
+ }
+ String command = String.format("pm %s %s %s", permissionOp.toString().toLowerCase(),
+ packageName, permissionName);
+ Log.d(TEST_TAG, String.format("executing - %s", command));
+ mUiAutomation.executeShellCommand(command);
+ mDevice.waitForIdle();
+ }
+
+ /**
+ * returns list of specific permissions in a dangerous permission group
+ * @param permissionGroupsToCheck
+ * @return
+ */
+ public List<String> getAllDangerousPermissionsByPermGrpNames(String[] permissionGroupsToCheck) {
+ List<String> allDangerousPermissions = new ArrayList<String>();
+ for (String s : permissionGroupsToCheck) {
+ String grpName = String.format("android.permission-group.%s", s.toUpperCase());
+ if (PermissionHelper.mPermissionGroupInfo.keySet().contains(grpName)) {
+ allDangerousPermissions.addAll(PermissionHelper.mPermissionGroupInfo.get(grpName));
+ }
+ }
+
+ return allDangerousPermissions;
+ }
+
+ /**
+ * Returns platform dangerous permission group names
+ * @return
+ */
+ public List<String> getPlatformDangerousPermissionGroupNames() {
+ List<String> allDangerousPermissions = new ArrayList<String>();
+ for (List<String> prmsList : PermissionHelper.mPermissionGroupInfo.values()) {
+ allDangerousPermissions.addAll(prmsList);
+ }
+ return allDangerousPermissions;
+ }
+
+ /**
+ * To ensure that all default dangerous permissions mentioned in manifest are granted for any
+ * privileged app
+ * @param packageName
+ * @param granted
+ * @param denied
+ */
+ public void ensureAppHasDefaultPermissions(String packageName, String[] granted,
+ String[] denied) {
+ List<String> defaultGranted = getAllDangerousPermissionsByPermGrpNames(granted);
+ List<String> currentGranted = getPermissionByPackage(packageName, Boolean.TRUE);
+ List<String> defaultDenied = getAllDangerousPermissionsByPermGrpNames(denied);
+ List<String> currentDenied = getPermissionByPackage(packageName, Boolean.FALSE);
+ defaultGranted.removeAll(currentGranted);
+ for (String permission : defaultGranted) {
+ grantOrRevokePermissionViaAdb(packageName, permission, PermissionOp.GRANT);
+ }
+ defaultDenied.removeAll(currentDenied);
+ for (String permission : defaultDenied) {
+ grantOrRevokePermissionViaAdb(packageName, permission, PermissionOp.REVOKE);
+ }
+ }
+
+ /**
+ * Get permission description via UI
+ * @param appName
+ * @return
+ */
+ public List<String> getPermissionDescGroupNames(String appName) {
+ List<String> groupNames = new ArrayList<String>();
+ openAppPermissionView(appName);
+ mDevice.wait(Until.findObject(By.desc("More options")), TIMEOUT).click();
+ mDevice.wait(Until.findObject(By.text("All permissions")), TIMEOUT).click();
+ UiObject2 permissionsListView = mDevice
+ .wait(Until.findObject(By.res("android:id/list_container")), TIMEOUT);
+ List<UiObject2> permissionList = permissionsListView
+ .findObjects(By.clazz("android.widget.TextView"));
+ for (UiObject2 obj : permissionList) {
+ if (obj.getText() != null && obj.getText() != "" && obj.getVisibleBounds().left == 0) {
+ if (obj.getText().equals("Other app capabilities"))
+ break;
+ groupNames.add(obj.getText().toUpperCase());
+ }
+ }
+ return groupNames;
+ }
+
+ public PackageManager getPackageManager() {
+ return mContext.getPackageManager();
+ }
+
+ public void launchApp(String packageName, String appName) {
+ if (!mDevice.hasObject(By.pkg(packageName).depth(0))) {
+ mLauncherStrategy.launch(appName, packageName);
+ }
+ }
+
+ public void cleanPackage(String packageName) {
+ mUiAutomation.executeShellCommand(String.format("pm clear %s", packageName));
+ SystemClock.sleep(2 * TIMEOUT);
+ }
+
+ /** Supported operations on permission */
+ public enum PermissionOp {
+ GRANT, REVOKE;
+ }
+
+ /** Available permission status */
+ public enum PermissionStatus {
+ ON, OFF;
+ }
+}
diff --git a/tests/functional/settingstests/Android.mk b/tests/functional/settingstests/Android.mk
new file mode 100644
index 0000000..4b373f5
--- /dev/null
+++ b/tests/functional/settingstests/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.
+
+LOCAL_PATH := $(call my-dir)
+include $(CLEAR_VARS)
+
+LOCAL_PACKAGE_NAME := SettingsFunctionalTests
+LOCAL_SRC_FILES := $(call all-java-files-under, src)
+LOCAL_MODULE_TAGS := tests
+LOCAL_STATIC_JAVA_LIBRARIES := \
+ base-app-helpers \
+ launcher-helper-lib \
+ services.core \
+ settings-app-helper \
+ timeresult-helper-lib \
+ ub-uiautomator
+
+#LOCAL_SDK_VERSION := current
+
+include $(BUILD_PACKAGE)
diff --git a/tests/functional/settingstests/AndroidManifest.xml b/tests/functional/settingstests/AndroidManifest.xml
new file mode 100644
index 0000000..cbd1bc1
--- /dev/null
+++ b/tests/functional/settingstests/AndroidManifest.xml
@@ -0,0 +1,36 @@
+<?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="android.settings.functional">
+
+ <application>
+ <uses-library android:name="android.test.runner" />
+ </application>
+
+ <uses-sdk android:minSdkVersion="19"
+ android:targetSdkVersion="24"/>
+
+ <uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
+ <uses-permission android:name="android.permission.WRITE_SECURE_SETTINGS" />
+ <uses-permission android:name="android.permission.BLUETOOTH" />
+ <uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
+
+ <instrumentation
+ android:name="android.test.InstrumentationTestRunner"
+ android:targetPackage="android.settings.functional"
+ android:label="Android Settings Functional Tests" />
+</manifest>
diff --git a/tests/functional/settingstests/src/com/android/settings/functional/AboutPhoneSettingsTests.java b/tests/functional/settingstests/src/com/android/settings/functional/AboutPhoneSettingsTests.java
new file mode 100644
index 0000000..2d6171b
--- /dev/null
+++ b/tests/functional/settingstests/src/com/android/settings/functional/AboutPhoneSettingsTests.java
@@ -0,0 +1,217 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.settings.functional;
+
+import android.content.Intent;
+import android.os.RemoteException;
+import android.provider.Settings;
+import android.support.test.uiautomator.By;
+import android.support.test.uiautomator.Direction;
+import android.support.test.uiautomator.UiDevice;
+import android.support.test.uiautomator.UiObject2;
+import android.support.test.uiautomator.Until;
+import android.test.InstrumentationTestCase;
+import android.test.suitebuilder.annotation.MediumTest;
+import android.text.TextUtils;
+import android.util.Log;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Iterator;
+
+/** Verifies basic functionality of the About Phone screen */
+public class AboutPhoneSettingsTests extends InstrumentationTestCase {
+ private static final boolean LOCAL_LOGV = false;
+ private static final String SETTINGS_PACKAGE = "com.android.settings";
+ private static final String TAG = "AboutPhoneSettingsTest";
+ private static final int TIMEOUT = 2000;
+
+ private UiDevice mDevice;
+
+ // TODO: retrieve using name/ids from com.android.settings package
+ private static final String[] sResourceTexts = {
+ "Status",
+ "Legal information",
+ "Regulatory information",
+ "Model number",
+ "Android version",
+ "Android security patch level",
+ "Baseband version",
+ "Kernel version",
+ "Build number"
+ };
+
+ private static final String[] sClickableResourceTexts = {
+ "Status", "Legal information", "Regulatory information",
+ };
+
+ @Override
+ public void setUp() throws Exception {
+ if (LOCAL_LOGV) {
+ Log.d(TAG, "-------");
+ }
+ super.setUp();
+ mDevice = UiDevice.getInstance(getInstrumentation());
+ try {
+ mDevice.setOrientationNatural();
+ } catch (RemoteException e) {
+ throw new RuntimeException("Failed to freeze device orientaion", e);
+ }
+
+ // make sure we are in a clean state before starting the test
+ mDevice.pressHome();
+ Thread.sleep(TIMEOUT * 2);
+ launchAboutPhoneSettings(Settings.ACTION_DEVICE_INFO_SETTINGS);
+ // TODO: make sure we are always at the top of the app
+ // currently this will fail if the user has navigated into submenus
+ UiObject2 view =
+ mDevice.wait(
+ Until.findObject(By.res(SETTINGS_PACKAGE + ":id/main_content")), TIMEOUT);
+ assertNotNull("Could not find main About Phone screen", view);
+ view.scroll(Direction.UP, 1.0f);
+ }
+
+ @Override
+ protected void tearDown() throws Exception {
+ mDevice.pressHome(); // finish settings activity
+ mDevice.waitForIdle(TIMEOUT * 2); // give UI time to finish animating
+ super.tearDown();
+ }
+
+ private void launchAboutPhoneSettings(String aboutSetting) throws Exception {
+ Intent aboutIntent = new Intent(aboutSetting);
+ aboutIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ getInstrumentation().getContext().startActivity(aboutIntent);
+ }
+
+ /**
+ * Callable actions that can be taken when a UIObject2 is found
+ *
+ * @param device The current UiDevice
+ * @param item The UiObject2 that was found and can be acted on
+ *
+ * @return {@code true} if the call was successful, and {@code false} otherwise
+ */
+ public interface UIObject2Callback {
+ boolean call(UiDevice device, UiObject2 item) throws Exception;
+ }
+
+ /**
+ * Clicks the given item and then presses the Back button
+ *
+ * <p>Used to test whether a given UiObject2 can be successfully clicked.
+ * Presses Back to restore state to the previous screen.
+ *
+ * @param device The device that can be used to press Back
+ * @param item The item to click
+ *
+ * @return {@code true} if clicking the item succeeded, and {@code false} otherwise
+ */
+ public class UiObject2Clicker implements UIObject2Callback {
+ public boolean call(UiDevice device, UiObject2 item) throws Exception {
+ item.click();
+ Thread.sleep(TIMEOUT * 2); // give UI time to finish animating
+ boolean pressWorked = device.pressBack();
+ Thread.sleep(TIMEOUT * 2);
+ return pressWorked;
+ }
+ }
+
+ /**
+ * Removes items found in the view and optionally takes some action.
+ *
+ * @param device The current UiDevice
+ * @param itemsLeftToFind The items to search for in the current view
+ * @param action Action to call on each item that is found; pass {@code null} to take no action
+ */
+ private void removeItemsAndTakeAction(
+ UiDevice device, ArrayList<String> itemsLeftToFind, UIObject2Callback action) throws Exception {
+ for (Iterator<String> iterator = itemsLeftToFind.iterator(); iterator.hasNext(); ) {
+ String itemText = iterator.next();
+ UiObject2 item = device.wait(Until.findObject(By.text(itemText)), TIMEOUT);
+ if (item != null) {
+ if (LOCAL_LOGV) {
+ Log.d(TAG, itemText + " is present");
+ }
+ iterator.remove();
+ if (action != null) {
+ boolean success = action.call(device, item);
+ assertTrue("Calling action after " + itemText + " did not work", success);
+ }
+ } else {
+ if (LOCAL_LOGV) {
+ Log.d(TAG, "Could not find " + itemText);
+ }
+ }
+ }
+ }
+
+ /**
+ * Searches for UI elements in the current view and optionally takes some action.
+ *
+ * <p>Will scroll down the screen until it has found all elements or reached the bottom.
+ * This allows elements to be found and acted on even if they change order.
+ *
+ * @param device The current UiDevice
+ * @param itemsToFind The items to search for in the current view
+ * @param action Action to call on each item that is found; pass {@code null} to take no action
+ */
+ public void searchForItemsAndTakeAction(UiDevice device, String[] itemsToFind, UIObject2Callback action)
+ throws Exception {
+
+ ArrayList<String> itemsLeftToFind = new ArrayList<String>(Arrays.asList(itemsToFind));
+ assertFalse(
+ "There must be at least one item to search for on the screen!",
+ itemsLeftToFind.isEmpty());
+
+ if (LOCAL_LOGV) {
+ Log.d(TAG, "items: " + TextUtils.join(", ", itemsLeftToFind));
+ }
+ boolean canScrollDown = true;
+ while (canScrollDown && !itemsLeftToFind.isEmpty()) {
+ removeItemsAndTakeAction(device, itemsLeftToFind, action);
+
+ // when we've finished searching the current view, scroll down
+ UiObject2 view =
+ device.wait(
+ Until.findObject(By.res(SETTINGS_PACKAGE + ":id/main_content")),
+ TIMEOUT * 2);
+ if (view != null) {
+ canScrollDown = view.scroll(Direction.DOWN, 1.0f);
+ } else {
+ canScrollDown = false;
+ }
+ }
+ // check the last items once we have reached the bottom of the view
+ removeItemsAndTakeAction(device, itemsLeftToFind, action);
+
+ assertTrue(
+ "The following items were not found on the screen: "
+ + TextUtils.join(", ", itemsLeftToFind),
+ itemsLeftToFind.isEmpty());
+ }
+
+ @MediumTest // UI interaction
+ public void testAllMenuEntriesExist() throws Exception {
+ searchForItemsAndTakeAction(mDevice, sResourceTexts, null);
+ }
+
+ @MediumTest // UI interaction
+ public void testClickableEntriesCanBeClicked() throws Exception {
+ searchForItemsAndTakeAction(mDevice, sClickableResourceTexts, new UiObject2Clicker());
+ }
+}
diff --git a/tests/functional/settingstests/src/com/android/settings/functional/AccessibilitySettingsTests.java b/tests/functional/settingstests/src/com/android/settings/functional/AccessibilitySettingsTests.java
new file mode 100644
index 0000000..c3a59da
--- /dev/null
+++ b/tests/functional/settingstests/src/com/android/settings/functional/AccessibilitySettingsTests.java
@@ -0,0 +1,247 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.settings.functional;
+
+import android.content.Context;
+import android.net.wifi.WifiManager;
+import android.os.RemoteException;
+import android.platform.test.helpers.SettingsHelperImpl;
+import android.provider.Settings;
+import android.support.test.uiautomator.By;
+import android.support.test.uiautomator.Direction;
+import android.support.test.uiautomator.UiDevice;
+import android.support.test.uiautomator.UiObject2;
+import android.support.test.uiautomator.Until;
+import android.test.InstrumentationTestCase;
+import android.test.suitebuilder.annotation.MediumTest;
+import android.test.suitebuilder.annotation.Suppress;
+
+public class AccessibilitySettingsTests extends InstrumentationTestCase {
+
+ private static final String SETTINGS_PACKAGE = "com.android.settings";
+ private static final int TIMEOUT = 2000;
+ private UiDevice mDevice;
+
+ @Override
+ public void setUp() throws Exception {
+ super.setUp();
+ mDevice = UiDevice.getInstance(getInstrumentation());
+ try {
+ mDevice.setOrientationNatural();
+ } catch (RemoteException e) {
+ throw new RuntimeException("failed to freeze device orientaion", e);
+ }
+ }
+
+ @Override
+ protected void tearDown() throws Exception {
+ // Need to finish settings activity
+ mDevice.pressBack();
+ mDevice.pressHome();
+ mDevice.waitForIdle();
+ super.tearDown();
+ }
+
+ @MediumTest
+ public void testHighContrastTextOn() throws Exception {
+ verifyAccessibilitySettingOnOrOff("High contrast text",
+ Settings.Secure.ACCESSIBILITY_HIGH_TEXT_CONTRAST_ENABLED, 0, 1);
+ }
+
+ @MediumTest
+ public void testHighContrastTextOff() throws Exception {
+ verifyAccessibilitySettingOnOrOff("High contrast text",
+ Settings.Secure.ACCESSIBILITY_HIGH_TEXT_CONTRAST_ENABLED, 1, 0);
+ }
+
+ @MediumTest
+ public void testPowerButtonEndsCallOn() throws Exception {
+ verifyAccessibilitySettingOnOrOff("Power button ends call",
+ Settings.Secure.INCALL_POWER_BUTTON_BEHAVIOR, 1, 2);
+ }
+
+ @MediumTest
+ public void testPowerButtonEndsCallOff() throws Exception {
+ verifyAccessibilitySettingOnOrOff("Power button ends call",
+ Settings.Secure.INCALL_POWER_BUTTON_BEHAVIOR, 2, 1);
+ }
+
+ /* Suppressing these four tests. The settings don't play
+ * nice with Settings.System.putInt or Settings.Secure.putInt.
+ * Need further clarification. Filed bug b/27792029
+ */
+ @Suppress
+ @MediumTest
+ public void testAutoRotateScreenOn() throws Exception {
+ verifyAccessibilitySettingOnOrOff("Auto-rotate screen",
+ Settings.System.ACCELEROMETER_ROTATION, 0, 1);
+ }
+
+ @Suppress
+ @MediumTest
+ public void testAutoRotateScreenOff() throws Exception {
+ verifyAccessibilitySettingOnOrOff("Auto-rotate screen",
+ Settings.System.ACCELEROMETER_ROTATION, 1, 0);
+ }
+
+ @Suppress
+ @MediumTest
+ public void testMonoAudioOn() throws Exception {
+ verifyAccessibilitySettingOnOrOff("Mono audio",
+ Settings.System.MASTER_MONO, 0, 1);
+ }
+
+ @Suppress
+ @MediumTest
+ public void testMonoAudioOff() throws Exception {
+ verifyAccessibilitySettingOnOrOff("Mono audio",
+ Settings.System.MASTER_MONO, 1, 0);
+ }
+
+ @MediumTest
+ public void testSpeakPasswordsOn() throws Exception {
+ verifyAccessibilitySettingOnOrOff("Speak passwords",
+ Settings.Secure.ACCESSIBILITY_SPEAK_PASSWORD, 0, 1);
+ }
+
+ @MediumTest
+ public void testSpeakPasswordsOff() throws Exception {
+ verifyAccessibilitySettingOnOrOff("Speak passwords",
+ Settings.Secure.ACCESSIBILITY_SPEAK_PASSWORD, 1, 0);
+ }
+
+ @MediumTest
+ public void testLargeMousePointerOn() throws Exception {
+ verifyAccessibilitySettingOnOrOff("Large mouse pointer",
+ Settings.Secure.ACCESSIBILITY_LARGE_POINTER_ICON, 0, 1);
+ }
+
+ @MediumTest
+ public void testLargeMousePointerOff() throws Exception {
+ verifyAccessibilitySettingOnOrOff("Large mouse pointer",
+ Settings.Secure.ACCESSIBILITY_LARGE_POINTER_ICON, 1, 0);
+ }
+
+ @MediumTest
+ public void testColorCorrection() throws Exception {
+ verifySettingToggleAfterScreenLoad("Color correction",
+ Settings.Secure.ACCESSIBILITY_DISPLAY_DALTONIZER_ENABLED);
+ }
+
+ // Suppressing this test, since UiAutomator + talkback don't play nice
+ @Suppress
+ @MediumTest
+ public void testTalkback() throws Exception {
+ verifySettingToggleAfterScreenLoad("TalkBack",
+ Settings.Secure.ACCESSIBILITY_ENABLED);
+ }
+
+ @MediumTest
+ public void testCaptions() throws Exception {
+ verifySettingToggleAfterScreenLoad("Captions",
+ Settings.Secure.ACCESSIBILITY_CAPTIONING_ENABLED);
+ }
+
+ @MediumTest
+ public void testMagnificationGesture() throws Exception {
+ verifySettingToggleAfterScreenLoad("Magnification gesture",
+ Settings.Secure.ACCESSIBILITY_DISPLAY_MAGNIFICATION_ENABLED);
+ }
+
+ @MediumTest
+ public void testClickAfterPointerStopsMoving() throws Exception {
+ verifySettingToggleAfterScreenLoad("Click after pointer stops moving",
+ Settings.Secure.ACCESSIBILITY_AUTOCLICK_ENABLED);
+ }
+
+ public void launchAccessibilitySettings() throws Exception {
+ SettingsHelperImpl.launchSettingsPage(getInstrumentation().getContext(),
+ Settings.ACTION_ACCESSIBILITY_SETTINGS);
+ }
+
+ private void verifyAccessibilitySettingOnOrOff(String settingText,
+ String settingFlag, int initialFlagValue, int expectedFlagValue) throws Exception {
+ Settings.Secure.putInt(getInstrumentation().getContext().getContentResolver(),
+ settingFlag, initialFlagValue);
+ launchAccessibilitySettings();
+ UiObject2 settingsTitle = findItemOnScreen(settingText);
+ settingsTitle.click();
+ Thread.sleep(TIMEOUT);
+ int settingValue = Settings.Secure
+ .getInt(getInstrumentation().getContext().getContentResolver(), settingFlag);
+ assertEquals(settingText + " not correctly set after toggle", expectedFlagValue, settingValue);
+ }
+
+ private void verifySettingToggleAfterScreenLoad(String settingText, String settingFlag) throws Exception {
+ // Load accessibility settings
+ launchAccessibilitySettings();
+ Settings.Secure.putInt(getInstrumentation().getContext().getContentResolver(),
+ settingFlag, 0);
+ Thread.sleep(TIMEOUT);
+ // Tap on setting required
+ UiObject2 settingTitle = findItemOnScreen(settingText);
+ // Load screen
+ settingTitle.click();
+ Thread.sleep(TIMEOUT);
+ // Toggle value
+ UiObject2 settingToggle = mDevice.wait(Until.findObject(By.text("Off")),
+ TIMEOUT);
+ settingToggle.click();
+ dismissOpenDialog();
+ Thread.sleep(TIMEOUT);
+ // Assert new value
+ int settingValue = Settings.Secure.
+ getInt(getInstrumentation().getContext().getContentResolver(), settingFlag);
+ assertEquals(settingText + " value not set correctly", 1, settingValue);
+ // Toogle value
+ settingToggle.click();
+ dismissOpenDialog();
+ mDevice.pressBack();
+ Thread.sleep(TIMEOUT);
+ // Assert reset to old value
+ settingValue = Settings.Secure.
+ getInt(getInstrumentation().getContext().getContentResolver(), settingFlag);
+ assertEquals(settingText + " value not set correctly", 0, settingValue);
+ }
+
+ private UiObject2 findItemOnScreen(String item) throws Exception {
+ int count = 0;
+ UiObject2 settingsPanel = mDevice.wait(Until.findObject
+ (By.res(SETTINGS_PACKAGE, "list")), TIMEOUT);
+ while (settingsPanel.fling(Direction.UP) && count < 3) {
+ count++;
+ }
+ count = 0;
+ UiObject2 setting = null;
+ while(count < 3 && setting == null) {
+ setting = mDevice.wait(Until.findObject(By.text(item)), TIMEOUT);
+ if (setting == null) {
+ settingsPanel.scroll(Direction.DOWN, 1.0f);
+ }
+ count++;
+ }
+ return setting;
+ }
+
+ private void dismissOpenDialog() throws Exception {
+ UiObject2 okButton = mDevice.wait(Until.findObject
+ (By.res("android:id/button1")), TIMEOUT*2);
+ if (okButton != null) {
+ okButton.click();
+ }
+ }
+}
diff --git a/tests/functional/settingstests/src/com/android/settings/functional/BluetoothNetworkSettingsTests.java b/tests/functional/settingstests/src/com/android/settings/functional/BluetoothNetworkSettingsTests.java
new file mode 100644
index 0000000..a55fea6
--- /dev/null
+++ b/tests/functional/settingstests/src/com/android/settings/functional/BluetoothNetworkSettingsTests.java
@@ -0,0 +1,160 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.settings.functional;
+
+import java.io.IOException;
+import android.content.Context;
+import android.content.Intent;
+import android.bluetooth.BluetoothManager;
+import android.bluetooth.BluetoothAdapter;
+import android.os.RemoteException;
+import android.os.SystemClock;
+import android.provider.Settings;
+import android.provider.Settings.SettingNotFoundException;
+import android.support.test.uiautomator.By;
+import android.support.test.uiautomator.Direction;
+import android.support.test.uiautomator.UiDevice;
+import android.support.test.uiautomator.UiObject2;
+import android.support.test.uiautomator.UiObjectNotFoundException;
+import android.support.test.uiautomator.Until;
+import android.test.InstrumentationTestCase;
+import android.test.suitebuilder.annotation.MediumTest;
+
+
+public class BluetoothNetworkSettingsTests extends InstrumentationTestCase {
+
+ private static final String SETTINGS_PACKAGE = "com.android.settings";
+ private static final int TIMEOUT = 2000;
+ private UiDevice mDevice;
+
+ @Override
+ public void setUp() throws Exception {
+ super.setUp();
+ mDevice = UiDevice.getInstance(getInstrumentation());
+ try {
+ mDevice.setOrientationNatural();
+ } catch (RemoteException e) {
+ throw new RuntimeException("failed to freeze device orientaion", e);
+ }
+ }
+
+ @Override
+ protected void tearDown() throws Exception {
+ mDevice.pressBack();
+ mDevice.pressHome();
+ mDevice.waitForIdle();
+ super.tearDown();
+ }
+
+ @MediumTest
+ public void testBluetoothEnabled() throws Exception {
+ verifyBluetoothOnOrOff(true);
+ }
+
+ @MediumTest
+ public void testBluetoothDisabled() throws Exception {
+ verifyBluetoothOnOrOff(false);
+ }
+
+ @MediumTest
+ public void testRefreshOverflowOption() throws Exception {
+ verifyBluetoothOverflowOptions("Refresh", false, null);
+ }
+
+ @MediumTest
+ public void testRenameOverflowOption() throws Exception {
+ verifyBluetoothOverflowOptions("Rename this device", true, "RENAME");
+ }
+
+ @MediumTest
+ public void testReceivedFilesOverflowOption() throws Exception {
+ verifyBluetoothOverflowOptions("Show received files", true, "Bluetooth received");
+ }
+
+ @MediumTest
+ public void testHelpFeedbackOverflowOption() throws Exception {
+ verifyBluetoothOverflowOptions("Help & feedback", true, "Help");
+ }
+
+ public void launchBluetoothSettings() throws Exception {
+ Intent btIntent = new Intent(Settings.ACTION_BLUETOOTH_SETTINGS);
+ btIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ getInstrumentation().getContext().startActivity(btIntent);
+ Thread.sleep(TIMEOUT * 2);
+ }
+
+ /**
+ * Verifies clicking on the BT overflow option and loading the right screen
+ * @param overflowOptionText the text of the option to be clicked
+ * @param verifyClick if you need a click to be verified
+ * @param optionLoaded text of an element on the post click screen for verification
+ */
+ public void verifyBluetoothOverflowOptions(String overflowOptionText, boolean verifyClick,
+ String optionLoaded) throws Exception {
+ BluetoothAdapter bluetoothAdapter = ((BluetoothManager) getInstrumentation().getContext()
+ .getSystemService(Context.BLUETOOTH_SERVICE)).getAdapter();
+ bluetoothAdapter.enable();
+ launchBluetoothSettings();
+ mDevice.wait(Until.findObject(By.desc("More options")), TIMEOUT).click();
+ Thread.sleep(TIMEOUT);
+ UiObject2 overflowOption = mDevice.wait(Until.findObject(By.text(overflowOptionText)),
+ TIMEOUT);
+ assertNotNull(overflowOptionText + " option is not present in advanced Bluetooth menu",
+ overflowOption);
+ if (verifyClick) {
+ overflowOption.click();
+ // Adding an extra back press to deal with IME+UiAutomator bug
+ if (optionLoaded.equals("RENAME")) {
+ mDevice.pressBack();
+ }
+ UiObject2 loadOption = mDevice.wait(Until.findObject(By.text(optionLoaded)), TIMEOUT);
+ assertNotNull(overflowOptionText + " option did not load correctly on tapping",
+ loadOption);
+ }
+ }
+
+ /**
+ * Toggles the Bluetooth switch and verifies that the change is reflected in Settings
+ * @param verifyOn set to whether you want the setting turned On or Off
+ */
+ private void verifyBluetoothOnOrOff(boolean verifyOn) throws Exception {
+ String switchText = "ON";
+ BluetoothAdapter bluetoothAdapter = ((BluetoothManager) getInstrumentation().getContext()
+ .getSystemService(Context.BLUETOOTH_SERVICE)).getAdapter();
+ if (verifyOn) {
+ switchText = "OFF";
+ bluetoothAdapter.disable();
+ }
+ else {
+ bluetoothAdapter.enable();
+ }
+ launchBluetoothSettings();
+ mDevice.wait(Until
+ .findObject(By.res(SETTINGS_PACKAGE, "switch_widget").text(switchText)), TIMEOUT)
+ .click();
+ Thread.sleep(TIMEOUT);
+ String bluetoothValue =
+ Settings.Global.getString(getInstrumentation().getContext().getContentResolver(),
+ Settings.Global.BLUETOOTH_ON);
+ if (verifyOn) {
+ assertEquals("1", bluetoothValue);
+ }
+ else {
+ assertEquals("0", bluetoothValue);
+ }
+ }
+}
diff --git a/tests/functional/settingstests/src/com/android/settings/functional/DataUsageSettingsTests.java b/tests/functional/settingstests/src/com/android/settings/functional/DataUsageSettingsTests.java
new file mode 100644
index 0000000..202264b
--- /dev/null
+++ b/tests/functional/settingstests/src/com/android/settings/functional/DataUsageSettingsTests.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 android.settings.functional;
+
+import android.content.Context;
+import android.net.wifi.WifiManager;
+import android.os.RemoteException;
+import android.provider.Settings;
+import android.platform.test.helpers.SettingsHelperImpl;
+import android.support.test.uiautomator.By;
+import android.support.test.uiautomator.UiDevice;
+import android.support.test.uiautomator.UiObject2;
+import android.support.test.uiautomator.Until;
+import android.test.InstrumentationTestCase;
+import android.test.suitebuilder.annotation.MediumTest;
+
+public class DataUsageSettingsTests extends InstrumentationTestCase {
+
+ private static final String SETTINGS_PACKAGE = "com.android.settings";
+ private static final int TIMEOUT = 2000;
+ private UiDevice mDevice;
+
+ @Override
+ public void setUp() throws Exception {
+ super.setUp();
+ mDevice = UiDevice.getInstance(getInstrumentation());
+ try {
+ mDevice.setOrientationNatural();
+ } catch (RemoteException e) {
+ throw new RuntimeException("failed to freeze device orientaion", e);
+ }
+ }
+
+ @Override
+ protected void tearDown() throws Exception {
+ // Need to finish settings activity
+ mDevice.pressBack();
+ mDevice.pressHome();
+ super.tearDown();
+ }
+
+ @MediumTest
+ public void testElementsOnDataUsageScreen() throws Exception {
+ launchDataUsageSettings();
+ assertNotNull("Data usage element not found",
+ mDevice.wait(Until.findObject(By.text("Usage")),
+ TIMEOUT));
+ assertNotNull("Data usage bar not found",
+ mDevice.wait(Until.findObject(By.res(SETTINGS_PACKAGE,
+ "color_bar")), TIMEOUT));
+ assertNotNull("Data saver element not found",
+ mDevice.wait(Until.findObject(By.text("Data saver")),
+ TIMEOUT));
+ assertNotNull("WiFi Data usage element not found",
+ mDevice.wait(Until.findObject(By.text("Wi-Fi data usage")),
+ TIMEOUT));
+ assertNotNull("Network restrictions element not found",
+ mDevice.wait(Until.findObject(By.text("Network restrictions")),
+ TIMEOUT));
+ }
+
+ public void launchDataUsageSettings() throws Exception {
+ SettingsHelperImpl.launchSettingsPage(getInstrumentation().getContext(),
+ Settings.ACTION_SETTINGS);
+ mDevice.wait(Until
+ .findObject(By.text("Data usage")), TIMEOUT)
+ .click();
+ Thread.sleep(TIMEOUT * 2);
+ }
+}
diff --git a/tests/functional/settingstests/src/com/android/settings/functional/DisplaySettingsTest.java b/tests/functional/settingstests/src/com/android/settings/functional/DisplaySettingsTest.java
new file mode 100644
index 0000000..54653c4
--- /dev/null
+++ b/tests/functional/settingstests/src/com/android/settings/functional/DisplaySettingsTest.java
@@ -0,0 +1,267 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.settings.functional;
+
+import android.content.ContentResolver;
+import android.provider.Settings;
+import android.platform.test.helpers.SettingsHelperImpl;
+import android.platform.test.helpers.SettingsHelperImpl.SettingsType;
+import android.support.test.uiautomator.By;
+import android.support.test.uiautomator.UiDevice;
+import android.support.test.uiautomator.Until;
+import android.test.InstrumentationTestCase;
+import android.test.suitebuilder.annotation.MediumTest;
+import android.test.suitebuilder.annotation.Suppress;
+
+import java.util.regex.Pattern;
+
+public class DisplaySettingsTest extends InstrumentationTestCase {
+
+ private static final String PAGE = Settings.ACTION_DISPLAY_SETTINGS;
+ private static final int TIMEOUT = 2000;
+ private static final FontSetting FONT_SMALL = new FontSetting("Small", 0.85f);
+ private static final FontSetting FONT_NORMAL = new FontSetting("Default", 1.00f);
+ private static final FontSetting FONT_LARGE = new FontSetting("Large", 1.15f);
+ private static final FontSetting FONT_HUGE = new FontSetting("Largest", 1.30f);
+
+ private UiDevice mDevice;
+ private ContentResolver mResolver;
+ private SettingsHelperImpl mHelper;
+
+ @Override
+ public void setUp() throws Exception {
+ super.setUp();
+ mDevice = UiDevice.getInstance(getInstrumentation());
+ mDevice.setOrientationNatural();
+ mResolver = getInstrumentation().getContext().getContentResolver();
+ mHelper = new SettingsHelperImpl(getInstrumentation());
+ }
+
+ @Override
+ public void tearDown() throws Exception {
+ // reset settings we touched that may impact others
+ Settings.System.putFloat(mResolver, Settings.System.FONT_SCALE, 1.00f);
+ mDevice.waitForIdle();
+ super.tearDown();
+ }
+
+ @MediumTest
+ public void testAdaptiveBrightness() throws Exception {
+ SettingsHelperImpl.launchSettingsPage(getInstrumentation().getContext(), PAGE);
+ mHelper.scrollVert(true);
+ Thread.sleep(1000);
+ assertTrue(mHelper.verifyToggleSetting(SettingsType.SYSTEM, PAGE, "Adaptive brightness",
+ Settings.System.SCREEN_BRIGHTNESS_MODE));
+ assertTrue(mHelper.verifyToggleSetting(SettingsType.SYSTEM, PAGE, "Adaptive brightness",
+ Settings.System.SCREEN_BRIGHTNESS_MODE));
+ }
+
+ @MediumTest
+ public void testCameraDoubleTap() throws Exception {
+ assertTrue(mHelper.verifyToggleSetting(SettingsType.SECURE, PAGE,
+ "Press power button twice for camera",
+ Settings.Secure.CAMERA_DOUBLE_TAP_POWER_GESTURE_DISABLED));
+ assertTrue(mHelper.verifyToggleSetting(SettingsType.SECURE, PAGE,
+ "Press power button twice for camera",
+ Settings.Secure.CAMERA_DOUBLE_TAP_POWER_GESTURE_DISABLED));
+ }
+
+ @MediumTest
+ public void testAmbientDisplay() throws Exception {
+ // unique to the ambient display setting, null is equivalent to "on",
+ // so we need to populate the setting if it hasn't been yet
+ String initialSetting = Settings.Secure.getString(mResolver, Settings.Secure.DOZE_ENABLED);
+ if (initialSetting == null) {
+ Settings.Secure.putString(mResolver, Settings.Secure.DOZE_ENABLED, "1");
+ }
+ assertTrue(mHelper.verifyToggleSetting(SettingsType.SECURE, PAGE, "Ambient display",
+ Settings.Secure.DOZE_ENABLED));
+ assertTrue(mHelper.verifyToggleSetting(SettingsType.SECURE, PAGE, "Ambient display",
+ Settings.Secure.DOZE_ENABLED));
+ }
+
+ // blocked on b/27487224
+ @MediumTest
+ @Suppress
+ public void testDaydreamToggle() throws Exception {
+ SettingsHelperImpl.launchSettingsPage(getInstrumentation().getContext(), PAGE);
+ Pattern p = Pattern.compile("On|Off");
+ mHelper.clickSetting("Screen saver");
+ Thread.sleep(1000);
+ try {
+ assertTrue(mHelper.verifyToggleSetting(SettingsType.SECURE, PAGE, p,
+ Settings.Secure.SCREENSAVER_ENABLED, false));
+ assertTrue(mHelper.verifyToggleSetting(SettingsType.SECURE, PAGE, p,
+ Settings.Secure.SCREENSAVER_ENABLED, false));
+ } finally {
+ mDevice.pressBack();
+ }
+ }
+
+ @MediumTest
+ public void testAccelRotation() throws Exception {
+ SettingsHelperImpl.launchSettingsPage(getInstrumentation().getContext(), PAGE);
+ mHelper.scrollVert(true);
+ Thread.sleep(4000);
+ String[] buttons = {
+ "Rotate the contents of the screen",
+ "Stay in portrait view"
+ };
+ int currentAccelSetting = Settings.System.getInt(
+ mResolver, Settings.System.ACCELEROMETER_ROTATION);
+ mHelper.scrollVert(false);
+ mHelper.clickSetting("When device is rotated");
+ assertTrue(mHelper.verifyToggleSetting(SettingsType.SYSTEM, PAGE,
+ buttons[currentAccelSetting], Settings.System.ACCELEROMETER_ROTATION, false));
+ mHelper.scrollVert(false);
+ mHelper.clickSetting("When device is rotated");
+ assertTrue(mHelper.verifyToggleSetting(SettingsType.SYSTEM, PAGE,
+ buttons[1 - currentAccelSetting], Settings.System.ACCELEROMETER_ROTATION, false));
+ }
+
+ @MediumTest
+ public void testDaydream() throws Exception {
+ Settings.Secure.putInt(mResolver, Settings.Secure.SCREENSAVER_ENABLED, 1);
+ SettingsHelperImpl.launchSettingsPage(getInstrumentation().getContext(), PAGE);
+ try {
+ assertTrue(mHelper.verifyRadioSetting(SettingsType.SECURE, PAGE,
+ "Screen saver", "Clock", Settings.Secure.SCREENSAVER_COMPONENTS,
+ "com.google.android.deskclock/com.android.deskclock.Screensaver"));
+ assertTrue(mHelper.verifyRadioSetting(SettingsType.SECURE, PAGE,
+ null, "Colors", Settings.Secure.SCREENSAVER_COMPONENTS,
+ "com.android.dreams.basic/com.android.dreams.basic.Colors"));
+ assertTrue(mHelper.verifyRadioSetting(SettingsType.SECURE, PAGE,
+ null, "Photos", Settings.Secure.SCREENSAVER_COMPONENTS,
+ "com.google.android.apps.photos/com.google.android.apps.photos.daydream.PhotosDreamService"));
+ } finally {
+ mDevice.pressBack();
+ Thread.sleep(2000);
+ }
+ }
+
+ @MediumTest
+ public void testSleep15Seconds() throws Exception {
+ SettingsHelperImpl.launchSettingsPage(getInstrumentation().getContext(), PAGE);
+ assertTrue(mHelper.verifyRadioSetting(SettingsType.SYSTEM, PAGE,
+ "Sleep", "15 seconds", Settings.System.SCREEN_OFF_TIMEOUT, "15000"));
+ }
+
+ @MediumTest
+ public void testSleep30Seconds() throws Exception {
+ SettingsHelperImpl.launchSettingsPage(getInstrumentation().getContext(), PAGE);
+ assertTrue(mHelper.verifyRadioSetting(SettingsType.SYSTEM, PAGE,
+ "Sleep", "30 seconds", Settings.System.SCREEN_OFF_TIMEOUT, "30000"));
+ }
+
+ @MediumTest
+ public void testSleep1Minute() throws Exception {
+ SettingsHelperImpl.launchSettingsPage(getInstrumentation().getContext(), PAGE);
+ assertTrue(mHelper.verifyRadioSetting(SettingsType.SYSTEM, PAGE,
+ "Sleep", "1 minute", Settings.System.SCREEN_OFF_TIMEOUT, "60000"));
+ }
+
+ @MediumTest
+ public void testSleep2Minutes() throws Exception {
+ SettingsHelperImpl.launchSettingsPage(getInstrumentation().getContext(), PAGE);
+ assertTrue(mHelper.verifyRadioSetting(SettingsType.SYSTEM, PAGE,
+ "Sleep", "2 minutes", Settings.System.SCREEN_OFF_TIMEOUT, "120000"));
+ }
+
+ @MediumTest
+ public void testSleep5Minutes() throws Exception {
+ SettingsHelperImpl.launchSettingsPage(getInstrumentation().getContext(), PAGE);
+ assertTrue(mHelper.verifyRadioSetting(SettingsType.SYSTEM, PAGE,
+ "Sleep", "5 minutes", Settings.System.SCREEN_OFF_TIMEOUT, "300000"));
+ }
+
+ @MediumTest
+ public void testSleep10Minutes() throws Exception {
+ SettingsHelperImpl.launchSettingsPage(getInstrumentation().getContext(), PAGE);
+ assertTrue(mHelper.verifyRadioSetting(SettingsType.SYSTEM, PAGE,
+ "Sleep", "10 minutes", Settings.System.SCREEN_OFF_TIMEOUT, "600000"));
+ }
+
+ @MediumTest
+ public void testSleep30Minutes() throws Exception {
+ SettingsHelperImpl.launchSettingsPage(getInstrumentation().getContext(), PAGE);
+ assertTrue(mHelper.verifyRadioSetting(SettingsType.SYSTEM, PAGE,
+ "Sleep", "30 minutes", Settings.System.SCREEN_OFF_TIMEOUT, "1800000"));
+ }
+
+ @MediumTest
+ public void testFontSizeLarge() throws Exception {
+ verifyFontSizeSetting(1.00f, FONT_LARGE);
+ // Leaving the font size at large can make later tests fail, so reset it
+ Settings.System.putFloat(mResolver, Settings.System.FONT_SCALE, 1.00f);
+ // It takes a second for the new font size to be picked up
+ Thread.sleep(2000);
+ }
+
+ @MediumTest
+ public void testFontSizeDefault() throws Exception {
+ verifyFontSizeSetting(1.15f, FONT_NORMAL);
+ }
+
+ @MediumTest
+ public void testFontSizeLargest() throws Exception {
+ verifyFontSizeSetting(1.00f, FONT_HUGE);
+ // Leaving the font size at huge can make later tests fail, so reset it
+ Settings.System.putFloat(mResolver, Settings.System.FONT_SCALE, 1.00f);
+ // It takes a second for the new font size to be picked up
+ Thread.sleep(2000);
+ }
+
+ @MediumTest
+ public void testFontSizeSmall() throws Exception {
+ verifyFontSizeSetting(1.00f, FONT_SMALL);
+ }
+
+ private void verifyFontSizeSetting(float resetValue, FontSetting setting)
+ throws Exception {
+ Settings.System.putFloat(mResolver, Settings.System.FONT_SCALE, resetValue);
+ SettingsHelperImpl.launchSettingsPage(getInstrumentation().getContext(), PAGE);
+ mHelper.clickSetting("Font size");
+ try {
+ mDevice.wait(Until.findObject(By.desc(setting.getName())), TIMEOUT).click();
+ Thread.sleep(1000);
+ float changedValue = Settings.System.getFloat(
+ mResolver, Settings.System.FONT_SCALE);
+ assertEquals(setting.getSize(), changedValue, 0.0001);
+ } finally {
+ // Make sure to back out of the font menu
+ mDevice.pressBack();
+ }
+ }
+
+ private static class FontSetting {
+ private final String mSizeName;
+ private final float mSizeVal;
+
+ public FontSetting(String sizeName, float sizeVal) {
+ mSizeName = sizeName;
+ mSizeVal = sizeVal;
+ }
+
+ public String getName() {
+ return mSizeName;
+ }
+
+ public float getSize() {
+ return mSizeVal;
+ }
+ }
+}
diff --git a/tests/functional/settingstests/src/com/android/settings/functional/LocationSettingsTests.java b/tests/functional/settingstests/src/com/android/settings/functional/LocationSettingsTests.java
new file mode 100644
index 0000000..a6e77bc
--- /dev/null
+++ b/tests/functional/settingstests/src/com/android/settings/functional/LocationSettingsTests.java
@@ -0,0 +1,217 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.settings.functional;
+
+import android.content.Context;
+import android.net.wifi.WifiManager;
+import android.nfc.NfcAdapter;
+import android.nfc.NfcManager;
+import android.os.RemoteException;
+import android.provider.Settings;
+import android.platform.test.helpers.SettingsHelperImpl;
+import android.support.test.uiautomator.By;
+import android.support.test.uiautomator.Direction;
+import android.support.test.uiautomator.UiDevice;
+import android.support.test.uiautomator.UiObject2;
+import android.support.test.uiautomator.Until;
+import android.test.InstrumentationTestCase;
+import android.test.suitebuilder.annotation.MediumTest;
+
+
+public class LocationSettingsTests extends InstrumentationTestCase {
+
+ private static final String SETTINGS_PACKAGE = "com.android.settings";
+ private static final int TIMEOUT = 2000;
+ private UiDevice mDevice;
+
+ @Override
+ public void setUp() throws Exception {
+ super.setUp();
+ mDevice = UiDevice.getInstance(getInstrumentation());
+ try {
+ mDevice.setOrientationNatural();
+ } catch (RemoteException e) {
+ throw new RuntimeException("failed to freeze device orientaion", e);
+ }
+ }
+
+ @Override
+ protected void tearDown() throws Exception {
+ mDevice.pressBack();
+ mDevice.pressHome();
+ super.tearDown();
+ }
+
+ @MediumTest
+ public void testLoadingLocationSettings () throws Exception {
+ // Load settings
+ SettingsHelperImpl.launchSettingsPage(getInstrumentation().getContext(),
+ Settings.ACTION_SETTINGS);
+ // Tap on location
+ UiObject2 settingsPanel = mDevice.wait(Until.findObject
+ (By.res(SETTINGS_PACKAGE, "dashboard_container")), TIMEOUT);
+ int count = 0;
+ UiObject2 locationTitle = null;
+ while(count < 6 && locationTitle == null) {
+ locationTitle = mDevice.wait(Until.findObject(By.text("Location")), TIMEOUT);
+ if (locationTitle == null) {
+ settingsPanel.scroll(Direction.DOWN, 1.0f);
+ }
+ count++;
+ }
+ // Verify location settings loads.
+ locationTitle.click();
+ Thread.sleep(TIMEOUT);
+ assertNotNull("Location screen has not loaded correctly",
+ mDevice.wait(Until.findObject(By.text("Location services")), TIMEOUT));
+ }
+
+ @MediumTest
+ public void testLocationSettingOn() throws Exception {
+ verifyLocationSettingsOnOrOff(true);
+ }
+
+ @MediumTest
+ public void testLocationSettingOff() throws Exception {
+ verifyLocationSettingsOnOrOff(false);
+ }
+
+ @MediumTest
+ public void testLocationDeviceOnlyMode() throws Exception {
+ // Changing the value from default before testing the toggle to Device only mode
+ Settings.Secure.putInt(getInstrumentation().getContext().getContentResolver(),
+ Settings.Secure.LOCATION_MODE, Settings.Secure.LOCATION_MODE_BATTERY_SAVING);
+ Thread.sleep(TIMEOUT);
+ verifyLocationSettingsMode(Settings.Secure.LOCATION_MODE_SENSORS_ONLY);
+ }
+
+ @MediumTest
+ public void testLocationBatterySavingMode() throws Exception {
+ Settings.Secure.putInt(getInstrumentation().getContext().getContentResolver(),
+ Settings.Secure.LOCATION_MODE, Settings.Secure.LOCATION_MODE_SENSORS_ONLY);
+ Thread.sleep(TIMEOUT);
+ verifyLocationSettingsMode(Settings.Secure.LOCATION_MODE_BATTERY_SAVING);
+ }
+
+ @MediumTest
+ public void testLocationHighAccuracyMode() throws Exception {
+ Settings.Secure.putInt(getInstrumentation().getContext().getContentResolver(),
+ Settings.Secure.LOCATION_MODE, Settings.Secure.LOCATION_MODE_SENSORS_ONLY);
+ Thread.sleep(TIMEOUT);
+ verifyLocationSettingsMode(Settings.Secure.LOCATION_MODE_HIGH_ACCURACY);
+ }
+
+ @MediumTest
+ public void testLocationSettingsElements() throws Exception {
+ String[] textElements = {"Location", "On", "Mode", "Recent location requests",
+ "Location services"};
+ SettingsHelperImpl.launchSettingsPage(getInstrumentation().getContext(),
+ Settings.ACTION_LOCATION_SOURCE_SETTINGS);
+ Thread.sleep(TIMEOUT);
+ for (String element : textElements) {
+ assertNotNull(element + " item not found under Location Settings",
+ mDevice.wait(Until.findObject(By.text(element)), TIMEOUT));
+ }
+ }
+
+ @MediumTest
+ public void testLocationSettingsOverflowMenuElements() throws Exception {
+ SettingsHelperImpl.launchSettingsPage(getInstrumentation().getContext(),
+ Settings.ACTION_LOCATION_SOURCE_SETTINGS);
+ // Tap on overflow menu
+ mDevice.wait(Until.findObject(By.desc("More options")), TIMEOUT).click();
+ // Verify scanning
+ assertNotNull("Scanning item not found under Location Settings",
+ mDevice.wait(Until.findObject(By.text("Scanning")), TIMEOUT));
+ // Verify help & feedback
+ assertNotNull("Help & feedback item not found under Location Settings",
+ mDevice.wait(Until.findObject(By.text("Help & feedback")), TIMEOUT));
+ }
+
+ private void verifyLocationSettingsMode(int mode) throws Exception {
+ int modeIntValue = 1;
+ String textMode = "Device only";
+ if (mode == Settings.Secure.LOCATION_MODE_HIGH_ACCURACY) {
+ modeIntValue = 3;
+ textMode = "High accuracy";
+ }
+ else if (mode == Settings.Secure.LOCATION_MODE_BATTERY_SAVING) {
+ modeIntValue = 2;
+ textMode = "Battery saving";
+ }
+ // Load location settings
+ SettingsHelperImpl.launchSettingsPage(getInstrumentation().getContext(),
+ Settings.ACTION_LOCATION_SOURCE_SETTINGS);
+ // Tap on mode
+ mDevice.wait(Until.findObject(By.text("Mode")), TIMEOUT).click();
+ Thread.sleep(TIMEOUT);
+ assertNotNull("Location mode screen not loaded", mDevice.wait(Until.findObject
+ (By.text("Location mode")), TIMEOUT));
+ // Choose said mode
+ mDevice.wait(Until.findObject(By.text(textMode)), TIMEOUT).click();
+ Thread.sleep(TIMEOUT);
+ if (mode == Settings.Secure.LOCATION_MODE_HIGH_ACCURACY ||
+ mode == Settings.Secure.LOCATION_MODE_BATTERY_SAVING) {
+ // Expect another dialog here at improving location tracking
+ // for the first time
+ UiObject2 agreeDialog = mDevice.wait(Until.findObject
+ (By.text("Improve location accuracy?")), TIMEOUT);
+ if (agreeDialog != null) {
+ mDevice.wait(Until.findObject
+ (By.text("AGREE")), TIMEOUT).click();
+ Thread.sleep(TIMEOUT);
+ assertNull("Improve location dialog not dismissed", mDevice.wait(Until.findObject
+ (By.text("Improve location accuracy?")), TIMEOUT));
+ }
+ }
+ // get setting and verify value
+ // Verify change of mode
+ int locationSettingMode =
+ Settings.Secure.getInt(getInstrumentation().getContext().getContentResolver(),
+ Settings.Secure.LOCATION_MODE);
+ assertEquals(mode + " value not set correctly for location.", modeIntValue,
+ locationSettingMode);
+ }
+
+ private void verifyLocationSettingsOnOrOff(boolean verifyOn) throws Exception {
+ // Set location flag
+ if (verifyOn) {
+ Settings.Secure.putInt(getInstrumentation().getContext().getContentResolver(),
+ Settings.Secure.LOCATION_MODE, Settings.Secure.LOCATION_MODE_OFF);
+ }
+ else {
+ Settings.Secure.putInt(getInstrumentation().getContext().getContentResolver(),
+ Settings.Secure.LOCATION_MODE, Settings.Secure.LOCATION_MODE_SENSORS_ONLY);
+ }
+ // Load location settings
+ SettingsHelperImpl.launchSettingsPage(getInstrumentation().getContext(),
+ Settings.ACTION_LOCATION_SOURCE_SETTINGS);
+ // Toggle UI
+ mDevice.wait(Until.findObject(By.res(SETTINGS_PACKAGE, "switch_widget")), TIMEOUT).click();
+ Thread.sleep(TIMEOUT);
+ // Verify change in setting
+ int locationEnabled = Settings.Secure.getInt(getInstrumentation()
+ .getContext().getContentResolver(),
+ Settings.Secure.LOCATION_MODE);
+ if (verifyOn) {
+ assertFalse("Location not enabled correctly", locationEnabled == 0);
+ }
+ else {
+ assertEquals("Location not disabled correctly", 0, locationEnabled);
+ }
+ }
+}
diff --git a/tests/functional/settingstests/src/com/android/settings/functional/MainSettingsLargeTests.java b/tests/functional/settingstests/src/com/android/settings/functional/MainSettingsLargeTests.java
new file mode 100644
index 0000000..afc4fe1
--- /dev/null
+++ b/tests/functional/settingstests/src/com/android/settings/functional/MainSettingsLargeTests.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 android.settings.functional;
+
+import android.content.Context;
+import android.content.Intent;
+import android.os.RemoteException;
+import android.os.SystemClock;
+import android.provider.Settings;
+import android.provider.Settings.SettingNotFoundException;
+import android.support.test.uiautomator.By;
+import android.support.test.uiautomator.Direction;
+import android.support.test.uiautomator.UiDevice;
+import android.support.test.uiautomator.UiObject2;
+import android.support.test.uiautomator.UiObjectNotFoundException;
+import android.support.test.uiautomator.Until;
+import android.test.InstrumentationTestCase;
+import android.test.suitebuilder.annotation.LargeTest;
+import android.util.Log;
+
+import junit.framework.Assert;
+
+public class MainSettingsLargeTests extends InstrumentationTestCase {
+ private static final String SETTINGS_PACKAGE = "com.android.settings";
+ private static final int TIMEOUT = 2000;
+ private static final String PERSONAL_CATEGORY = "Personal";
+ private static final String[] sPersonalItems = new String[] {
+ "Location", "Security", "Accounts", "Google", "Languages & input", "Backup & reset"
+ };
+ private static final String SYSTEM_CATEGORY = "System";
+ private static final String[] sSystemItems = new String[] {
+ "Date & time", "Accessibility", "Printing", "About phone"
+ };
+ private UiDevice mDevice;
+ private Context mContext = null;
+
+ @Override
+ public void setUp() throws Exception {
+ super.setUp();
+ mDevice = UiDevice.getInstance(getInstrumentation());
+ mContext = getInstrumentation().getContext();
+ try {
+ mDevice.setOrientationNatural();
+ } catch (RemoteException e) {
+ throw new RuntimeException("failed to freeze device orientaion", e);
+ }
+ }
+
+ @Override
+ public void tearDown() throws Exception {
+ // Need to finish settings activity
+ mDevice.pressHome();
+ super.tearDown();
+ }
+
+ @LargeTest
+ public void testPersonalSettingCategory() throws Exception {
+ launchMainSettingsCategory(PERSONAL_CATEGORY, sPersonalItems);
+ }
+
+ @LargeTest
+ public void testSystemSettingCategory() throws Exception {
+ launchMainSettingsCategory(SYSTEM_CATEGORY, sSystemItems);
+ }
+
+ private void launchMainSettingsCategory(String category, String[] items) throws Exception {
+ launchMainSettings(Settings.ACTION_SETTINGS);
+ launchSettingItems(category);
+ for (String i : items) {
+ launchSettingItems(i);
+ Log.d(SETTINGS_PACKAGE, String.format("launch setting: %s", i));
+ }
+ }
+
+ private void launchMainSettings(String mainSetting) throws Exception {
+ mDevice.pressHome();
+ Intent settingIntent = new Intent(mainSetting);
+ settingIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ getInstrumentation().getContext().startActivity(settingIntent);
+ Thread.sleep(TIMEOUT * 2);
+ }
+
+ private void launchSettingItems(String title) throws Exception {
+ int maxAttempt = 5;
+ UiObject2 item = null;
+ UiObject2 view = null;
+ while (maxAttempt-- > 0) {
+ item = mDevice.wait(Until.findObject(By.res("android:id/title").text(title)), TIMEOUT);
+ if (item == null) {
+ view = mDevice.wait(
+ Until.findObject(By.res(SETTINGS_PACKAGE, "main_content")),
+ TIMEOUT);
+ view.scroll(Direction.DOWN, 1.0f);
+ } else {
+ return;
+ }
+ }
+ assertNotNull(String.format("%s in Setting has not been loaded correctly", title), item);
+ }
+}
\ No newline at end of file
diff --git a/tests/functional/settingstests/src/com/android/settings/functional/MainSettingsTests.java b/tests/functional/settingstests/src/com/android/settings/functional/MainSettingsTests.java
new file mode 100644
index 0000000..eec5f39
--- /dev/null
+++ b/tests/functional/settingstests/src/com/android/settings/functional/MainSettingsTests.java
@@ -0,0 +1,158 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.settings.functional;
+
+import android.content.Context;
+import android.content.Intent;
+import android.os.RemoteException;
+import android.os.SystemClock;
+import android.platform.test.helpers.SettingsHelperImpl;
+import android.provider.Settings;
+import android.provider.Settings.SettingNotFoundException;
+import android.util.Log;
+import android.view.inputmethod.InputMethodManager;
+import android.support.test.uiautomator.By;
+import android.support.test.uiautomator.Direction;
+import android.support.test.uiautomator.UiDevice;
+import android.support.test.uiautomator.UiObject2;
+import android.support.test.uiautomator.UiObjectNotFoundException;
+import android.support.test.uiautomator.Until;
+import android.test.InstrumentationTestCase;
+import android.test.suitebuilder.annotation.MediumTest;
+
+import junit.framework.Assert;
+
+/** Component test for verifying basic functionality of Main Setting screen */
+public class MainSettingsTests extends InstrumentationTestCase {
+ private static final String SETTINGS_PACKAGE = "com.android.settings";
+ private static final int TIMEOUT = 2000;
+ private static final String WIFI_CATEGORY = "Wireless & networks";
+ private static final String[] sWifiItems = new String[] {
+ "Wi‑Fi", "Bluetooth", "Data usage", "More"
+ };
+ private static final String DEVICE_CATEGORY = "Device";
+ private static final String[] sDeviceItems = new String[] {
+ "Display", "Notifications", "Sound", "Apps", "Storage", "Battery", "Memory",
+ "Users"
+ };
+ private UiDevice mDevice;
+ private Context mContext = null;
+
+ @Override
+ public void setUp() throws Exception {
+ super.setUp();
+ mDevice = UiDevice.getInstance(getInstrumentation());
+ mContext = getInstrumentation().getContext();
+ mDevice.setOrientationNatural();
+ }
+
+ @Override
+ public void tearDown() throws Exception {
+ // Need to finish settings activity
+ mDevice.pressHome();
+ mDevice.unfreezeRotation();
+ mDevice.waitForIdle();
+ super.tearDown();
+ }
+
+ @MediumTest
+ public void testLoadSetting() throws Exception {
+ launchMainSettings(Settings.ACTION_SETTINGS);
+ UiObject2 settingHeading = mDevice.wait(Until.findObject(By.text("Settings")),
+ TIMEOUT);
+ assertNotNull("Setting menu has not loaded correctly", settingHeading);
+ }
+
+ @MediumTest
+ public void testWifiSettingCategory() throws Exception {
+ launchMainSettingsCategory(WIFI_CATEGORY, sWifiItems);
+ }
+
+ @MediumTest
+ public void testDeviceSettingCategory() throws Exception {
+ launchMainSettingsCategory(DEVICE_CATEGORY, sDeviceItems);
+ }
+
+ private void launchMainSettingsCategory(String category, String[] items) throws Exception {
+ SettingsHelper.launchSettingsPage(getInstrumentation().getContext(),
+ Settings.ACTION_SETTINGS);
+ launchSettingItems(category);
+ for (String i : items) {
+ launchSettingItems(i);
+ Log.d(SETTINGS_PACKAGE, String.format("launch setting: %s", i));
+ }
+ UiObject2 mainSettings = mDevice.wait(
+ Until.findObject(By.res("com.android.settings:id/main_content")),
+ TIMEOUT);
+ // Scrolling back up twice so we're at the top of the settings list.
+ for ( int i = 0; i < 2; i++) {
+ mainSettings.scroll(Direction.UP, 1.0f);
+ }
+ }
+
+ @MediumTest
+ public void testSearchSetting() throws Exception {
+ launchMainSettings(Settings.ACTION_SETTINGS);
+ mDevice.wait(Until.findObject(By.desc("Search settings")), TIMEOUT).click();
+ UiObject2 searchBox = mDevice.wait(Until.findObject(By.res("android:id/search_src_text")),
+ TIMEOUT);
+ InputMethodManager imm = (InputMethodManager) mContext
+ .getSystemService(Context.INPUT_METHOD_SERVICE);
+ if (!imm.isAcceptingText()) {
+ mDevice.wait(Until.findObject(By.desc("Collapse")), TIMEOUT).click();
+ assertNotNull("Search Setting has not loaded correctly", searchBox);
+ } else {
+ mDevice.wait(Until.findObject(By.desc("Collapse")), TIMEOUT).click();
+ }
+ }
+
+ @MediumTest
+ public void testOverflowSetting() throws Exception {
+ launchMainSettings(Settings.ACTION_SETTINGS);
+ mDevice.wait(Until.findObject(By.desc("More options")), TIMEOUT).click();
+ mDevice.wait(Until.findObject(By.text("Help & feedback")), TIMEOUT).click();
+ UiObject2 help = mDevice.wait(Until.findObject(By.text("Help")),
+ TIMEOUT);
+ assertNotNull("Overflow setting has not loaded correctly", help);
+ }
+
+ private void launchMainSettings(String mainSetting) throws Exception {
+ mDevice.pressHome();
+ Intent settingIntent = new Intent(mainSetting);
+ settingIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ getInstrumentation().getContext().startActivity(settingIntent);
+ Thread.sleep(TIMEOUT * 3);
+ }
+
+ private void launchSettingItems(String title) throws Exception {
+ int maxAttempt = 5;
+ UiObject2 item = null;
+ UiObject2 view = null;
+ while (maxAttempt-- > 0) {
+ item = mDevice.wait(Until.findObject(By.res("android:id/title").text(title)), TIMEOUT);
+ if (item == null) {
+ view = mDevice.wait(
+ Until.findObject(By.res("com.android.settings:id/main_content")),
+ TIMEOUT);
+ view.scroll(Direction.DOWN, 1.0f);
+ } else {
+ return;
+ }
+ }
+ assertNotNull(String.format("%s in Setting has not been loaded correctly", title), item);
+ }
+}
diff --git a/tests/functional/settingstests/src/com/android/settings/functional/MoreWirelessSettingsTests.java b/tests/functional/settingstests/src/com/android/settings/functional/MoreWirelessSettingsTests.java
new file mode 100644
index 0000000..e306a3f
--- /dev/null
+++ b/tests/functional/settingstests/src/com/android/settings/functional/MoreWirelessSettingsTests.java
@@ -0,0 +1,147 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.settings.functional;
+
+import android.content.Context;
+import android.net.wifi.WifiManager;
+import android.nfc.NfcAdapter;
+import android.nfc.NfcManager;
+import android.os.RemoteException;
+import android.provider.Settings;
+import android.platform.test.helpers.SettingsHelperImpl;
+import android.support.test.uiautomator.By;
+import android.support.test.uiautomator.UiDevice;
+import android.support.test.uiautomator.UiObject2;
+import android.support.test.uiautomator.Until;
+import android.test.InstrumentationTestCase;
+import android.test.suitebuilder.annotation.MediumTest;
+
+
+public class MoreWirelessSettingsTests extends InstrumentationTestCase {
+
+ private static final String SETTINGS_PACKAGE = "com.android.settings";
+ private static final int TIMEOUT = 2000;
+ private UiDevice mDevice;
+
+ @Override
+ public void setUp() throws Exception {
+ super.setUp();
+ mDevice = UiDevice.getInstance(getInstrumentation());
+ try {
+ mDevice.setOrientationNatural();
+ } catch (RemoteException e) {
+ throw new RuntimeException("failed to freeze device orientaion", e);
+ }
+ }
+
+ @Override
+ protected void tearDown() throws Exception {
+ mDevice.pressBack();
+ mDevice.pressHome();
+ super.tearDown();
+ }
+
+ @MediumTest
+ public void testAirplaneModeEnabled() throws Exception {
+ verifyAirplaneModeOnOrOff(true);
+ }
+
+ @MediumTest
+ public void testAirplaneModeDisabled() throws Exception {
+ verifyAirplaneModeOnOrOff(false);
+ }
+
+ // This NFC toggle test is set up this way since there's no way to set
+ // the NFC flag to enabled or disabled without touching UI.
+ // This way, we get coverage for whether or not the toggle button works.
+ @MediumTest
+ public void testNFCToggle() throws Exception {
+ NfcManager manager = (NfcManager) getInstrumentation().getContext()
+ .getSystemService(Context.NFC_SERVICE);
+ NfcAdapter nfcAdapter = manager.getDefaultAdapter();
+ boolean nfcInitiallyEnabled = nfcAdapter.isEnabled();
+ SettingsHelperImpl.launchSettingsPage(getInstrumentation().getContext(),
+ Settings.ACTION_WIRELESS_SETTINGS);
+ UiObject2 nfcSetting = mDevice.wait(Until.findObject(By.text("NFC")), TIMEOUT);
+ nfcSetting.click();
+ Thread.sleep(TIMEOUT*2);
+ if (nfcInitiallyEnabled) {
+ assertFalse("NFC wasn't disabled on toggle", nfcAdapter.isEnabled());
+ nfcSetting.click();
+ Thread.sleep(TIMEOUT*2);
+ assertTrue("NFC wasn't enabled on toggle", nfcAdapter.isEnabled());
+ }
+ else {
+ assertTrue("NFC wasn't enabled on toggle", nfcAdapter.isEnabled());
+ nfcSetting.click();
+ Thread.sleep(TIMEOUT*2);
+ assertFalse("NFC wasn't disabled on toggle", nfcAdapter.isEnabled());
+ }
+ }
+
+ @MediumTest
+ public void testTetheringMenuLoad() throws Exception {
+ SettingsHelperImpl.launchSettingsPage(getInstrumentation().getContext(),
+ Settings.ACTION_WIRELESS_SETTINGS);
+ mDevice.wait(Until
+ .findObject(By.text("Tethering & portable hotspot")), TIMEOUT)
+ .click();
+ Thread.sleep(TIMEOUT);
+ UiObject2 usbTethering = mDevice.wait(Until
+ .findObject(By.text("USB tethering")), TIMEOUT);
+ assertNotNull("Tethering screen did not load correctly", usbTethering);
+ }
+
+ @MediumTest
+ public void testVPNMenuLoad() throws Exception {
+ SettingsHelperImpl.launchSettingsPage(getInstrumentation().getContext(),
+ Settings.ACTION_WIRELESS_SETTINGS);
+ mDevice.wait(Until
+ .findObject(By.text("VPN")), TIMEOUT)
+ .click();
+ Thread.sleep(TIMEOUT);
+ UiObject2 usbTethering = mDevice.wait(Until
+ .findObject(By.res(SETTINGS_PACKAGE, "vpn_create")), TIMEOUT);
+ assertNotNull("VPN screen did not load correctly", usbTethering);
+ }
+
+ private void verifyAirplaneModeOnOrOff(boolean verifyOn) throws Exception {
+ if (verifyOn) {
+ Settings.Global.putString(getInstrumentation().getContext().getContentResolver(),
+ Settings.Global.AIRPLANE_MODE_ON, "0");
+ }
+ else {
+ Settings.Global.putString(getInstrumentation().getContext().getContentResolver(),
+ Settings.Global.AIRPLANE_MODE_ON, "1");
+ }
+ SettingsHelperImpl.launchSettingsPage(getInstrumentation().getContext(),
+ Settings.ACTION_WIRELESS_SETTINGS);
+ mDevice.wait(Until
+ .findObject(By.text("Airplane mode")), TIMEOUT)
+ .click();
+ Thread.sleep(TIMEOUT);
+ String airplaneModeValue = Settings.Global
+ .getString(getInstrumentation().getContext().getContentResolver(),
+ Settings.Global.AIRPLANE_MODE_ON);
+ if (verifyOn) {
+ assertEquals("1", airplaneModeValue);
+ }
+ else {
+ assertEquals("0", airplaneModeValue);
+ }
+ }
+}
diff --git a/tests/functional/settingstests/src/com/android/settings/functional/QuickSettingsTest.java b/tests/functional/settingstests/src/com/android/settings/functional/QuickSettingsTest.java
new file mode 100644
index 0000000..50cd5b9
--- /dev/null
+++ b/tests/functional/settingstests/src/com/android/settings/functional/QuickSettingsTest.java
@@ -0,0 +1,333 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.settings.functional;
+
+import android.bluetooth.BluetoothManager;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.location.LocationManager;
+import android.net.wifi.WifiManager;
+import android.os.RemoteException;
+import android.provider.Settings;
+import android.support.test.uiautomator.UiDevice;
+import android.support.test.uiautomator.UiObject2;
+import android.support.test.uiautomator.By;
+import android.support.test.uiautomator.Until;
+import android.test.InstrumentationTestCase;
+import android.test.suitebuilder.annotation.MediumTest;
+import android.util.Log;
+
+public class QuickSettingsTest extends InstrumentationTestCase {
+ private static final String LOG_TAG = QuickSettingsTest.class.getSimpleName();
+ private static final int LONG_TIMEOUT = 2000;
+ private static final int SHORT_TIMEOUT = 500;
+
+ private enum QuickSettingTiles {
+ WIFI("Wi-Fi"), SIM("SIM"), DND("Do not disturb"), FLASHLIGHT("Flashlight"), SCREEN(
+ "Auto-rotate screen"), BLUETOOTH("Bluetooth"), AIRPLANE("Airplane mode"),
+ LOCATION("Location"), BRIGHTNESS("Display brightness");
+
+ private final String name;
+
+ private QuickSettingTiles(String name) {
+ this.name = name;
+ }
+
+ public String getName() {
+ return this.name;
+ }
+ };
+
+ private UiDevice mDevice = null;
+ private ContentResolver mResolver;
+
+ @Override
+ public void setUp() throws Exception {
+ super.setUp();
+ mDevice = UiDevice.getInstance(getInstrumentation());
+ getInstrumentation().getContext();
+ mResolver = getInstrumentation().getContext().getContentResolver();
+ mDevice.wakeUp();
+ mDevice.pressHome();
+ mDevice.setOrientationNatural();
+ }
+
+ @Override
+ protected void tearDown() throws Exception {
+ // Need to finish settings activity
+ mDevice.pressHome();
+ mDevice.unfreezeRotation();
+ super.tearDown();
+ }
+
+ @MediumTest
+ public void testQuickSettingDrawDown() throws Exception {
+ mDevice.pressHome();
+ // Draw down once to load quick settings shade only
+ swipeDown();
+ UiObject2 quicksettingsShade = mDevice.wait(
+ Until.findObject(By.res("com.android.systemui:id/expand_indicator")),
+ LONG_TIMEOUT);
+ assertNotNull("Quick settings shade not visible on draw down", quicksettingsShade);
+ }
+
+ @MediumTest
+ public void testQuickSettingExpand() throws Exception {
+ launchQuickSetting();
+ // Verify that the settings object is visible on full expansion
+ UiObject2 quicksettingsExpand = mDevice.wait(Until.findObject(By.desc("Open settings.")),
+ LONG_TIMEOUT);
+ assertNotNull("Quick settings shade did not expand correctly on two swipe downs",
+ quicksettingsExpand);
+ }
+
+ @MediumTest
+ public void testQuickSettingCollapse() throws Exception {
+ launchQuickSetting();
+ // Tap on the expand chevron once more to collapse the QS shade
+ mDevice.wait(Until.findObject(By.res("com.android.systemui:id/expand_indicator")),
+ LONG_TIMEOUT).click();
+
+ // Verify that the brightness slider which is only visible on full expansion
+ // isn't visible in the collapsed state
+ UiObject2 quicksettingsExpandedShade = mDevice.wait(
+ Until.findObject(By.descContains(QuickSettingTiles.BRIGHTNESS.getName())),
+ LONG_TIMEOUT);
+ assertNotNull("Quick settings shade did not collapse correctly",
+ quicksettingsExpandedShade);
+ }
+
+ @MediumTest
+ public void testQuickSettingDismiss() throws Exception {
+ launchQuickSetting();
+ // Swipe up twice to fully dismiss quick settings
+ swipeUp();
+ swipeUp();
+ UiObject2 quicksettingsShade = mDevice.wait(
+ Until.findObject(By.res("com.android.systemui:id/expand_indicator")),
+ SHORT_TIMEOUT);
+ assertNull("Quick settings collapsed shade was not dismissed correctly",
+ quicksettingsShade);
+ }
+
+ @MediumTest
+ public void testQuickSettingTilesVisible() throws Exception {
+ launchQuickSetting();
+ Thread.sleep(LONG_TIMEOUT);
+ for (QuickSettingTiles tile : QuickSettingTiles.values()) {
+ UiObject2 quickSettingTile = mDevice.wait(
+ Until.findObject(By.descContains(tile.getName())),
+ SHORT_TIMEOUT);
+ assertNotNull(String.format("%s did not load correctly", tile.getName()),
+ quickSettingTile);
+ }
+ }
+
+ @MediumTest
+ public void testQuickSettingWifiEnabled() throws Exception {
+ verifyWiFiOnOrOff(true);
+ }
+
+ @MediumTest
+ public void testQuickSettingWifiDisabled() throws Exception {
+ verifyWiFiOnOrOff(false);
+ }
+
+ private void verifyWiFiOnOrOff(boolean verifyOn) throws Exception {
+ String airPlaneMode = Settings.Global.getString(
+ mResolver,
+ Settings.Global.AIRPLANE_MODE_ON);
+ try {
+ Settings.Global.putString(mResolver,
+ Settings.Global.AIRPLANE_MODE_ON, "0");
+ Thread.sleep(LONG_TIMEOUT);
+ WifiManager wifiManager = (WifiManager) getInstrumentation().getContext()
+ .getSystemService(Context.WIFI_SERVICE);
+ wifiManager.setWifiEnabled(!verifyOn);
+ launchQuickSetting();
+ mDevice.wait(Until.findObject(By.descContains(QuickSettingTiles.WIFI.getName())),
+ LONG_TIMEOUT).click();
+ if (verifyOn) {
+ mDevice.pressBack();
+ } else {
+ mDevice.wait(Until.findObject(By.res("android:id/toggle")), LONG_TIMEOUT).click();
+ }
+ Thread.sleep(LONG_TIMEOUT);
+ String changedWifiValue = Settings.Global.getString(mResolver, Settings.Global.WIFI_ON);
+ Thread.sleep(LONG_TIMEOUT);
+ if (verifyOn) {
+ assertEquals("1", changedWifiValue);
+ } else {
+ assertEquals("0", changedWifiValue);
+ }
+ } finally {
+ Settings.Global.putString(getInstrumentation().getContext().getContentResolver(),
+ Settings.Global.AIRPLANE_MODE_ON, airPlaneMode);
+ }
+ }
+
+ @MediumTest
+ public void testQuickSettingBluetoothEnabled() throws Exception {
+ verifyBluetoothOnOrOff(true);
+ }
+
+ @MediumTest
+ public void testQuickSettingBluetoothDisabled() throws Exception {
+ verifyBluetoothOnOrOff(false);
+ }
+
+ private void verifyBluetoothOnOrOff(boolean verifyOn) throws Exception {
+ BluetoothManager bluetoothManager = (BluetoothManager) getInstrumentation().getContext()
+ .getSystemService(Context.BLUETOOTH_SERVICE);
+ if (!verifyOn) {
+ bluetoothManager.getAdapter().enable();
+ } else {
+ bluetoothManager.getAdapter().disable();
+ }
+ launchQuickSetting();
+ mDevice.wait(Until.findObject(By.textContains(QuickSettingTiles.BLUETOOTH.getName())),
+ LONG_TIMEOUT).click();
+ if (verifyOn) {
+ mDevice.pressBack();
+ } else {
+ mDevice.wait(Until.findObject(By.res("android:id/toggle")), LONG_TIMEOUT).click();
+ }
+ Thread.sleep(LONG_TIMEOUT);
+ String bluetoothVal = Settings.Global.getString(
+ mResolver,
+ Settings.Global.BLUETOOTH_ON);
+ if (verifyOn) {
+ assertEquals("1", bluetoothVal);
+ } else {
+ assertEquals("0", bluetoothVal);
+ }
+ }
+
+ @MediumTest
+ public void testQuickSettingFlashLight() throws Exception {
+ String lightOn = "On";
+ String lightOff = "Off";
+ boolean verifyOn = false;
+ launchQuickSetting();
+ UiObject2 flashLight = mDevice.wait(
+ Until.findObject(By.descContains(QuickSettingTiles.FLASHLIGHT.getName())),
+ LONG_TIMEOUT);
+ if (flashLight.getText().equals(lightOn)) {
+ verifyOn = true;
+ }
+ mDevice.wait(Until.findObject(By.textContains(QuickSettingTiles.FLASHLIGHT.getName())),
+ LONG_TIMEOUT).click();
+ Thread.sleep(LONG_TIMEOUT);
+ flashLight = mDevice.wait(
+ Until.findObject(By.descContains(QuickSettingTiles.FLASHLIGHT.getName())),
+ LONG_TIMEOUT);
+ if (verifyOn) {
+ assertTrue(flashLight.getText().equals(lightOff));
+ } else {
+ assertTrue(flashLight.getText().equals(lightOn));
+ mDevice.wait(Until.findObject(By.textContains(QuickSettingTiles.FLASHLIGHT.getName())),
+ LONG_TIMEOUT).click();
+ }
+ }
+
+ @MediumTest
+ public void testQuickSettingDND() throws Exception {
+ int onSetting = Settings.Global.getInt(mResolver, "zen_mode");
+ launchQuickSetting();
+ mDevice.wait(Until.findObject(By.descContains(QuickSettingTiles.DND.getName())),
+ LONG_TIMEOUT).click();
+ if (onSetting == 0) {
+ mDevice.pressBack();
+ }
+ Thread.sleep(LONG_TIMEOUT);
+ int changedSetting = Settings.Global.getInt(mResolver, "zen_mode");
+ assertFalse(onSetting == changedSetting);
+ }
+
+ @MediumTest
+ public void testQuickSettingAirplaneMode() throws Exception {
+ int onSetting = Integer.parseInt(Settings.Global.getString(
+ mResolver,
+ Settings.Global.AIRPLANE_MODE_ON));
+ try {
+ launchQuickSetting();
+ mDevice.wait(Until.findObject(By.descContains(QuickSettingTiles.AIRPLANE.getName())),
+ LONG_TIMEOUT).click();
+ Thread.sleep(LONG_TIMEOUT);
+ int changedSetting = Integer.parseInt(Settings.Global.getString(
+ mResolver,
+ Settings.Global.AIRPLANE_MODE_ON));
+ assertTrue((1 - onSetting) == changedSetting);
+ } finally {
+ Settings.Global.putString(getInstrumentation().getContext().getContentResolver(),
+ Settings.Global.AIRPLANE_MODE_ON, Integer.toString(onSetting));
+ }
+ }
+
+ @MediumTest
+ public void testQuickSettingOrientation() throws Exception {
+ launchQuickSetting();
+ mDevice.wait(Until.findObject(By.descContains(QuickSettingTiles.SCREEN.getName())),
+ LONG_TIMEOUT).click();
+ Thread.sleep(LONG_TIMEOUT);
+ String rotation = Settings.System.getString(mResolver,
+ Settings.System.ACCELEROMETER_ROTATION);
+ assertEquals("1", rotation);
+ }
+
+ @MediumTest
+ public void testQuickSettingLocation() throws Exception {
+ LocationManager service = (LocationManager) getInstrumentation().getContext()
+ .getSystemService(Context.LOCATION_SERVICE);
+ boolean onSetting = service.isProviderEnabled(LocationManager.GPS_PROVIDER);
+ try {
+ launchQuickSetting();
+ mDevice.wait(Until.findObject(By.descContains(QuickSettingTiles.LOCATION.getName())),
+ LONG_TIMEOUT).click();
+ Thread.sleep(LONG_TIMEOUT);
+ boolean changedSetting = service.isProviderEnabled(LocationManager.GPS_PROVIDER);
+ assertTrue(onSetting == !changedSetting);
+ } finally {
+ mDevice.wait(Until.findObject(By.descContains(QuickSettingTiles.LOCATION.getName())),
+ LONG_TIMEOUT).click();
+ }
+ }
+
+ private void launchQuickSetting() throws Exception {
+ mDevice.pressHome();
+ swipeDown();
+ Thread.sleep(LONG_TIMEOUT);
+ swipeDown();
+ }
+
+ private void swipeUp() throws Exception {
+ mDevice.swipe(mDevice.getDisplayWidth() / 2, mDevice.getDisplayHeight(),
+ mDevice.getDisplayWidth() / 2, 0, 30);
+ Thread.sleep(SHORT_TIMEOUT);
+ }
+
+ private void swipeDown() throws Exception {
+ mDevice.swipe(mDevice.getDisplayWidth() / 2, 0, mDevice.getDisplayWidth() / 2,
+ mDevice.getDisplayHeight() / 2 + 50, 20);
+ Thread.sleep(SHORT_TIMEOUT);
+ }
+
+ private void swipeLeft() {
+ mDevice.swipe(mDevice.getDisplayWidth() / 2, mDevice.getDisplayHeight() / 2, 0,
+ mDevice.getDisplayHeight() / 2, 5);
+ }
+}
diff --git a/tests/functional/settingstests/src/com/android/settings/functional/SettingsHelper.java b/tests/functional/settingstests/src/com/android/settings/functional/SettingsHelper.java
new file mode 100644
index 0000000..58875d2
--- /dev/null
+++ b/tests/functional/settingstests/src/com/android/settings/functional/SettingsHelper.java
@@ -0,0 +1,140 @@
+package android.settings.functional;
+
+import android.app.Instrumentation;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.Intent;
+import android.provider.Settings;
+import android.provider.Settings.SettingNotFoundException;
+import android.support.test.uiautomator.By;
+import android.support.test.uiautomator.UiDevice;
+import android.support.test.uiautomator.Until;
+
+import java.util.regex.Pattern;
+
+public class SettingsHelper {
+
+ public static final String PKG = "com.android.settings";
+ private static final int TIMEOUT = 2000;
+
+ private UiDevice mDevice;
+ private Instrumentation mInst;
+ private ContentResolver mResolver;
+
+ public SettingsHelper(UiDevice device, Instrumentation inst) {
+ mDevice = device;
+ mInst = inst;
+ mResolver = inst.getContext().getContentResolver();
+ }
+
+ public static enum SettingsType {
+ SYSTEM,
+ SECURE,
+ GLOBAL
+ }
+
+ public static void launchSettingsPage(Context ctx, String pageName) throws Exception {
+ Intent intent = new Intent(pageName);
+ intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ ctx.startActivity(intent);
+ Thread.sleep(TIMEOUT * 2);
+ }
+
+ public void clickSetting(String settingName) {
+ mDevice.wait(Until.findObject(By.text(settingName)), TIMEOUT).click();
+ }
+
+ public void clickSetting(Pattern settingName) {
+ mDevice.wait(Until.findObject(By.text(settingName)), TIMEOUT).click();
+ }
+
+ public void scrollVert(boolean isUp) {
+ int w = mDevice.getDisplayWidth();
+ int h = mDevice.getDisplayHeight();
+ mDevice.swipe(w / 2, h / 2, w / 2, isUp ? h : 0, 2);
+ }
+
+ public boolean verifyToggleSetting(SettingsType type, String settingAction,
+ String settingName, String internalName) throws Exception {
+ return verifyToggleSetting(
+ type, settingAction, Pattern.compile(settingName), internalName, true);
+ }
+
+ public boolean verifyToggleSetting(SettingsType type, String settingAction,
+ Pattern settingName, String internalName) throws Exception {
+ return verifyToggleSetting(type, settingAction, settingName, internalName, true);
+ }
+
+ public boolean verifyToggleSetting(SettingsType type, String settingAction,
+ String settingName, String internalName, boolean doLaunch) throws Exception {
+ return verifyToggleSetting(
+ type, settingAction, Pattern.compile(settingName), internalName, doLaunch);
+ }
+
+ public boolean verifyToggleSetting(SettingsType type, String settingAction,
+ Pattern settingName, String internalName, boolean doLaunch) throws Exception {
+ int onSetting = Integer.parseInt(getStringSetting(type, internalName));
+ if (doLaunch) {
+ launchSettingsPage(mInst.getContext(), settingAction);
+ }
+ clickSetting(settingName);
+ Thread.sleep(1000);
+ String changedSetting = getStringSetting(type, internalName);
+ return (1 - onSetting) == Integer.parseInt(changedSetting);
+ }
+
+ public boolean verifyRadioSetting(SettingsType type, String settingAction,
+ String baseName, String settingName,
+ String internalName, String testVal) throws Exception {
+ clickSetting(baseName);
+ clickSetting(settingName);
+ Thread.sleep(500);
+ return getStringSetting(type, internalName).equals(testVal);
+ }
+
+ private void putStringSetting(SettingsType type, String sName, String value) {
+ switch (type) {
+ case SYSTEM:
+ Settings.System.putString(mResolver, sName, value); break;
+ case GLOBAL:
+ Settings.Global.putString(mResolver, sName, value); break;
+ case SECURE:
+ Settings.Secure.putString(mResolver, sName, value); break;
+ }
+ }
+
+ private String getStringSetting(SettingsType type, String sName) {
+ switch (type) {
+ case SYSTEM:
+ return Settings.System.getString(mResolver, sName);
+ case GLOBAL:
+ return Settings.Global.getString(mResolver, sName);
+ case SECURE:
+ return Settings.Secure.getString(mResolver, sName);
+ }
+ return "";
+ }
+
+ private void putIntSetting(SettingsType type, String sName, int value) {
+ switch (type) {
+ case SYSTEM:
+ Settings.System.putInt(mResolver, sName, value); break;
+ case GLOBAL:
+ Settings.Global.putInt(mResolver, sName, value); break;
+ case SECURE:
+ Settings.Secure.putInt(mResolver, sName, value); break;
+ }
+ }
+
+ private int getIntSetting(SettingsType type, String sName) throws SettingNotFoundException {
+ switch (type) {
+ case SYSTEM:
+ return Settings.System.getInt(mResolver, sName);
+ case GLOBAL:
+ return Settings.Global.getInt(mResolver, sName);
+ case SECURE:
+ return Settings.Secure.getInt(mResolver, sName);
+ }
+ return Integer.MIN_VALUE;
+ }
+}
diff --git a/tests/functional/settingstests/src/com/android/settings/functional/SoundSettingsTest.java b/tests/functional/settingstests/src/com/android/settings/functional/SoundSettingsTest.java
new file mode 100644
index 0000000..ee4952c
--- /dev/null
+++ b/tests/functional/settingstests/src/com/android/settings/functional/SoundSettingsTest.java
@@ -0,0 +1,277 @@
+package android.settings.functional;
+
+import android.app.NotificationManager;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.os.Handler;
+import android.os.SystemClock;
+import android.provider.Settings;
+import android.service.notification.ZenModeConfig;
+import android.platform.test.helpers.SettingsHelperImpl;
+import android.platform.test.helpers.SettingsHelperImpl.SettingsType;
+import android.support.test.uiautomator.By;
+import android.support.test.uiautomator.UiDevice;
+import android.support.test.uiautomator.Until;
+import android.test.InstrumentationTestCase;
+import android.test.suitebuilder.annotation.MediumTest;
+import android.test.suitebuilder.annotation.Suppress;
+
+import com.android.server.notification.ConditionProviders;
+import com.android.server.notification.ManagedServices.UserProfiles;
+import com.android.server.notification.ZenModeHelper;
+
+public class SoundSettingsTest extends InstrumentationTestCase {
+ private static final String PAGE = Settings.ACTION_SOUND_SETTINGS;
+ private static final int TIMEOUT = 2000;
+
+ private UiDevice mDevice;
+ private ContentResolver mResolver;
+ private SettingsHelperImpl mHelper;
+ private ZenModeHelper mZenHelper;
+
+ @Override
+ public void setUp() throws Exception {
+ super.setUp();
+ mDevice = UiDevice.getInstance(getInstrumentation());
+ mDevice.setOrientationNatural();
+ mResolver = getInstrumentation().getContext().getContentResolver();
+ mHelper = new SettingsHelperImpl(getInstrumentation());
+ ConditionProviders cps = new ConditionProviders(
+ getInstrumentation().getContext(), new Handler(), new UserProfiles());
+ mZenHelper = new ZenModeHelper(getInstrumentation().getContext(),
+ getInstrumentation().getContext().getMainLooper(),
+ cps);
+ }
+
+ @Override
+ public void tearDown() throws Exception {
+ mDevice.unfreezeRotation();
+ super.tearDown();
+ }
+
+ @MediumTest
+ public void testCallVibrate() throws Exception {
+ assertTrue(mHelper.verifyToggleSetting(SettingsType.SYSTEM, PAGE,
+ "Also vibrate for calls", Settings.System.VIBRATE_WHEN_RINGING));
+ assertTrue(mHelper.verifyToggleSetting(SettingsType.SYSTEM, PAGE,
+ "Also vibrate for calls", Settings.System.VIBRATE_WHEN_RINGING));
+ }
+
+ @MediumTest
+ public void testOtherSounds() throws Exception {
+ SettingsHelperImpl.launchSettingsPage(getInstrumentation().getContext(), PAGE);
+ mHelper.scrollVert(false);
+ Thread.sleep(1000);
+ mHelper.clickSetting("Other sounds");
+ Thread.sleep(1000);
+ try {
+ assertTrue("Dial pad tones not toggled", mHelper.verifyToggleSetting(
+ SettingsType.SYSTEM, PAGE, "Dial pad tones",
+ Settings.System.DTMF_TONE_WHEN_DIALING));
+ assertTrue("Screen locking sounds not toggled",
+ mHelper.verifyToggleSetting(SettingsType.SYSTEM, PAGE,
+ "Screen locking sounds", Settings.System.LOCKSCREEN_SOUNDS_ENABLED));
+ assertTrue("Charging sounds not toggled",
+ mHelper.verifyToggleSetting(SettingsType.GLOBAL, PAGE,
+ "Charging sounds", Settings.Global.CHARGING_SOUNDS_ENABLED));
+ assertTrue("Touch sounds not toggled",
+ mHelper.verifyToggleSetting(SettingsType.SYSTEM, PAGE,
+ "Touch sounds", Settings.System.SOUND_EFFECTS_ENABLED));
+ assertTrue("Vibrate on tap not toggled",
+ mHelper.verifyToggleSetting(SettingsType.SYSTEM, PAGE,
+ "Vibrate on tap", Settings.System.HAPTIC_FEEDBACK_ENABLED));
+ } finally {
+ mDevice.pressBack();
+ }
+ }
+
+ @MediumTest
+ @Suppress
+ public void testDndPriorityAllows() throws Exception {
+ SettingsHelperImpl.launchSettingsPage(getInstrumentation().getContext(), PAGE);
+ Context ctx = getInstrumentation().getContext();
+ try {
+ mHelper.clickSetting("Do not disturb");
+ try {
+ mHelper.clickSetting("Priority only allows");
+ ZenModeConfig baseZenCfg = mZenHelper.getConfig();
+
+ mHelper.clickSetting("Reminders");
+ mHelper.clickSetting("Events");
+ mHelper.clickSetting("Repeat callers");
+
+ ZenModeConfig changedCfg = mZenHelper.getConfig();
+ assertFalse(baseZenCfg.allowReminders == changedCfg.allowReminders);
+ assertFalse(baseZenCfg.allowEvents == changedCfg.allowEvents);
+ assertFalse(baseZenCfg.allowRepeatCallers == changedCfg.allowRepeatCallers);
+
+ mHelper.clickSetting("Reminders");
+ mHelper.clickSetting("Events");
+ mHelper.clickSetting("Repeat callers");
+
+ changedCfg = mZenHelper.getConfig();
+ assertTrue(baseZenCfg.allowReminders == changedCfg.allowReminders);
+ assertTrue(baseZenCfg.allowEvents == changedCfg.allowEvents);
+ assertTrue(baseZenCfg.allowRepeatCallers == changedCfg.allowRepeatCallers);
+
+ mHelper.clickSetting("Messages");
+ mHelper.clickSetting("From anyone");
+ mHelper.clickSetting("Calls");
+ mHelper.clickSetting("From anyone");
+
+ changedCfg = mZenHelper.getConfig();
+ assertFalse(baseZenCfg.allowCallsFrom == changedCfg.allowCallsFrom);
+ assertFalse(baseZenCfg.allowMessagesFrom == changedCfg.allowMessagesFrom);
+ } finally {
+ mDevice.pressBack();
+ }
+ } finally {
+ mDevice.pressHome();
+ }
+ }
+
+ @MediumTest
+ @Suppress
+ public void testDndVisualInterruptions() throws Exception {
+ SettingsHelper.launchSettingsPage(getInstrumentation().getContext(), PAGE);
+ try {
+ mHelper.clickSetting("Do not disturb");
+ try {
+ mHelper.clickSetting("Visual interruptions");
+ ZenModeConfig baseZenCfg = mZenHelper.getConfig();
+
+ mHelper.clickSetting("Block when screen is on");
+ mHelper.clickSetting("Block when screen is off");
+
+ ZenModeConfig changedCfg = mZenHelper.getConfig();
+ assertFalse(baseZenCfg.allowWhenScreenOff == changedCfg.allowWhenScreenOff);
+ assertFalse(baseZenCfg.allowWhenScreenOn == changedCfg.allowWhenScreenOn);
+
+ mHelper.clickSetting("Block when screen is on");
+ mHelper.clickSetting("Block when screen is off");
+
+ changedCfg = mZenHelper.getConfig();
+ assertTrue(baseZenCfg.allowWhenScreenOff == changedCfg.allowWhenScreenOff);
+ assertTrue(baseZenCfg.allowWhenScreenOn == changedCfg.allowWhenScreenOn);
+ } finally {
+ mDevice.pressBack();
+ }
+ } finally {
+ mDevice.pressBack();
+ }
+ }
+
+ /*
+ * Rather than verifying every ringtone, verify the ones least likely to change
+ * (None and Hangouts) and an arbitrary one from the ringtone pool.
+ */
+ @MediumTest
+ public void testPhoneRingtoneNone() throws Exception {
+ SettingsHelper.launchSettingsPage(getInstrumentation().getContext(), PAGE);
+ mHelper.clickSetting("Phone ringtone");
+ verifyRingtone(new RingtoneSetting("None", "null"),
+ Settings.System.RINGTONE, ScrollDir.UP);
+ }
+
+ @MediumTest
+ @Suppress
+ public void testPhoneRingtoneHangouts() throws Exception {
+ SettingsHelper.launchSettingsPage(getInstrumentation().getContext(), PAGE);
+ mHelper.clickSetting("Phone ringtone");
+ verifyRingtone(new RingtoneSetting("Hangouts Call", "31"), Settings.System.RINGTONE);
+ }
+
+ @MediumTest
+ public void testPhoneRingtoneUmbriel() throws Exception {
+ SettingsHelper.launchSettingsPage(getInstrumentation().getContext(), PAGE);
+ mHelper.clickSetting("Phone ringtone");
+ verifyRingtone(new RingtoneSetting("Umbriel", "49"),
+ Settings.System.RINGTONE, ScrollDir.DOWN);
+ }
+
+ @MediumTest
+ public void testNotificationRingtoneNone() throws Exception {
+ SettingsHelper.launchSettingsPage(getInstrumentation().getContext(), PAGE);
+ mHelper.clickSetting("Default notification ringtone");
+ verifyRingtone(new RingtoneSetting("None", "null"),
+ Settings.System.NOTIFICATION_SOUND, ScrollDir.UP);
+ }
+
+ @MediumTest
+ @Suppress
+ public void testNotificationRingtoneHangouts() throws Exception {
+ SettingsHelper.launchSettingsPage(getInstrumentation().getContext(), PAGE);
+ mHelper.clickSetting("Default notification ringtone");
+ verifyRingtone(new RingtoneSetting("Hangouts Message", "30"),
+ Settings.System.NOTIFICATION_SOUND);
+ }
+
+ @MediumTest
+ public void testNotificationRingtoneTitan() throws Exception {
+ SettingsHelper.launchSettingsPage(getInstrumentation().getContext(), PAGE);
+ mHelper.clickSetting("Default notification ringtone");
+ verifyRingtone(new RingtoneSetting("Titan", "35"),
+ Settings.System.NOTIFICATION_SOUND, ScrollDir.DOWN);
+ }
+
+ @MediumTest
+ public void testAlarmRingtoneNone() throws Exception {
+ SettingsHelper.launchSettingsPage(getInstrumentation().getContext(), PAGE);
+ mHelper.clickSetting("Default alarm ringtone");
+ verifyRingtone(new RingtoneSetting("None", "null"),
+ Settings.System.ALARM_ALERT, ScrollDir.UP);
+ }
+
+ @MediumTest
+ public void testAlarmRingtoneXenon() throws Exception {
+ SettingsHelper.launchSettingsPage(getInstrumentation().getContext(), PAGE);
+ mHelper.clickSetting("Default alarm ringtone");
+ verifyRingtone(new RingtoneSetting("Xenon", "22"),
+ Settings.System.ALARM_ALERT, ScrollDir.DOWN);
+ }
+
+ private void verifyRingtone(RingtoneSetting r, String settingName) {
+ verifyRingtone(r, settingName, ScrollDir.NOSCROLL);
+ }
+
+ private void verifyRingtone(RingtoneSetting r, String settingName, ScrollDir dir) {
+ if (dir != ScrollDir.NOSCROLL) {
+ mHelper.scrollVert(dir == ScrollDir.UP);
+ SystemClock.sleep(1000);
+ }
+ mDevice.wait(Until.findObject(By.text(r.getName())), TIMEOUT).click();
+ mDevice.wait(Until.findObject(By.text("OK")), TIMEOUT).click();
+ SystemClock.sleep(1000);
+ if (r.getVal().equals("null")) {
+ assertEquals(null,
+ Settings.System.getString(mResolver, settingName));
+ } else if (r.getName().contains("Hangouts")) {
+ assertEquals("content://media/external/audio/media/" + r.getVal(),
+ Settings.System.getString(mResolver, settingName));
+ } else {
+ assertEquals("content://media/internal/audio/media/" + r.getVal(),
+ Settings.System.getString(mResolver, settingName));
+ }
+ }
+
+ private enum ScrollDir {
+ UP,
+ DOWN,
+ NOSCROLL
+ }
+
+ class RingtoneSetting {
+ private final String mName;
+ private final String mMediaVal;
+ public RingtoneSetting(String name, String fname) {
+ mName = name;
+ mMediaVal = fname;
+ }
+ public String getName() {
+ return mName;
+ }
+ public String getVal() {
+ return mMediaVal;
+ }
+ }
+}
diff --git a/tests/functional/settingstests/src/com/android/settings/functional/WirelessNetworkSettingsTests.java b/tests/functional/settingstests/src/com/android/settings/functional/WirelessNetworkSettingsTests.java
new file mode 100644
index 0000000..fca0149
--- /dev/null
+++ b/tests/functional/settingstests/src/com/android/settings/functional/WirelessNetworkSettingsTests.java
@@ -0,0 +1,760 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.settings.functional;
+
+import android.content.Context;
+import android.net.wifi.WifiManager;
+import android.os.RemoteException;
+import android.provider.Settings;
+import android.platform.test.helpers.SettingsHelperImpl;
+import android.support.test.uiautomator.By;
+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.UiObject2;
+import android.support.test.uiautomator.Until;
+import android.test.InstrumentationTestCase;
+import android.test.suitebuilder.annotation.MediumTest;
+
+public class WirelessNetworkSettingsTests extends InstrumentationTestCase {
+ // These back button presses are performed in tearDown() to exit Wifi
+ // Settings sub-menus that a test might finish in. This number should be
+ // high enough to account for the deepest sub-menu a test might enter.
+ private static final int NUM_BACK_BUTTON_PRESSES = 5;
+ private static final int TIMEOUT = 2000;
+ private static final int SLEEP_TIME = 500;
+
+ // Note: The values of these variables might affect flakiness in tests that involve
+ // scrolling. Adjust where necessary.
+ private static final float SCROLL_UP_PERCENT = 10.0f;
+ private static final float SCROLL_DOWN_PERCENT = 0.5f;
+ private static final int MAX_SCROLL_ATTEMPTS = 10;
+ private static final int MAX_ADD_NETWORK_BUTTON_ATTEMPTS = 3;
+ private static final int SCROLL_SPEED = 2000;
+
+ private static final String TEST_SSID = "testSsid";
+ private static final String TEST_PW_GE_8_CHAR = "testPasswordGreaterThan8Char";
+ private static final String TEST_PW_LT_8_CHAR = "lt8Char";
+ private static final String TEST_DOMAIN = "testDomain.com";
+
+ private static final String SETTINGS_PACKAGE = "com.android.settings";
+
+ private static final String CHECKBOX_CLASS = "android.widget.CheckBox";
+ private static final String SPINNER_CLASS = "android.widget.Spinner";
+ private static final String EDIT_TEXT_CLASS = "android.widget.EditText";
+ private static final String SCROLLVIEW_CLASS = "android.widget.ScrollView";
+ private static final String LISTVIEW_CLASS = "android.widget.ListView";
+
+ private static final String ADD_NETWORK_MENU_CANCEL_BUTTON_TEXT = "CANCEL";
+ private static final String ADD_NETWORK_MENU_SAVE_BUTTON_TEXT = "SAVE";
+ private static final String ADD_NETWORK_PREFERENCE_TEXT = "Add network";
+ private static final String CACERT_MENU_PLEASE_SELECT_TEXT = "Please select";
+ private static final String CACERT_MENU_USE_SYSTEM_CERTS_TEXT = "Use system certificates";
+ private static final String CACERT_MENU_DO_NOT_VALIDATE_TEXT = "Do not validate";
+ private static final String USERCERT_MENU_PLEASE_SELECT_TEXT = "Please select";
+ private static final String USERCERT_MENU_DO_NOT_PROVIDE_TEXT = "Do not provide";
+ private static final String SECURITY_OPTION_NONE_TEXT = "None";
+ private static final String SECURITY_OPTION_WEP_TEXT = "WEP";
+ private static final String SECURITY_OPTION_PSK_TEXT = "WPA/WPA2 PSK";
+ private static final String SECURITY_OPTION_EAP_TEXT = "802.1x EAP";
+ private static final String EAP_METHOD_PEAP_TEXT = "PEAP";
+ private static final String EAP_METHOD_TLS_TEXT = "TLS";
+ private static final String EAP_METHOD_TTLS_TEXT = "TTLS";
+ private static final String EAP_METHOD_PWD_TEXT = "PWD";
+ private static final String EAP_METHOD_SIM_TEXT = "SIM";
+ private static final String EAP_METHOD_AKA_TEXT = "AKA";
+ private static final String EAP_METHOD_AKA_PRIME_TEXT = "AKA'";
+ private static final String PHASE2_MENU_NONE_TEXT = "None";
+ private static final String PHASE2_MENU_MSCHAPV2_TEXT = "MSCHAPV2";
+ private static final String PHASE2_MENU_GTC_TEXT = "GTC";
+
+ private static final String ADD_NETWORK_MENU_ADV_TOGGLE_RES_ID = "wifi_advanced_togglebox";
+ private static final String ADD_NETWORK_MENU_IP_SETTINGS_RES_ID = "ip_settings";
+ private static final String ADD_NETWORK_MENU_PROXY_SETTINGS_RES_ID = "proxy_settings";
+ private static final String ADD_NETWORK_MENU_SECURITY_OPTION_RES_ID = "security";
+ private static final String ADD_NETWORK_MENU_EAP_METHOD_RES_ID = "method";
+ private static final String ADD_NETWORK_MENU_SSID_RES_ID = "ssid";
+ private static final String ADD_NETWORK_MENU_PHASE2_RES_ID = "phase2";
+ private static final String ADD_NETWORK_MENU_CACERT_RES_ID = "ca_cert";
+ private static final String ADD_NETWORK_MENU_USERCERT_RES_ID = "user_cert";
+ private static final String ADD_NETWORK_MENU_NO_DOMAIN_WARNING_RES_ID = "no_domain_warning";
+ private static final String ADD_NETWORK_MENU_NO_CACERT_WARNING_RES_ID = "no_ca_cert_warning";
+ private static final String ADD_NETWORK_MENU_DOMAIN_LAYOUT_RES_ID = "l_domain";
+ private static final String ADD_NETWORK_MENU_DOMAIN_RES_ID = "domain";
+ private static final String ADD_NETWORK_MENU_IDENTITY_LAYOUT_RES_ID = "l_identity";
+ private static final String ADD_NETWORK_MENU_ANONYMOUS_LAYOUT_RES_ID = "l_anonymous";
+ private static final String ADD_NETWORK_MENU_PASSWORD_LAYOUT_RES_ID = "password_layout";
+ private static final String ADD_NETWORK_MENU_SHOW_PASSWORD_LAYOUT_RES_ID =
+ "show_password_layout";
+ private static final String ADD_NETWORK_MENU_PASSWORD_RES_ID = "password";
+
+ private static final BySelector ADD_NETWORK_MENU_SCROLLABLE_BY_SELECTOR =
+ By.scrollable(true).clazz(SCROLLVIEW_CLASS);
+ private static final BySelector SPINNER_OPTIONS_SCROLLABLE_BY_SELECTOR =
+ By.scrollable(true).clazz(LISTVIEW_CLASS);
+
+ private UiDevice mDevice;
+
+ @Override
+ public void setUp() throws Exception {
+ super.setUp();
+ mDevice = UiDevice.getInstance(getInstrumentation());
+ try {
+ mDevice.setOrientationNatural();
+ } catch (RemoteException e) {
+ throw new RuntimeException("failed to freeze device orientation", e);
+ }
+ }
+
+ @Override
+ protected void tearDown() throws Exception {
+ // Exit all settings sub-menus.
+ for (int i = 0; i < NUM_BACK_BUTTON_PRESSES; ++i) {
+ mDevice.pressBack();
+ }
+ mDevice.pressHome();
+ super.tearDown();
+ }
+
+ @MediumTest
+ public void testWiFiEnabled() throws Exception {
+ verifyWiFiOnOrOff(true);
+ }
+
+ @MediumTest
+ public void testWiFiDisabled() throws Exception {
+ verifyWiFiOnOrOff(false);
+ }
+
+ @MediumTest
+ public void testWifiMenuLoadConfigure() throws Exception {
+ loadWiFiConfigureMenu();
+ Thread.sleep(SLEEP_TIME);
+ UiObject2 configureWiFiHeading = mDevice.wait(Until.findObject(By.text("Configure Wi‑Fi")),
+ TIMEOUT);
+ assertNotNull("Configure WiFi menu has not loaded correctly", configureWiFiHeading);
+ }
+
+ @MediumTest
+ public void testNetworkNotificationsOn() throws Exception {
+ verifyNetworkNotificationsOnOrOff(true);
+ }
+
+ @MediumTest
+ public void testNetworkNotificationsOff() throws Exception {
+ verifyNetworkNotificationsOnOrOff(false);
+ }
+
+ @MediumTest
+ public void testKeepWiFiDuringSleepAlways() throws Exception {
+ // Change the default and then change it back
+ Settings.Global.putInt(getInstrumentation().getContext().getContentResolver(),
+ Settings.Global.WIFI_SLEEP_POLICY, Settings.Global.WIFI_SLEEP_POLICY_DEFAULT);
+ verifyKeepWiFiOnDuringSleep("Always", Settings.Global.WIFI_SLEEP_POLICY_NEVER);
+ }
+
+ @MediumTest
+ public void testKeepWiFiDuringSleepOnlyWhenPluggedIn() throws Exception {
+ verifyKeepWiFiOnDuringSleep("Only when plugged in",
+ Settings.Global.WIFI_SLEEP_POLICY_NEVER_WHILE_PLUGGED);
+ }
+
+ @MediumTest
+ public void testKeepWiFiDuringSleepNever() throws Exception {
+ verifyKeepWiFiOnDuringSleep("Never", Settings.Global.WIFI_SLEEP_POLICY_DEFAULT);
+ }
+
+ @MediumTest
+ public void testAddNetworkMenu_Default() throws Exception {
+ loadAddNetworkMenu();
+
+ // Submit button should be disabled by default, while cancel button should be enabled.
+ assertFalse(mDevice.wait(Until.findObject(
+ By.text(ADD_NETWORK_MENU_SAVE_BUTTON_TEXT)), TIMEOUT).isEnabled());
+ assertTrue(mDevice.wait(Until.findObject(
+ By.text(ADD_NETWORK_MENU_CANCEL_BUTTON_TEXT)), TIMEOUT).isEnabled());
+
+ // Check that the SSID field is defaults to the hint.
+ assertEquals("Enter the SSID", mDevice.wait(Until.findObject(By
+ .res(SETTINGS_PACKAGE, ADD_NETWORK_MENU_SSID_RES_ID)
+ .clazz(EDIT_TEXT_CLASS)), TIMEOUT)
+ .getText());
+
+ // Check Security defaults to None.
+ assertEquals("None", mDevice.wait(Until.findObject(By
+ .res(SETTINGS_PACKAGE, ADD_NETWORK_MENU_SECURITY_OPTION_RES_ID)
+ .clazz(SPINNER_CLASS)), TIMEOUT)
+ .getChildren().get(0).getText());
+
+ // Check advanced options are collapsed by default.
+ assertFalse(mDevice.wait(Until.findObject(By
+ .res(SETTINGS_PACKAGE, ADD_NETWORK_MENU_ADV_TOGGLE_RES_ID)
+ .clazz(CHECKBOX_CLASS)), TIMEOUT).isChecked());
+
+ }
+
+ @MediumTest
+ public void testAddNetworkMenu_Proxy() throws Exception {
+ loadAddNetworkMenu();
+
+ // Toggle advanced options.
+ mDevice.wait(Until.findObject(By
+ .res(SETTINGS_PACKAGE, ADD_NETWORK_MENU_ADV_TOGGLE_RES_ID)
+ .clazz(CHECKBOX_CLASS)), TIMEOUT).click();
+
+ // Verify Proxy defaults to None.
+ BySelector proxySettingsBySelector =
+ By.res(SETTINGS_PACKAGE, ADD_NETWORK_MENU_PROXY_SETTINGS_RES_ID)
+ .clazz(SPINNER_CLASS);
+ findOrScrollToObject(ADD_NETWORK_MENU_SCROLLABLE_BY_SELECTOR, proxySettingsBySelector);
+ assertEquals("None", mDevice.wait(Until.findObject(proxySettingsBySelector), TIMEOUT)
+ .getChildren().get(0).getText());
+
+ // Verify that Proxy Manual fields appear.
+ findOrScrollToObject(ADD_NETWORK_MENU_SCROLLABLE_BY_SELECTOR, proxySettingsBySelector);
+ mDevice.wait(Until.findObject(proxySettingsBySelector), TIMEOUT).click();
+ mDevice.wait(Until.findObject(By.text("Manual")), TIMEOUT).click();
+ findOrScrollToObject(ADD_NETWORK_MENU_SCROLLABLE_BY_SELECTOR,
+ By.res(SETTINGS_PACKAGE, "proxy_warning_limited_support"));
+ findOrScrollToObject(ADD_NETWORK_MENU_SCROLLABLE_BY_SELECTOR,
+ By.res(SETTINGS_PACKAGE, "proxy_hostname"));
+ findOrScrollToObject(ADD_NETWORK_MENU_SCROLLABLE_BY_SELECTOR,
+ By.res(SETTINGS_PACKAGE, "proxy_exclusionlist"));
+
+ // Verify that Proxy Auto-Config options appear.
+ findOrScrollToObject(ADD_NETWORK_MENU_SCROLLABLE_BY_SELECTOR, proxySettingsBySelector);
+ mDevice.wait(Until.findObject(proxySettingsBySelector), TIMEOUT).click();
+ mDevice.wait(Until.findObject(By.text("Proxy Auto-Config")), TIMEOUT).click();
+ findOrScrollToObject(ADD_NETWORK_MENU_SCROLLABLE_BY_SELECTOR,
+ By.res(SETTINGS_PACKAGE, "proxy_pac"));
+ }
+
+ @MediumTest
+ public void testAddNetworkMenu_IpSettings() throws Exception {
+ loadAddNetworkMenu();
+
+ // Toggle advanced options.
+ mDevice.wait(Until.findObject(By
+ .res(SETTINGS_PACKAGE, ADD_NETWORK_MENU_ADV_TOGGLE_RES_ID)
+ .clazz(CHECKBOX_CLASS)), TIMEOUT).click();
+
+ // Verify IP settings defaults to DHCP.
+ BySelector ipSettingsBySelector =
+ By.res(SETTINGS_PACKAGE, ADD_NETWORK_MENU_IP_SETTINGS_RES_ID).clazz(SPINNER_CLASS);
+ findOrScrollToObject(ADD_NETWORK_MENU_SCROLLABLE_BY_SELECTOR, ipSettingsBySelector);
+ assertEquals("DHCP", mDevice.wait(Until.findObject(ipSettingsBySelector), TIMEOUT)
+ .getChildren().get(0).getText());
+
+ // Verify that Static IP settings options appear.
+ findOrScrollToObject(ADD_NETWORK_MENU_SCROLLABLE_BY_SELECTOR, ipSettingsBySelector).click();
+ mDevice.wait(Until.findObject(By.text("Static")), TIMEOUT).click();
+ findOrScrollToObject(ADD_NETWORK_MENU_SCROLLABLE_BY_SELECTOR,
+ By.res(SETTINGS_PACKAGE, "ipaddress"));
+ findOrScrollToObject(ADD_NETWORK_MENU_SCROLLABLE_BY_SELECTOR,
+ By.res(SETTINGS_PACKAGE, "gateway"));
+ findOrScrollToObject(ADD_NETWORK_MENU_SCROLLABLE_BY_SELECTOR,
+ By.res(SETTINGS_PACKAGE, "network_prefix_length"));
+ findOrScrollToObject(ADD_NETWORK_MENU_SCROLLABLE_BY_SELECTOR,
+ By.res(SETTINGS_PACKAGE, "dns1"));
+ findOrScrollToObject(ADD_NETWORK_MENU_SCROLLABLE_BY_SELECTOR,
+ By.res(SETTINGS_PACKAGE, "dns2"));
+ }
+
+ @MediumTest
+ public void testPhase2Settings() throws Exception {
+ loadAddNetworkMenu();
+ selectSecurityOption(SECURITY_OPTION_EAP_TEXT);
+
+ BySelector phase2SettingsBySelector =
+ By.res(SETTINGS_PACKAGE, ADD_NETWORK_MENU_PHASE2_RES_ID).clazz(SPINNER_CLASS);
+ findOrScrollToObject(ADD_NETWORK_MENU_SCROLLABLE_BY_SELECTOR, phase2SettingsBySelector);
+ assertEquals(PHASE2_MENU_NONE_TEXT, mDevice.wait(Until
+ .findObject(phase2SettingsBySelector), TIMEOUT).getChildren().get(0).getText());
+ mDevice.wait(Until.findObject(phase2SettingsBySelector), TIMEOUT).click();
+ Thread.sleep(SLEEP_TIME);
+
+ // Verify Phase 2 authentication spinner options.
+ assertNotNull(mDevice.wait(Until.findObject(By.text(PHASE2_MENU_NONE_TEXT)), TIMEOUT));
+ assertNotNull(mDevice.wait(Until.findObject(By.text(PHASE2_MENU_MSCHAPV2_TEXT)), TIMEOUT));
+ assertNotNull(mDevice.wait(Until.findObject(By.text(PHASE2_MENU_GTC_TEXT)), TIMEOUT));
+ }
+
+ @MediumTest
+ public void testCaCertSettings() throws Exception {
+ loadAddNetworkMenu();
+ selectSecurityOption(SECURITY_OPTION_EAP_TEXT);
+
+ BySelector caCertSettingsBySelector =
+ By.res(SETTINGS_PACKAGE, ADD_NETWORK_MENU_CACERT_RES_ID).clazz(SPINNER_CLASS);
+ findOrScrollToObject(ADD_NETWORK_MENU_SCROLLABLE_BY_SELECTOR, caCertSettingsBySelector);
+ assertEquals(CACERT_MENU_PLEASE_SELECT_TEXT, mDevice.wait(Until
+ .findObject(caCertSettingsBySelector), TIMEOUT).getChildren().get(0).getText());
+ mDevice.wait(Until.findObject(caCertSettingsBySelector), TIMEOUT).click();
+ Thread.sleep(SLEEP_TIME);
+
+ // Verify CA certificate spinner options.
+ assertNotNull(mDevice.wait(Until.findObject(
+ By.text(CACERT_MENU_PLEASE_SELECT_TEXT)), TIMEOUT));
+ assertNotNull(mDevice.wait(Until.findObject(
+ By.text(CACERT_MENU_USE_SYSTEM_CERTS_TEXT)), TIMEOUT));
+ assertNotNull(mDevice.wait(Until.findObject(
+ By.text(CACERT_MENU_DO_NOT_VALIDATE_TEXT)), TIMEOUT));
+
+ // Verify that a domain field and warning appear when the user selects the
+ // "Use system certificates" option.
+ mDevice.wait(Until.findObject(By.text(CACERT_MENU_USE_SYSTEM_CERTS_TEXT)), TIMEOUT).click();
+ findOrScrollToObject(ADD_NETWORK_MENU_SCROLLABLE_BY_SELECTOR,
+ By.res(SETTINGS_PACKAGE, ADD_NETWORK_MENU_DOMAIN_LAYOUT_RES_ID));
+ findOrScrollToObject(ADD_NETWORK_MENU_SCROLLABLE_BY_SELECTOR,
+ By.res(SETTINGS_PACKAGE, ADD_NETWORK_MENU_NO_DOMAIN_WARNING_RES_ID));
+
+ // Verify that a warning appears when the user chooses the "Do Not Validate" option.
+ mDevice.wait(Until.findObject(caCertSettingsBySelector), TIMEOUT).click();
+ mDevice.wait(Until.findObject(By.text(CACERT_MENU_DO_NOT_VALIDATE_TEXT)), TIMEOUT).click();
+ findOrScrollToObject(ADD_NETWORK_MENU_SCROLLABLE_BY_SELECTOR,
+ By.res(SETTINGS_PACKAGE, ADD_NETWORK_MENU_NO_CACERT_WARNING_RES_ID));
+ }
+
+ @MediumTest
+ public void testAddNetwork_NoSecurity() throws Exception {
+ loadAddNetworkMenu();
+ selectSecurityOption(SECURITY_OPTION_NONE_TEXT);
+
+ // Entering an SSID is enough to enable the submit button. // TODO THIS GUY
+ enterSSID(TEST_SSID);
+ assertTrue(mDevice.wait(Until
+ .findObject(By.text(ADD_NETWORK_MENU_SAVE_BUTTON_TEXT)), TIMEOUT).isEnabled());
+ }
+
+ @MediumTest
+ public void testAddNetwork_WEP() throws Exception {
+ loadAddNetworkMenu();
+ selectSecurityOption(SECURITY_OPTION_WEP_TEXT);
+ assertFalse(mDevice.wait(Until.findObject(
+ By.text(ADD_NETWORK_MENU_SAVE_BUTTON_TEXT)), TIMEOUT).isEnabled());
+
+ // Verify that WEP fields appear.
+ findOrScrollToObject(ADD_NETWORK_MENU_SCROLLABLE_BY_SELECTOR,
+ By.res(SETTINGS_PACKAGE, ADD_NETWORK_MENU_PASSWORD_LAYOUT_RES_ID));
+ findOrScrollToObject(ADD_NETWORK_MENU_SCROLLABLE_BY_SELECTOR,
+ By.res(SETTINGS_PACKAGE, ADD_NETWORK_MENU_SHOW_PASSWORD_LAYOUT_RES_ID));
+
+ // Entering an SSID alone does not enable the submit button.
+ enterSSID(TEST_SSID);
+ assertFalse(mDevice.wait(Until.findObject(
+ By.text(ADD_NETWORK_MENU_SAVE_BUTTON_TEXT)), TIMEOUT).isEnabled());
+
+ // Submit button is only enabled after a password is entered.
+ enterPassword(TEST_PW_GE_8_CHAR);
+ assertTrue(mDevice.wait(Until
+ .findObject(By.text(ADD_NETWORK_MENU_SAVE_BUTTON_TEXT)), TIMEOUT).isEnabled());
+ }
+
+ @MediumTest
+ public void testAddNetwork_PSK() throws Exception {
+ loadAddNetworkMenu();
+ selectSecurityOption(SECURITY_OPTION_PSK_TEXT);
+ assertFalse(mDevice.wait(Until.findObject(
+ By.text(ADD_NETWORK_MENU_SAVE_BUTTON_TEXT)), TIMEOUT).isEnabled());
+
+ // Verify that PSK fields appear.
+ findOrScrollToObject(ADD_NETWORK_MENU_SCROLLABLE_BY_SELECTOR,
+ By.res(SETTINGS_PACKAGE, ADD_NETWORK_MENU_PASSWORD_LAYOUT_RES_ID));
+ findOrScrollToObject(ADD_NETWORK_MENU_SCROLLABLE_BY_SELECTOR,
+ By.res(SETTINGS_PACKAGE, ADD_NETWORK_MENU_SHOW_PASSWORD_LAYOUT_RES_ID));
+
+ // Entering an SSID alone does not enable the submit button.
+ enterSSID(TEST_SSID);
+ assertFalse(mDevice.wait(Until.findObject(
+ By.text(ADD_NETWORK_MENU_SAVE_BUTTON_TEXT)), TIMEOUT).isEnabled());
+
+ // Entering an password that is too short does not enable submit button.
+ enterPassword(TEST_PW_LT_8_CHAR);
+ assertFalse(mDevice.wait(Until.findObject(
+ By.text(ADD_NETWORK_MENU_SAVE_BUTTON_TEXT)), TIMEOUT).isEnabled());
+
+ // Submit button is only enabled after a password of valid length is entered.
+ enterPassword(TEST_PW_GE_8_CHAR);
+ assertTrue(mDevice.wait(Until.findObject(
+ By.text(ADD_NETWORK_MENU_SAVE_BUTTON_TEXT)), TIMEOUT).isEnabled());
+ }
+
+ @MediumTest
+ public void testAddNetwork_EAP_PEAP() throws Exception {
+ loadAddNetworkMenu();
+ selectSecurityOption(SECURITY_OPTION_EAP_TEXT);
+ assertFalse(mDevice.wait(Until.findObject(
+ By.text(ADD_NETWORK_MENU_SAVE_BUTTON_TEXT)), TIMEOUT).isEnabled());
+
+ selectEAPMethod(EAP_METHOD_PEAP_TEXT);
+
+ // Verify that EAP-PEAP fields appear.
+ findOrScrollToObject(ADD_NETWORK_MENU_SCROLLABLE_BY_SELECTOR,
+ By.res(SETTINGS_PACKAGE, ADD_NETWORK_MENU_PHASE2_RES_ID));
+ findOrScrollToObject(ADD_NETWORK_MENU_SCROLLABLE_BY_SELECTOR,
+ By.res(SETTINGS_PACKAGE, ADD_NETWORK_MENU_CACERT_RES_ID));
+ findOrScrollToObject(ADD_NETWORK_MENU_SCROLLABLE_BY_SELECTOR,
+ By.res(SETTINGS_PACKAGE, ADD_NETWORK_MENU_IDENTITY_LAYOUT_RES_ID));
+ findOrScrollToObject(ADD_NETWORK_MENU_SCROLLABLE_BY_SELECTOR,
+ By.res(SETTINGS_PACKAGE, ADD_NETWORK_MENU_ANONYMOUS_LAYOUT_RES_ID));
+ findOrScrollToObject(ADD_NETWORK_MENU_SCROLLABLE_BY_SELECTOR,
+ By.res(SETTINGS_PACKAGE, ADD_NETWORK_MENU_PASSWORD_LAYOUT_RES_ID));
+ findOrScrollToObject(ADD_NETWORK_MENU_SCROLLABLE_BY_SELECTOR,
+ By.res(SETTINGS_PACKAGE, ADD_NETWORK_MENU_SHOW_PASSWORD_LAYOUT_RES_ID));
+
+ // Entering an SSID alone does not enable the submit button.
+ enterSSID(TEST_SSID);
+ assertFalse(mDevice.wait(Until.findObject(
+ By.text(ADD_NETWORK_MENU_SAVE_BUTTON_TEXT)), TIMEOUT).isEnabled());
+
+ verifyCaCertificateSubmitConditions();
+ }
+
+ @MediumTest
+ public void testAddNetwork_EAP_TLS() throws Exception {
+ loadAddNetworkMenu();
+ selectSecurityOption(SECURITY_OPTION_EAP_TEXT);
+ assertFalse(mDevice.wait(Until.findObject(
+ By.text(ADD_NETWORK_MENU_SAVE_BUTTON_TEXT)), TIMEOUT).isEnabled());
+
+ selectEAPMethod(EAP_METHOD_TLS_TEXT);
+
+ // Verify that EAP-TLS fields appear.
+ findOrScrollToObject(ADD_NETWORK_MENU_SCROLLABLE_BY_SELECTOR,
+ By.res(SETTINGS_PACKAGE, ADD_NETWORK_MENU_CACERT_RES_ID));
+ findOrScrollToObject(ADD_NETWORK_MENU_SCROLLABLE_BY_SELECTOR,
+ By.res(SETTINGS_PACKAGE, ADD_NETWORK_MENU_USERCERT_RES_ID));
+ findOrScrollToObject(ADD_NETWORK_MENU_SCROLLABLE_BY_SELECTOR,
+ By.res(SETTINGS_PACKAGE, ADD_NETWORK_MENU_IDENTITY_LAYOUT_RES_ID));
+
+ // Entering an SSID alone does not enable the submit button.
+ enterSSID(TEST_SSID);
+ assertFalse(mDevice.wait(Until.findObject(
+ By.text(ADD_NETWORK_MENU_SAVE_BUTTON_TEXT)), TIMEOUT).isEnabled());
+
+ // Selecting the User certificate "Do not provide" option alone does not enable the submit
+ // button.
+ selectUserCertificateOption(USERCERT_MENU_DO_NOT_PROVIDE_TEXT);
+ assertFalse(mDevice.wait(Until.findObject(
+ By.text(ADD_NETWORK_MENU_SAVE_BUTTON_TEXT)), TIMEOUT).isEnabled());
+
+ verifyCaCertificateSubmitConditions();
+ }
+
+ @MediumTest
+ public void testAddNetwork_EAP_TTLS() throws Exception {
+ loadAddNetworkMenu();
+ selectSecurityOption(SECURITY_OPTION_EAP_TEXT);
+ assertFalse(mDevice.wait(Until.findObject(
+ By.text(ADD_NETWORK_MENU_SAVE_BUTTON_TEXT)), TIMEOUT).isEnabled());
+
+ selectEAPMethod(EAP_METHOD_TTLS_TEXT);
+
+ // Verify that EAP-TLS fields appear.
+ findOrScrollToObject(ADD_NETWORK_MENU_SCROLLABLE_BY_SELECTOR,
+ By.res(SETTINGS_PACKAGE, ADD_NETWORK_MENU_PHASE2_RES_ID));
+ findOrScrollToObject(ADD_NETWORK_MENU_SCROLLABLE_BY_SELECTOR,
+ By.res(SETTINGS_PACKAGE, ADD_NETWORK_MENU_CACERT_RES_ID));
+ findOrScrollToObject(ADD_NETWORK_MENU_SCROLLABLE_BY_SELECTOR,
+ By.res(SETTINGS_PACKAGE, ADD_NETWORK_MENU_IDENTITY_LAYOUT_RES_ID));
+ findOrScrollToObject(ADD_NETWORK_MENU_SCROLLABLE_BY_SELECTOR,
+ By.res(SETTINGS_PACKAGE, ADD_NETWORK_MENU_ANONYMOUS_LAYOUT_RES_ID));
+ findOrScrollToObject(ADD_NETWORK_MENU_SCROLLABLE_BY_SELECTOR,
+ By.res(SETTINGS_PACKAGE, ADD_NETWORK_MENU_PASSWORD_LAYOUT_RES_ID));
+
+ // Entering an SSID alone does not enable the submit button.
+ enterSSID(TEST_SSID);
+ assertFalse(mDevice.wait(Until.findObject(
+ By.text(ADD_NETWORK_MENU_SAVE_BUTTON_TEXT)), TIMEOUT).isEnabled());
+
+ verifyCaCertificateSubmitConditions();
+ }
+
+ @MediumTest
+ public void testAddNetwork_EAP_PWD() throws Exception {
+ loadAddNetworkMenu();
+ selectSecurityOption(SECURITY_OPTION_EAP_TEXT);
+ assertFalse(mDevice.wait(Until.findObject(
+ By.text(ADD_NETWORK_MENU_SAVE_BUTTON_TEXT)), TIMEOUT).isEnabled());
+
+ selectEAPMethod(EAP_METHOD_PWD_TEXT);
+
+ // Verify that EAP-TLS fields appear.
+ findOrScrollToObject(ADD_NETWORK_MENU_SCROLLABLE_BY_SELECTOR,
+ By.res(SETTINGS_PACKAGE, ADD_NETWORK_MENU_IDENTITY_LAYOUT_RES_ID));
+ findOrScrollToObject(ADD_NETWORK_MENU_SCROLLABLE_BY_SELECTOR,
+ By.res(SETTINGS_PACKAGE, ADD_NETWORK_MENU_PASSWORD_LAYOUT_RES_ID));
+
+ // Entering an SSID alone enables the submit button.
+ enterSSID(TEST_SSID);
+ assertTrue(mDevice.wait(Until.findObject(
+ By.text(ADD_NETWORK_MENU_SAVE_BUTTON_TEXT)), TIMEOUT).isEnabled());
+ }
+
+ @MediumTest
+ public void testAddNetwork_EAP_SIM() throws Exception {
+ loadAddNetworkMenu();
+ selectSecurityOption(SECURITY_OPTION_EAP_TEXT);
+ assertFalse(mDevice.wait(Until.findObject(
+ By.text(ADD_NETWORK_MENU_SAVE_BUTTON_TEXT)), TIMEOUT).isEnabled());
+
+ selectEAPMethod(EAP_METHOD_SIM_TEXT);
+
+ // Entering an SSID alone enables the submit button.
+ enterSSID(TEST_SSID);
+ assertTrue(mDevice.wait(Until.findObject(
+ By.text(ADD_NETWORK_MENU_SAVE_BUTTON_TEXT)), TIMEOUT).isEnabled());
+ }
+
+ @MediumTest
+ public void testAddNetwork_EAP_AKA() throws Exception {
+ loadAddNetworkMenu();
+ selectSecurityOption(SECURITY_OPTION_EAP_TEXT);
+ assertFalse(mDevice.wait(Until.findObject(
+ By.text(ADD_NETWORK_MENU_SAVE_BUTTON_TEXT)), TIMEOUT).isEnabled());
+
+ selectEAPMethod(EAP_METHOD_AKA_TEXT);
+
+ // Entering an SSID alone enables the submit button.
+ enterSSID(TEST_SSID);
+ assertTrue(mDevice.wait(Until.findObject(
+ By.text(ADD_NETWORK_MENU_SAVE_BUTTON_TEXT)), TIMEOUT).isEnabled());
+ }
+
+ @MediumTest
+ public void testAddNetwork_EAP_AKA_PRIME() throws Exception {
+ loadAddNetworkMenu();
+ selectSecurityOption(SECURITY_OPTION_EAP_TEXT);
+ assertFalse(mDevice.wait(Until.findObject(
+ By.text(ADD_NETWORK_MENU_SAVE_BUTTON_TEXT)), TIMEOUT).isEnabled());
+
+ selectEAPMethod(EAP_METHOD_AKA_PRIME_TEXT);
+
+ // Entering an SSID alone enables the submit button.
+ enterSSID(TEST_SSID);
+ assertTrue(mDevice.wait(Until.findObject(
+ By.text(ADD_NETWORK_MENU_SAVE_BUTTON_TEXT)), TIMEOUT).isEnabled());
+ }
+
+ private void verifyKeepWiFiOnDuringSleep(String settingToBeVerified, int settingValue)
+ throws Exception {
+ loadWiFiConfigureMenu();
+ mDevice.wait(Until.findObject(By.text("Keep Wi‑Fi on during sleep")), TIMEOUT)
+ .click();
+ mDevice.wait(Until.findObject(By.clazz("android.widget.CheckedTextView")
+ .text(settingToBeVerified)), TIMEOUT).click();
+ Thread.sleep(SLEEP_TIME);
+ int keepWiFiOnSetting =
+ Settings.Global.getInt(getInstrumentation().getContext().getContentResolver(),
+ Settings.Global.WIFI_SLEEP_POLICY);
+ assertEquals(settingValue, keepWiFiOnSetting);
+ }
+
+ private void verifyNetworkNotificationsOnOrOff(boolean verifyOn)
+ throws Exception {
+ String switchText = "ON";
+ if (verifyOn) {
+ switchText = "OFF";
+ Settings.Global.putString(getInstrumentation().getContext().getContentResolver(),
+ Settings.Global.WIFI_NETWORKS_AVAILABLE_NOTIFICATION_ON, "0");
+ }
+ else {
+ Settings.Global.putString(getInstrumentation().getContext().getContentResolver(),
+ Settings.Global.WIFI_NETWORKS_AVAILABLE_NOTIFICATION_ON, "1");
+ }
+ loadWiFiConfigureMenu();
+ mDevice.wait(Until.findObject(By.res("android:id/switch_widget").text(switchText)), TIMEOUT)
+ .click();
+ Thread.sleep(SLEEP_TIME);
+ String wifiNotificationValue =
+ Settings.Global.getString(getInstrumentation().getContext().getContentResolver(),
+ Settings.Global.WIFI_NETWORKS_AVAILABLE_NOTIFICATION_ON);
+ if (verifyOn) {
+ assertEquals("1", wifiNotificationValue);
+ }
+ else {
+ assertEquals("0", wifiNotificationValue);
+ }
+ }
+
+ private void verifyWiFiOnOrOff(boolean verifyOn) throws Exception {
+ String switchText = "On";
+ if (verifyOn) {
+ switchText = "Off";
+ }
+ loadWiFiSettingsPage(!verifyOn);
+ mDevice.wait(Until
+ .findObject(By.res(SETTINGS_PACKAGE, "switch_bar").text(switchText)), TIMEOUT)
+ .click();
+ Thread.sleep(SLEEP_TIME);
+ String wifiValue =
+ Settings.Global.getString(getInstrumentation().getContext().getContentResolver(),
+ Settings.Global.WIFI_ON);
+ if (verifyOn) {
+ assertEquals("1", wifiValue);
+ }
+ else {
+ assertEquals("0", wifiValue);
+ }
+ }
+
+ private void verifyCaCertificateSubmitConditions() throws Exception {
+ // Selecting the CA certificate "Do not validate" option enables the submit button.
+ selectCaCertificateOption(CACERT_MENU_DO_NOT_VALIDATE_TEXT);
+ assertTrue(mDevice.wait(Until.findObject(
+ By.text(ADD_NETWORK_MENU_SAVE_BUTTON_TEXT)), TIMEOUT).isEnabled());
+
+ // However, selecting the CA certificate "Use system certificates option" is not enough to
+ // enable the submit button.
+ selectCaCertificateOption(CACERT_MENU_USE_SYSTEM_CERTS_TEXT);
+ assertFalse(mDevice.wait(Until.findObject(
+ By.text(ADD_NETWORK_MENU_SAVE_BUTTON_TEXT)), TIMEOUT).isEnabled());
+
+ // Submit button is only enabled after a domain is entered as well.
+ enterDomain(TEST_DOMAIN);
+ assertTrue(mDevice.wait(Until.findObject(
+ By.text(ADD_NETWORK_MENU_SAVE_BUTTON_TEXT)), TIMEOUT).isEnabled());
+ }
+
+ private void loadWiFiSettingsPage(boolean wifiEnabled) throws Exception {
+ WifiManager wifiManager = (WifiManager)getInstrumentation().getContext()
+ .getSystemService(Context.WIFI_SERVICE);
+ wifiManager.setWifiEnabled(wifiEnabled);
+ SettingsHelper.launchSettingsPage(getInstrumentation().getContext(),
+ Settings.ACTION_WIFI_SETTINGS);
+ }
+
+ private void loadWiFiConfigureMenu() throws Exception {
+ loadWiFiSettingsPage(true);
+ mDevice.wait(Until.findObject(By.desc("Configure")), TIMEOUT).click();
+ }
+
+ private void loadAddNetworkMenu() throws Exception {
+ loadWiFiSettingsPage(true);
+ for (int attempts = 0; attempts < MAX_ADD_NETWORK_BUTTON_ATTEMPTS; ++attempts) {
+ UiObject2 found = null;
+ try {
+ findOrScrollToObject(By.scrollable(true), By.text(ADD_NETWORK_PREFERENCE_TEXT))
+ .click();
+ } catch (StaleObjectException e) {
+ // The network list might have been updated between when the Add network button was
+ // found, and when it UI automator attempted to click on it. Retry.
+ continue;
+ }
+ // If we get here, we successfully clicked on the Add network button, so we are done.
+ // Adding a sleep and a back press to dismiss the IME, as a workaround for
+ // b/28862652
+ Thread.sleep(SLEEP_TIME*5);
+ mDevice.pressBack();
+ return;
+ }
+
+ fail("Failed to load Add Network Menu after " + MAX_ADD_NETWORK_BUTTON_ATTEMPTS
+ + " retries");
+ }
+
+ private void selectSecurityOption(String securityOption) throws Exception {
+ // We might not need to scroll to the security options if not enough add network menu
+ // options are visible.
+ findOrScrollToObject(ADD_NETWORK_MENU_SCROLLABLE_BY_SELECTOR,
+ By.res(SETTINGS_PACKAGE, ADD_NETWORK_MENU_SECURITY_OPTION_RES_ID)
+ .clazz(SPINNER_CLASS)).click();
+ Thread.sleep(SLEEP_TIME);
+ mDevice.wait(Until.findObject(By.text(securityOption)), TIMEOUT).click();
+ }
+
+ private void selectEAPMethod(String eapMethod) throws Exception {
+ findOrScrollToObject(ADD_NETWORK_MENU_SCROLLABLE_BY_SELECTOR,
+ By.res(SETTINGS_PACKAGE, ADD_NETWORK_MENU_EAP_METHOD_RES_ID).clazz(SPINNER_CLASS))
+ .click();
+ Thread.sleep(SLEEP_TIME);
+ findOrScrollToObject(SPINNER_OPTIONS_SCROLLABLE_BY_SELECTOR, By.text(eapMethod)).click();
+ }
+
+ private void selectUserCertificateOption(String userCertificateOption) throws Exception {
+ findOrScrollToObject(ADD_NETWORK_MENU_SCROLLABLE_BY_SELECTOR,
+ By.res(SETTINGS_PACKAGE, ADD_NETWORK_MENU_USERCERT_RES_ID).clazz(SPINNER_CLASS))
+ .click();
+ mDevice.wait(Until.findObject(By.text(userCertificateOption)), TIMEOUT).click();
+ }
+
+ private void selectCaCertificateOption(String caCertificateOption) throws Exception {
+ findOrScrollToObject(ADD_NETWORK_MENU_SCROLLABLE_BY_SELECTOR,
+ By.res(SETTINGS_PACKAGE, ADD_NETWORK_MENU_CACERT_RES_ID).clazz(SPINNER_CLASS))
+ .click();
+ mDevice.wait(Until.findObject(By.text(caCertificateOption)), TIMEOUT).click();
+ }
+
+ private void enterSSID(String ssid) throws Exception {
+ // We might not need to scroll to the SSID option if not enough add network menu options
+ // are visible.
+ findOrScrollToObject(ADD_NETWORK_MENU_SCROLLABLE_BY_SELECTOR,
+ By.res(SETTINGS_PACKAGE, ADD_NETWORK_MENU_SSID_RES_ID).clazz(EDIT_TEXT_CLASS))
+ .setText(ssid);
+ }
+
+ private void enterPassword(String password) throws Exception {
+ findOrScrollToObject(ADD_NETWORK_MENU_SCROLLABLE_BY_SELECTOR,
+ By.res(SETTINGS_PACKAGE, ADD_NETWORK_MENU_PASSWORD_RES_ID).clazz(EDIT_TEXT_CLASS))
+ .setText(password);
+ }
+
+ private void enterDomain(String domain) throws Exception {
+ findOrScrollToObject(ADD_NETWORK_MENU_SCROLLABLE_BY_SELECTOR,
+ By.res(SETTINGS_PACKAGE, ADD_NETWORK_MENU_DOMAIN_RES_ID)).setText(domain);
+ }
+
+ // Use this if the UI object might or might not need to be scrolled to.
+ private UiObject2 findOrScrollToObject(BySelector scrollableSelector, BySelector objectSelector)
+ throws Exception {
+ UiObject2 object = mDevice.wait(Until.findObject(objectSelector), TIMEOUT);
+ if (object == null) {
+ object = scrollToObject(scrollableSelector, objectSelector);
+ }
+ return object;
+ }
+
+ private UiObject2 scrollToObject(BySelector scrollableSelector, BySelector objectSelector)
+ throws Exception {
+ UiObject2 scrollable = mDevice.wait(Until.findObject(scrollableSelector), TIMEOUT);
+ if (scrollable == null) {
+ fail("Could not find scrollable UI object identified by " + scrollableSelector);
+ }
+ UiObject2 found = null;
+ // Scroll all the way up first, then all the way down.
+ while (true) {
+ // Optimization: terminate if we find the object while scrolling up to reset, so
+ // we save the time spent scrolling down again.
+ boolean canScrollAgain = scrollable.scroll(Direction.UP, SCROLL_UP_PERCENT,
+ SCROLL_SPEED);
+ found = mDevice.findObject(objectSelector);
+ if (found != null) return found;
+ if (!canScrollAgain) break;
+ }
+ for (int attempts = 0; found == null && attempts < MAX_SCROLL_ATTEMPTS; ++attempts) {
+ // Return value of UiObject2.scroll() is not reliable, so do not use it in loop
+ // condition, in case it causes this loop to terminate prematurely.
+ scrollable.scroll(Direction.DOWN, SCROLL_DOWN_PERCENT, SCROLL_SPEED);
+ found = mDevice.findObject(objectSelector);
+ }
+ if (found == null) {
+ fail("Could not scroll to UI object identified by " + objectSelector);
+ }
+ return found;
+ }
+}
diff --git a/tests/functional/testapks/applinktestapp/Android.mk b/tests/functional/testapks/applinktestapp/Android.mk
new file mode 100644
index 0000000..08c75f8
--- /dev/null
+++ b/tests/functional/testapks/applinktestapp/Android.mk
@@ -0,0 +1,26 @@
+# Copyright 2016 Google Inc. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT 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 := tests
+
+# omit gradle 'build' dir
+LOCAL_SRC_FILES := $(call all-java-files-under,src)
+
+LOCAL_RESOURCE_DIR := $(LOCAL_PATH)/res
+LOCAL_PACKAGE_NAME := AppLinkTestApp
+LOCAL_CERTIFICATE := platform
+include $(BUILD_PACKAGE)
diff --git a/tests/functional/testapks/applinktestapp/AndroidManifest.xml b/tests/functional/testapks/applinktestapp/AndroidManifest.xml
new file mode 100644
index 0000000..6c0b4ad
--- /dev/null
+++ b/tests/functional/testapks/applinktestapp/AndroidManifest.xml
@@ -0,0 +1,46 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright 2016 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.android.applinktestapp"
+ android:versionCode="1"
+ android:versionName="1.0"
+ android:sharedUserId="com.android.functional.applink" >
+
+ <uses-sdk android:minSdkVersion="19"
+ android:targetSdkVersion="24"/>
+
+ <application
+ android:icon="@mipmap/ic_launcher"
+ android:label="AppLinkTestApp" >
+ <activity android:name=".MainActivity" >
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN" />
+ <category android:name="android.intent.category.LAUNCHER" />
+ </intent-filter>
+ </activity>
+<activity android:name=".MainActivity">
+ <intent-filter>
+ <action android:name="android.intent.action.VIEW" />
+ <category android:name="android.intent.category.DEFAULT" />
+ <category android:name="android.intent.category.BROWSABLE" />
+ <data android:host="youtube.com" />
+ <data android:scheme="http" />
+ <data android:host="test.com" />
+ </intent-filter>
+</activity>
+ </application>
+</manifest>
diff --git a/tests/functional/testapks/applinktestapp/res/layout/activity_main.xml b/tests/functional/testapks/applinktestapp/res/layout/activity_main.xml
new file mode 100644
index 0000000..319faed
--- /dev/null
+++ b/tests/functional/testapks/applinktestapp/res/layout/activity_main.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical" >
+ <TextView android:id="@+id/text"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="App Link Test App" />
+</LinearLayout>
diff --git a/tests/functional/testapks/applinktestapp/res/mipmap-hdpi/ic_launcher.png b/tests/functional/testapks/applinktestapp/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 0000000..cde69bc
--- /dev/null
+++ b/tests/functional/testapks/applinktestapp/res/mipmap-hdpi/ic_launcher.png
Binary files differ
diff --git a/tests/functional/testapks/applinktestapp/src/com/android/applinktestapp/MainActivity.java b/tests/functional/testapks/applinktestapp/src/com/android/applinktestapp/MainActivity.java
new file mode 100644
index 0000000..b69c222
--- /dev/null
+++ b/tests/functional/testapks/applinktestapp/src/com/android/applinktestapp/MainActivity.java
@@ -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.
+ */
+
+package com.android.applinktestapp;
+
+import android.os.Bundle;
+import android.content.Intent;
+import android.app.Activity;
+import android.net.Uri;
+
+public class MainActivity extends Activity {
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_main);
+ }
+}
diff --git a/tests/functional/testapks/permissiontestappmv1/Android.mk b/tests/functional/testapks/permissiontestappmv1/Android.mk
new file mode 100644
index 0000000..0bf4196
--- /dev/null
+++ b/tests/functional/testapks/permissiontestappmv1/Android.mk
@@ -0,0 +1,14 @@
+LOCAL_PATH:= $(call my-dir)
+include $(CLEAR_VARS)
+
+LOCAL_MODULE_TAGS := tests
+
+# omit gradle 'build' dir
+LOCAL_SRC_FILES := $(call all-java-files-under,src)
+
+LOCAL_STATIC_JAVA_LIBRARIES = android-support-v4
+LOCAL_RESOURCE_DIR := \
+ $(LOCAL_PATH)/res
+LOCAL_PACKAGE_NAME := PermissionTestAppMV1
+LOCAL_CERTIFICATE := platform
+include $(BUILD_PACKAGE)
diff --git a/tests/functional/testapks/permissiontestappmv1/AndroidManifest.xml b/tests/functional/testapks/permissiontestappmv1/AndroidManifest.xml
new file mode 100644
index 0000000..cf6d152
--- /dev/null
+++ b/tests/functional/testapks/permissiontestappmv1/AndroidManifest.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.android.permissiontestappmv1"
+ android:versionCode="1"
+ android:versionName="1.0" >
+
+ <uses-sdk android:minSdkVersion="23"
+ android:targetSdkVersion="23"/>
+
+ <uses-permission android:name="android.permission.READ_CONTACTS" />
+ <application
+ android:icon="@mipmap/ic_launcher"
+ android:label="PermissionTestAppMV1" >
+ <activity android:name=".MainActivity" >
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN" />
+ <category android:name="android.intent.category.LAUNCHER" />
+ </intent-filter>
+ </activity>
+ </application>
+
+</manifest>
diff --git a/tests/functional/testapks/permissiontestappmv1/res/layout/activity_main.xml b/tests/functional/testapks/permissiontestappmv1/res/layout/activity_main.xml
new file mode 100644
index 0000000..a298f09
--- /dev/null
+++ b/tests/functional/testapks/permissiontestappmv1/res/layout/activity_main.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical" >
+ <TextView android:id="@+id/text"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="Build Target: M\nVersion: 1.0\nDangerous Permissions:Contacts\nNormal Permission: \n\n\n" />
+ <Button android:id="@+id/buttonGetPermission"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="Get Contact Permission" />
+</LinearLayout>
diff --git a/tests/functional/testapks/permissiontestappmv1/res/mipmap-hdpi/ic_launcher.png b/tests/functional/testapks/permissiontestappmv1/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 0000000..cde69bc
--- /dev/null
+++ b/tests/functional/testapks/permissiontestappmv1/res/mipmap-hdpi/ic_launcher.png
Binary files differ
diff --git a/tests/functional/testapks/permissiontestappmv1/src/com/android/permissontestappmv1/MainActivity.java b/tests/functional/testapks/permissiontestappmv1/src/com/android/permissontestappmv1/MainActivity.java
new file mode 100644
index 0000000..a6cd8a8
--- /dev/null
+++ b/tests/functional/testapks/permissiontestappmv1/src/com/android/permissontestappmv1/MainActivity.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.permissiontestappmv1;
+
+import android.provider.Settings;
+import android.support.v4.app.ActivityCompat;
+import android.support.v4.content.ContextCompat;
+import android.os.Bundle;
+import android.view.inputmethod.InputMethodManager;
+import android.widget.Button;
+import android.view.View;
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.widget.EditText;
+import android.widget.Toast;
+import android.view.inputmethod.InputMethodManager;
+import android.content.ContentResolver;
+import android.Manifest;
+import android.app.Activity;
+
+public class MainActivity extends Activity {
+
+ private static final int READ_PERMISSION_RESULT = 1;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_main);
+
+ final Button buttonGetPermission = (Button) findViewById(R.id.buttonGetPermission);
+ buttonGetPermission.setOnClickListener(new View.OnClickListener() {
+ public void onClick(View v) {
+ showContacts();
+ }
+ });
+ }
+
+ /**
+ * This method asks for 'READ_CONTACTS' permission on button 'Get Contact Permission' press
+ * Method does nothing with Contact content
+ */
+ private void showContacts() {
+ if (ContextCompat.checkSelfPermission(this,
+ Manifest.permission.READ_CONTACTS) != PackageManager.PERMISSION_GRANTED) {
+ if (ActivityCompat.shouldShowRequestPermissionRationale(this,
+ Manifest.permission.READ_CONTACTS)) {
+ }
+ ActivityCompat.requestPermissions(this, new String[] {
+ Manifest.permission.READ_CONTACTS
+ }, READ_PERMISSION_RESULT);
+ return;
+ }
+ }
+}
diff --git a/tests/jank/UbSystemUiJankTests/src/android/platform/systemui/tests/jank/SettingsJankTests.java b/tests/jank/UbSystemUiJankTests/src/android/platform/systemui/tests/jank/SettingsJankTests.java
index b783ce6..e6c7b8e 100644
--- a/tests/jank/UbSystemUiJankTests/src/android/platform/systemui/tests/jank/SettingsJankTests.java
+++ b/tests/jank/UbSystemUiJankTests/src/android/platform/systemui/tests/jank/SettingsJankTests.java
@@ -41,7 +41,8 @@
private static final int TIMEOUT = 5000;
private static final String SETTINGS_PACKAGE = "com.android.settings";
- private static final BySelector SETTINGS_DASHBOARD = By.res(SETTINGS_PACKAGE, "dashboard");
+ private static final BySelector SETTINGS_DASHBOARD = By.res(SETTINGS_PACKAGE,
+ "dashboard_container");
// short transitions should be repeated within the test function, otherwise frame stats
// captured are not really meaningful in a statistical sense
private static final int INNER_LOOP = 2;
@@ -70,7 +71,6 @@
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK); // Clear out any previous instances
context.startActivity(intent);
mDevice.wait(Until.hasObject(By.pkg(SETTINGS_PACKAGE).depth(0)), TIMEOUT);
-
SystemClock.sleep(1000);
}
@@ -82,6 +82,12 @@
public void flingSettingsToStart() throws IOException {
UiObject2 list = mDevice.wait(Until.findObject(SETTINGS_DASHBOARD), TIMEOUT);
+ int count = 0;
+ while (!list.isScrollable() && count <= 5) {
+ mDevice.wait(Until.findObject(By.text("SEE ALL")), TIMEOUT).click();
+ list = mDevice.wait(Until.findObject(SETTINGS_DASHBOARD), TIMEOUT);
+ count++;
+ }
while (list.fling(Direction.UP));
mDevice.waitForIdle();
TimeResultLogger.writeTimeStampLogStart(String.format("%s-%s",
diff --git a/tests/jank/UbSystemUiJankTests/src/android/platform/systemui/tests/jank/SystemUiJankTests.java b/tests/jank/UbSystemUiJankTests/src/android/platform/systemui/tests/jank/SystemUiJankTests.java
index 789461a..94599f9 100644
--- a/tests/jank/UbSystemUiJankTests/src/android/platform/systemui/tests/jank/SystemUiJankTests.java
+++ b/tests/jank/UbSystemUiJankTests/src/android/platform/systemui/tests/jank/SystemUiJankTests.java
@@ -116,8 +116,8 @@
}
// Close any crash dialogs
- while (mDevice.hasObject(By.textContains("has stopped."))) {
- mDevice.findObject(By.text("OK")).clickAndWait(Until.newWindow(), 2000);
+ while (mDevice.hasObject(By.textContains("has stopped"))) {
+ mDevice.findObject(By.text("Close")).clickAndWait(Until.newWindow(), 2000);
}
TimeResultLogger.writeTimeStampLogStart(String.format("%s-%s",
getClass().getSimpleName(), getName()), TIMESTAMP_FILE);
diff --git a/tests/jank/androidtvjanktests/Android.mk b/tests/jank/androidtvjanktests/Android.mk
new file mode 100644
index 0000000..f01074f
--- /dev/null
+++ b/tests/jank/androidtvjanktests/Android.mk
@@ -0,0 +1,26 @@
+# Copyright (C) 2015 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+LOCAL_PATH := $(call my-dir)
+include $(CLEAR_VARS)
+
+LOCAL_PACKAGE_NAME := AndroidTVJankTests
+LOCAL_SRC_FILES := $(call all-java-files-under, src)
+LOCAL_MODULE_TAGS := tests
+
+LOCAL_STATIC_JAVA_LIBRARIES := ub-janktesthelper ub-uiautomator timeresult-helper-lib
+
+LOCAL_SDK_VERSION := 21
+
+include $(BUILD_PACKAGE)
\ No newline at end of file
diff --git a/tests/jank/androidtvjanktests/AndroidManifest.xml b/tests/jank/androidtvjanktests/AndroidManifest.xml
new file mode 100644
index 0000000..bdd3039
--- /dev/null
+++ b/tests/jank/androidtvjanktests/AndroidManifest.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2015 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.android.androidtv.janktests">
+
+ <application>
+ <uses-library android:name="android.test.runner" />
+ </application>
+
+ <uses-sdk android:minSdkVersion="19"
+ android:targetSdkVersion="23"/>
+ <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
+ <instrumentation
+ android:name="android.test.InstrumentationTestRunner"
+ android:targetPackage="com.android.androidtv.janktests"
+ android:label="Platform Android TV Jank Tests" />
+</manifest>
diff --git a/tests/jank/androidtvjanktests/src/com/android/androidtv/janktests/SystemAppJankTests.java b/tests/jank/androidtvjanktests/src/com/android/androidtv/janktests/SystemAppJankTests.java
new file mode 100644
index 0000000..ccd913c
--- /dev/null
+++ b/tests/jank/androidtvjanktests/src/com/android/androidtv/janktests/SystemAppJankTests.java
@@ -0,0 +1,118 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.androidtv.janktests;
+
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.os.Bundle;
+import android.os.SystemClock;
+import android.support.test.jank.GfxMonitor;
+import android.support.test.jank.JankTest;
+import android.support.test.jank.JankTestBase;
+import android.support.test.uiautomator.By;
+import android.support.test.uiautomator.Direction;
+import android.support.test.uiautomator.UiDevice;
+import android.support.test.uiautomator.UiObject2;
+import android.support.test.uiautomator.UiObjectNotFoundException;
+import android.support.test.uiautomator.Until;
+import android.util.Log;
+import java.io.IOException;
+
+/*
+ * This class contains the tests for key system apps on Android TV jank.
+ */
+public class SystemAppJankTests extends JankTestBase {
+
+ private static final int LONG_TIMEOUT = 5000;
+ private static final int INNER_LOOP = 8;
+ private static final int FLING_SPEED = 12000;
+ private static final String YOUTUBE_PACKAGE = "com.google.android.youtube.tv";
+ private UiDevice mDevice;
+
+ @Override
+ public void setUp() {
+ mDevice = UiDevice.getInstance(getInstrumentation());
+ }
+
+ @Override
+ protected void tearDown() throws Exception {
+ super.tearDown();
+ }
+
+ public void afterTestSystemApp(Bundle metrics) throws IOException {
+ mDevice.pressHome();
+ super.afterTest(metrics);
+ }
+
+ public void launchYoutube() throws UiObjectNotFoundException {
+ mDevice.pressHome();
+ launchApp(YOUTUBE_PACKAGE);
+ SystemClock.sleep(LONG_TIMEOUT);
+ // Ensure that Youtube has loaded on Android TV with nav bar in focus
+ UiObject2 youtubeScreen = mDevice.wait(
+ Until.findObject(By.scrollable(true).res(YOUTUBE_PACKAGE, "guide")), LONG_TIMEOUT);
+ }
+
+ // Measures jank while scrolling down the Youtube Navigation Bar
+ @JankTest(expectedFrames=100, beforeTest = "launchYoutube",
+ afterTest="afterTestSystemApp")
+ @GfxMonitor(processName=YOUTUBE_PACKAGE)
+ public void testYoutubeGuideNavigation() throws UiObjectNotFoundException {
+ // As of launching Youtube, we're already at the screen where
+ // the navigation bar is in focus, so we only need to scroll.
+ navigateDownAndUpCurrentScreen();
+ }
+
+ public void goToYoutubeContainer() throws UiObjectNotFoundException {
+ launchYoutube();
+ // Move focus from Youtube navigation bar to content
+ mDevice.pressDPadRight();
+ SystemClock.sleep(LONG_TIMEOUT);
+ // Ensure that Youtube content is in focus
+ UiObject2 youtubeScreen = mDevice.wait( Until.findObject(By.scrollable(true)
+ .res(YOUTUBE_PACKAGE, "container_list")), LONG_TIMEOUT);
+ }
+
+ // Measures jank while scrolling down the Youtube Navigation Bar
+ @JankTest(expectedFrames=100, beforeTest = "goToYoutubeContainer",
+ afterTest="afterTestSystemApp")
+ @GfxMonitor(processName=YOUTUBE_PACKAGE)
+ public void testYoutubeContainerListNavigation() throws UiObjectNotFoundException {
+ // The gotoYouTubeContainer method confirms that the focus is
+ // on the content, so we only need to scroll.
+ navigateDownAndUpCurrentScreen();
+ }
+
+ public void navigateDownAndUpCurrentScreen() {
+ for (int i = 0; i < INNER_LOOP; i++) {
+ // Press DPad button down eight times in succession to scroll down.
+ mDevice.pressDPadDown();
+ }
+ for (int i = 0; i < INNER_LOOP; i++) {
+ // Press DPad button up eight times in succession to scroll up.
+ mDevice.pressDPadUp();
+ }
+ }
+
+ public void launchApp(String packageName) throws UiObjectNotFoundException {
+ PackageManager pm = getInstrumentation().getContext().getPackageManager();
+ Intent appIntent = pm.getLaunchIntentForPackage(packageName);
+ appIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ getInstrumentation().getContext().startActivity(appIntent);
+ SystemClock.sleep(LONG_TIMEOUT);
+ }
+}
\ No newline at end of file
diff --git a/tests/jank/androidtvjanktests/src/com/android/androidtv/janktests/SystemUiJankTests.java b/tests/jank/androidtvjanktests/src/com/android/androidtv/janktests/SystemUiJankTests.java
new file mode 100644
index 0000000..444c19b
--- /dev/null
+++ b/tests/jank/androidtvjanktests/src/com/android/androidtv/janktests/SystemUiJankTests.java
@@ -0,0 +1,143 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.androidtv.janktests;
+
+import android.os.Bundle;
+import android.os.SystemClock;
+import android.support.test.jank.GfxMonitor;
+import android.support.test.jank.JankTest;
+import android.support.test.jank.JankTestBase;
+import android.support.test.uiautomator.By;
+import android.support.test.uiautomator.Direction;
+import android.support.test.uiautomator.UiDevice;
+import android.support.test.uiautomator.UiObject2;
+import android.support.test.uiautomator.UiObjectNotFoundException;
+import android.support.test.uiautomator.Until;
+import android.util.Log;
+import java.io.IOException;
+
+/*
+ * This class contains the tests for Android TV jank.
+ */
+public class SystemUiJankTests extends JankTestBase {
+
+ private static final int SHORT_TIMEOUT = 1000;
+ private static final int LONG_TIMEOUT = 3000;
+ private static final int INNER_LOOP = 4;
+ private static final int FLING_SPEED = 12000;
+ private static final String LEANBACK_LAUNCHER = "com.google.android.leanbacklauncher";
+ private static final String SETTINGS_PACKAGE = "com.android.tv.settings";
+ private UiDevice mDevice;
+
+ @Override
+ public void setUp() {
+ mDevice = UiDevice.getInstance(getInstrumentation());
+ }
+
+ @Override
+ protected void tearDown() throws Exception {
+ super.tearDown();
+ }
+
+ public void goHome() {
+ mDevice.pressHome();
+ // Ensure that Home screen is being displayed
+ UiObject2 homeScreen = mDevice.wait(
+ Until.findObject(By.scrollable(true).res(LEANBACK_LAUNCHER, "main_list_view")),
+ SHORT_TIMEOUT);
+ }
+
+ public void afterTestHomeScreenNavigation(Bundle metrics) throws IOException {
+ super.afterTest(metrics);
+ }
+
+ // Measures jank while scrolling down the Home screen
+ @JankTest(expectedFrames=100, beforeTest = "goHome",
+ afterTest="afterTestHomeScreenNavigation")
+ @GfxMonitor(processName=LEANBACK_LAUNCHER)
+ public void testHomeScreenNavigation() throws UiObjectNotFoundException {
+ // We've already verified that Home screen is being displayed.
+ // Scroll up and down the home screen.
+ navigateDownAndUpCurrentScreen();
+ }
+
+ // Navigates to the Settings row on the Home screen
+ public void goToSettingsRow() {
+ // Navigate to Home screen and verify that it is being displayed.
+ goHome();
+ mDevice.wait(Until.findObject(By.scrollable(true).res(LEANBACK_LAUNCHER, "main_list_view")),
+ SHORT_TIMEOUT);
+ // Look for the row with 'Settings' text.
+ // This will ensure that the DPad focus is on the Settings icon.
+ int count = 0;
+ while (count <= 5 && !(mDevice.hasObject(By.res(LEANBACK_LAUNCHER, "label")
+ .text("Settings")))) {
+ mDevice.pressDPadDown();
+ count++;
+ }
+ if (!mDevice.hasObject(By.res(LEANBACK_LAUNCHER, "label").text("Settings"))) {
+ Log.d(LEANBACK_LAUNCHER, "Couldn't navigate to settings");
+ }
+ }
+
+ public void afterTestSettings(Bundle metrics) throws IOException {
+ // Navigate back home
+ goHome();
+ super.afterTest(metrics);
+ }
+
+ // Measures jank while navigating to Settings from Home and back
+ @JankTest(expectedFrames=100, beforeTest="goToSettingsRow",
+ afterTest="afterTestSettings")
+ @GfxMonitor(processName=SETTINGS_PACKAGE)
+ public void testNavigateToSettings() throws UiObjectNotFoundException {
+ for (int i = 0; i < INNER_LOOP * 10; i++) {
+ // Press DPad center button to navigate to settings.
+ mDevice.pressDPadCenter();
+ // Press Back button to go back to the Home screen with focus on Settings
+ mDevice.pressBack();
+ }
+ }
+
+ // Navigates to the Settings Screen
+ public void goToSettings() {
+ goToSettingsRow();
+ mDevice.pressDPadCenter();
+ }
+
+ // Measures jank while scrolling on the Settings screen
+ @JankTest(expectedFrames=100, beforeTest="goToSettings",
+ afterTest="afterTestSettings")
+ @GfxMonitor(processName=SETTINGS_PACKAGE)
+ public void testSettingsScreenNavigation() throws UiObjectNotFoundException {
+ // Ensure that Settings screen is being displayed
+ mDevice.wait(Until.findObject(By.scrollable(true).res(SETTINGS_PACKAGE, "container_list")),
+ SHORT_TIMEOUT);
+ navigateDownAndUpCurrentScreen();
+ }
+
+ public void navigateDownAndUpCurrentScreen() {
+ for (int i = 0; i < INNER_LOOP; i++) {
+ // Press DPad button down eight times in succession
+ mDevice.pressDPadDown();
+ }
+ for (int i = 0; i < INNER_LOOP; i++) {
+ // Press DPad button up eight times in succession.
+ mDevice.pressDPadUp();
+ }
+ }
+}
diff --git a/tests/jank/dialer/AndroidManifest.xml b/tests/jank/dialer/AndroidManifest.xml
index 3973f4f..b59e090 100644
--- a/tests/jank/dialer/AndroidManifest.xml
+++ b/tests/jank/dialer/AndroidManifest.xml
@@ -16,12 +16,12 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.android.dialer.janktests">
-
<uses-sdk android:minSdkVersion="22" />
<uses-permission android:name="android.permission.READ_CONTACTS" />
<uses-permission android:name="android.permission.WRITE_CONTACTS" />
<uses-permission android:name="android.permission.READ_CALL_LOG" />
<uses-permission android:name="android.permission.WRITE_CALL_LOG" />
+ <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<application>
<uses-library android:name="android.test.runner" />
diff --git a/tests/jank/dialer/src/com/android/dialer/janktests/DialerJankTests.java b/tests/jank/dialer/src/com/android/dialer/janktests/DialerJankTests.java
index e252e28..61af03e 100644
--- a/tests/jank/dialer/src/com/android/dialer/janktests/DialerJankTests.java
+++ b/tests/jank/dialer/src/com/android/dialer/janktests/DialerJankTests.java
@@ -97,8 +97,7 @@
mDevice.waitForIdle();
// Open contacts list
- UiObject2 contacts = mDevice.wait(Until.findObject(
- By.clazz(View.class).desc("Contacts")), TIMEOUT);
+ UiObject2 contacts = mDevice.wait(Until.findObject(By.desc("Contacts")), TIMEOUT);
assertNotNull("Contacts can't be found", contacts);
contacts.clickAndWait(Until.newWindow(), TIMEOUT);
// Find a contact by a given contact-name
@@ -141,19 +140,16 @@
}
launchApp(PACKAGE_NAME);
mDevice.waitForIdle();
- // Find recents and click
- mDevice.wait(Until.findObject(By.clazz(View.class).desc("Recents")), TIMEOUT).click();
- // to ensure enough record for fling, expand full call-history
- mDevice.wait(Until.findObject(
- By.res(RES_PACKAGE_NAME,"lists_pager")), TIMEOUT).fling(Direction.DOWN);
- mDevice.wait(Until.findObject(By.text("View full call history")), TIMEOUT).click();
+ // Find 'Call History' and click
+ mDevice.wait(Until.findObject(By.desc("Call History")), TIMEOUT).click();
+ mDevice.wait(Until.findObject(By.res(RES_PACKAGE_NAME,"lists_pager")), TIMEOUT);
}
@JankTest(beforeTest="launchCallLog", expectedFrames=EXPECTED_FRAMES)
@GfxMonitor(processName=PACKAGE_NAME)
public void testDialerCallLogFling() {
UiObject2 callLog = mDevice.wait(Until.findObject(
- By.res(RES_PACKAGE_NAME, "call_log_pager")), TIMEOUT);
+ By.res(RES_PACKAGE_NAME,"lists_pager")), TIMEOUT);
assertNotNull("Call log can't be found", callLog);
for (int i = 0; i < INNER_LOOP; i++) {
callLog.fling(Direction.DOWN);
diff --git a/tests/jank/jankmicrobenchmark/Android.mk b/tests/jank/jankmicrobenchmark/Android.mk
index 4819ef2..d0be009 100644
--- a/tests/jank/jankmicrobenchmark/Android.mk
+++ b/tests/jank/jankmicrobenchmark/Android.mk
@@ -21,6 +21,7 @@
LOCAL_STATIC_JAVA_LIBRARIES := ub-uiautomator ub-janktesthelper
-LOCAK_SDK_VERSION := current
+LOCAL_SDK_VERSION := current
include $(BUILD_PACKAGE)
+
diff --git a/tests/jank/jankmicrobenchmark/AndroidManifest.xml b/tests/jank/jankmicrobenchmark/AndroidManifest.xml
index 4ce5e54..d856757 100644
--- a/tests/jank/jankmicrobenchmark/AndroidManifest.xml
+++ b/tests/jank/jankmicrobenchmark/AndroidManifest.xml
@@ -16,7 +16,8 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.android.jankmicrobenchmark.janktests">
-
+ <uses-sdk android:minSdkVersion="23" android:targetSdkVersion="23" />
+ <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<application>
<uses-library android:name="android.test.runner" />
</application>
diff --git a/tests/jank/jankmicrobenchmark/src/com/android/jankmicrobenchmark/janktests/ApiDemoJankTests.java b/tests/jank/jankmicrobenchmark/src/com/android/jankmicrobenchmark/janktests/ApiDemoJankTests.java
index ec2bca5..9b8a9bb 100644
--- a/tests/jank/jankmicrobenchmark/src/com/android/jankmicrobenchmark/janktests/ApiDemoJankTests.java
+++ b/tests/jank/jankmicrobenchmark/src/com/android/jankmicrobenchmark/janktests/ApiDemoJankTests.java
@@ -45,7 +45,9 @@
private static final int EXPECTED_FRAMES = 100;
private static final String PACKAGE_NAME = "com.example.android.apis";
private static final String RES_PACKAGE_NAME = "android";
+ private static final String LEANBACK_LAUNCHER = "com.google.android.leanbacklauncher";
private UiDevice mDevice;
+ private UiObject2 mListView;
@Override
public void setUp() throws Exception {
@@ -60,14 +62,32 @@
super.tearDown();
}
- public void launchApiDemos() {
+ // This method distinguishes between home screen for handheld devices
+ // and home screen for Android TV, both of whom have different Home elements.
+ public UiObject2 getHomeScreen() throws UiObjectNotFoundException {
+ if (mDevice.getProductName().equals("fugu")) {
+ return mDevice.wait(Until.findObject(By.res(LEANBACK_LAUNCHER, "main_list_view")),
+ LONG_TIMEOUT);
+ }
+ else {
+ String launcherPackage = mDevice.getLauncherPackageName();
+ return mDevice.wait(Until.findObject(By.res(launcherPackage,"workspace")),
+ LONG_TIMEOUT);
+ }
+ }
+
+ public void launchApiDemos() throws UiObjectNotFoundException {
+ UiObject2 homeScreen = getHomeScreen();
+ if (homeScreen == null)
+ navigateToHome();
Intent intent = getInstrumentation().getContext().getPackageManager()
.getLaunchIntentForPackage(PACKAGE_NAME);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
getInstrumentation().getContext().startActivity(intent);
mDevice.waitForIdle();
}
- public void selectAnimation(String optionName) {
+
+ public void selectAnimation(String optionName) throws UiObjectNotFoundException {
launchApiDemos();
UiObject2 animation = mDevice.wait(Until.findObject(
By.res(RES_PACKAGE_NAME, "text1").text("Animation")), LONG_TIMEOUT);
@@ -78,7 +98,7 @@
int maxAttempt = 3;
while (option == null && maxAttempt > 0) {
mDevice.wait(Until.findObject(By.res(RES_PACKAGE_NAME, "content")), LONG_TIMEOUT)
- .scroll(Direction.DOWN, 1.0f);
+ .scroll(Direction.DOWN, 1.0f);
option = mDevice.wait(Until.findObject(By.res(RES_PACKAGE_NAME, "text1")
.text(optionName)), LONG_TIMEOUT);
--maxAttempt;
@@ -87,31 +107,41 @@
option.click();
}
+ // Since afterTest only runs when the test has passed, there's no way of going
+ // back to the Home Screen if a test fails. This method is a workaround. A feature
+ // request has been filed to have a per test tearDown method - b/25673300
+ public void navigateToHome() throws UiObjectNotFoundException {
+ UiObject2 homeScreen = getHomeScreen();
+ int count = 0;
+ while (homeScreen == null && count <= 10) {
+ mDevice.pressBack();
+ homeScreen = getHomeScreen();
+ count++;
+ }
+ Assert.assertNotNull("Hit maximum retries and couldn't find Home Screen", homeScreen);
+ }
+
// Since the app doesn't start at the first page when reloaded after the first time,
// ensuring that we head back to the first screen before going Home so we're always
// on screen one.
public void goBackHome(Bundle metrics) throws UiObjectNotFoundException {
- String launcherPackage = mDevice.getLauncherPackageName();
- UiObject2 homeScreen = mDevice.findObject(By.res(launcherPackage,"workspace"));
- while (homeScreen == null) {
- mDevice.pressBack();
- homeScreen = mDevice.findObject(By.res(launcherPackage,"workspace"));
- }
+ navigateToHome();
super.afterTest(metrics);
}
// Loads the 'activity transition' animation
public void selectActivityTransitionAnimation() throws UiObjectNotFoundException {
- selectAnimation("Activity Transition");
+ selectAnimation("Activity Transition");
}
// Measures jank for activity transition animation
@JankTest(beforeTest="selectActivityTransitionAnimation", afterTest="goBackHome",
- expectedFrames=EXPECTED_FRAMES)
+ expectedFrames=EXPECTED_FRAMES)
@GfxMonitor(processName=PACKAGE_NAME)
public void testActivityTransitionAnimation() {
for (int i = 0; i < INNER_LOOP; i++) {
- UiObject2 redBallTile = mDevice.findObject(By.res(PACKAGE_NAME, "ball"));
+ UiObject2 redBallTile = mDevice.wait(Until.findObject(By.res(PACKAGE_NAME, "ball")),
+ LONG_TIMEOUT);
redBallTile.click();
SystemClock.sleep(LONG_TIMEOUT);
mDevice.pressBack();
@@ -125,7 +155,7 @@
// Measures jank for view flip animation
@JankTest(beforeTest="selectViewFlipAnimation", afterTest="goBackHome",
- expectedFrames=EXPECTED_FRAMES)
+ expectedFrames=EXPECTED_FRAMES)
@GfxMonitor(processName=PACKAGE_NAME)
public void testViewFlipAnimation() {
for (int i = 0; i < INNER_LOOP; i++) {
@@ -142,7 +172,7 @@
// Measures jank for cloning animation
@JankTest(beforeTest="selectCloningAnimation", afterTest="goBackHome",
- expectedFrames=EXPECTED_FRAMES)
+ expectedFrames=EXPECTED_FRAMES)
@GfxMonitor(processName=PACKAGE_NAME)
public void testCloningAnimation() {
for (int i = 0; i < INNER_LOOP; i++) {
@@ -159,11 +189,11 @@
// Measures jank for 'loading' animation
@JankTest(beforeTest="selectLoadingOption", afterTest="goBackHome",
- expectedFrames=EXPECTED_FRAMES)
+ expectedFrames=EXPECTED_FRAMES)
@GfxMonitor(processName=PACKAGE_NAME)
public void testLoadingJank() {
UiObject2 runButton = mDevice.wait(Until.findObject(
- By.res(PACKAGE_NAME, "startButton").text("Run")), LONG_TIMEOUT);
+ By.res(PACKAGE_NAME, "startButton").text("RUN")), LONG_TIMEOUT);
Assert.assertNotNull("Run button is null", runButton);
for (int i = 0; i < INNER_LOOP; i++) {
runButton.click();
@@ -178,7 +208,7 @@
// Measures jank for 'simple transition' animation
@JankTest(beforeTest="selectSimpleTransitionOption", afterTest="goBackHome",
- expectedFrames=EXPECTED_FRAMES)
+ expectedFrames=EXPECTED_FRAMES)
@GfxMonitor(processName=PACKAGE_NAME)
public void testSimpleTransitionJank() {
for (int i = 0; i < INNER_LOOP; i++) {
@@ -203,12 +233,12 @@
// Measures jank for 'hide/show' animation
@JankTest(beforeTest="selectHideShowAnimationOption", afterTest="goBackHome",
- expectedFrames=EXPECTED_FRAMES)
+ expectedFrames=EXPECTED_FRAMES)
@GfxMonitor(processName=PACKAGE_NAME)
public void testHideShowAnimationJank() {
for (int i = 0; i < INNER_LOOP; i++) {
UiObject2 showButton = mDevice.wait(Until.findObject(By.res(
- PACKAGE_NAME, "addNewButton").text("Show Buttons")), LONG_TIMEOUT);
+ PACKAGE_NAME, "addNewButton").text("SHOW BUTTONS")), LONG_TIMEOUT);
Assert.assertNotNull("'Show Buttons' button can't be found", showButton);
showButton.click();
SystemClock.sleep(SHORT_TIMEOUT);
@@ -239,7 +269,7 @@
}
}
- public void selectViews(String optionName) {
+ public void selectViews(String optionName) throws UiObjectNotFoundException {
launchApiDemos();
UiObject2 views = null;
short maxAttempt = 4;
@@ -248,7 +278,7 @@
.text("Views")), LONG_TIMEOUT);
if (views == null) {
mDevice.wait(Until.findObject(By.res(RES_PACKAGE_NAME, "content")), LONG_TIMEOUT)
- .scroll(Direction.DOWN, 1.0f);
+ .scroll(Direction.DOWN, 1.0f);
}
--maxAttempt;
}
@@ -277,20 +307,20 @@
By.res(RES_PACKAGE_NAME, "text1").text("01. Array")), LONG_TIMEOUT);
Assert.assertNotNull("Array listview can't be found", array);
array.click();
+ mListView = mDevice.wait(Until.findObject(By.res(
+ RES_PACKAGE_NAME, "content")), LONG_TIMEOUT);
+ Assert.assertNotNull("Content pane isn't found to move up", mListView);
}
// Measures jank for simple listview fling
@JankTest(beforeTest="selectListsArray", afterTest="goBackHome",
- expectedFrames=EXPECTED_FRAMES)
+ expectedFrames=EXPECTED_FRAMES)
@GfxMonitor(processName=PACKAGE_NAME)
public void testListViewJank() {
for (int i = 0; i < INNER_LOOP; i++) {
- UiObject2 listView = mDevice.wait(Until.findObject(By.res(
- RES_PACKAGE_NAME, "content")), LONG_TIMEOUT);
- Assert.assertNotNull("Content pane isn't found to move up", listView);
- listView.fling(Direction.DOWN);
+ mListView.fling(Direction.DOWN);
SystemClock.sleep(SHORT_TIMEOUT);
- listView.fling(Direction.UP);
+ mListView.fling(Direction.UP);
SystemClock.sleep(SHORT_TIMEOUT);
}
}
@@ -307,36 +337,36 @@
// Measures jank for simple expandable list view expansion
// Expansion group1, group3 and group4 arbitrarily selected
@JankTest(beforeTest="selectExpandableListsSimpleAdapter", afterTest="goBackHome",
- expectedFrames=EXPECTED_FRAMES)
+ expectedFrames=EXPECTED_FRAMES)
@GfxMonitor(processName=PACKAGE_NAME)
public void testExapandableListViewJank() {
for (int i = 0; i < INNER_LOOP; i++) {
- UiObject2 group1 = mDevice.wait(Until.findObject(By.res(
- RES_PACKAGE_NAME, "text1").text("Group 1")), LONG_TIMEOUT);
- Assert.assertNotNull("Group 1 isn't found to be expanded", group1);
- group1.click();
- SystemClock.sleep(SHORT_TIMEOUT);
- group1.click();
- SystemClock.sleep(SHORT_TIMEOUT);
- UiObject2 group3 = mDevice.wait(Until.findObject(By.res(
- RES_PACKAGE_NAME, "text1").text("Group 3")), LONG_TIMEOUT);
- Assert.assertNotNull("Group 3 isn't found to be expanded", group3);
- group3.click();
- SystemClock.sleep(SHORT_TIMEOUT);
- group3.click();
- SystemClock.sleep(SHORT_TIMEOUT);
- UiObject2 group4 = mDevice.wait(Until.findObject(By.res(
- RES_PACKAGE_NAME, "text1").text("Group 4")), LONG_TIMEOUT);
- Assert.assertNotNull("Group 4 isn't found to be expanded", group4);
- group4.click();
- SystemClock.sleep(SHORT_TIMEOUT);
- group4.click();
- SystemClock.sleep(SHORT_TIMEOUT);
- UiObject2 content = mDevice.wait(Until.findObject(By.res(
- RES_PACKAGE_NAME, "content")), LONG_TIMEOUT);
- Assert.assertNotNull("Content pane isn't found to move up", content);
- content.fling(Direction.UP);
- SystemClock.sleep(SHORT_TIMEOUT);
+ UiObject2 group1 = mDevice.wait(Until.findObject(By.res(
+ RES_PACKAGE_NAME, "text1").text("Group 1")), LONG_TIMEOUT);
+ Assert.assertNotNull("Group 1 isn't found to be expanded", group1);
+ group1.click();
+ SystemClock.sleep(SHORT_TIMEOUT);
+ group1.click();
+ SystemClock.sleep(SHORT_TIMEOUT);
+ UiObject2 group3 = mDevice.wait(Until.findObject(By.res(
+ RES_PACKAGE_NAME, "text1").text("Group 3")), LONG_TIMEOUT);
+ Assert.assertNotNull("Group 3 isn't found to be expanded", group3);
+ group3.click();
+ SystemClock.sleep(SHORT_TIMEOUT);
+ group3.click();
+ SystemClock.sleep(SHORT_TIMEOUT);
+ UiObject2 group4 = mDevice.wait(Until.findObject(By.res(
+ RES_PACKAGE_NAME, "text1").text("Group 4")), LONG_TIMEOUT);
+ Assert.assertNotNull("Group 4 isn't found to be expanded", group4);
+ group4.click();
+ SystemClock.sleep(SHORT_TIMEOUT);
+ group4.click();
+ SystemClock.sleep(SHORT_TIMEOUT);
+ UiObject2 content = mDevice.wait(Until.findObject(By.res(
+ RES_PACKAGE_NAME, "content")), LONG_TIMEOUT);
+ Assert.assertNotNull("Content pane isn't found to move up", content);
+ content.fling(Direction.UP);
+ SystemClock.sleep(SHORT_TIMEOUT);
}
}
}
diff --git a/tests/jank/sysapp/Android.mk b/tests/jank/sysapp/Android.mk
index b84bc10..41c3d13 100644
--- a/tests/jank/sysapp/Android.mk
+++ b/tests/jank/sysapp/Android.mk
@@ -19,7 +19,7 @@
LOCAL_SRC_FILES := $(call all-java-files-under, src)
LOCAL_MODULE_TAGS := tests
-LOCAL_STATIC_JAVA_LIBRARIES := ub-uiautomator ub-janktesthelper timeresult-helper-lib
+LOCAL_STATIC_JAVA_LIBRARIES := ub-uiautomator ub-janktesthelper timeresult-helper-lib app-helpers
LOCAK_SDK_VERSION := current
diff --git a/tests/jank/sysapp/src/com/android/sysapp/janktests/CalendarJankTests.java b/tests/jank/sysapp/src/com/android/sysapp/janktests/CalendarJankTests.java
index 253a82a..c69522f 100644
--- a/tests/jank/sysapp/src/com/android/sysapp/janktests/CalendarJankTests.java
+++ b/tests/jank/sysapp/src/com/android/sysapp/janktests/CalendarJankTests.java
@@ -37,6 +37,8 @@
import android.support.test.uiautomator.UiObjectNotFoundException;
import android.support.test.uiautomator.Until;
import android.view.View;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
import junit.framework.Assert;
import android.support.test.timeresulthelper.TimeResultLogger;
@@ -125,7 +127,7 @@
private void dismissCling() {
UiObject2 splashScreen = null;
splashScreen = mDevice.wait(Until.findObject(
- By.pkg(PACKAGE_NAME).clazz(View.class).desc("Got it")), LONG_TIMEOUT);
+ By.pkg(PACKAGE_NAME).clazz(View.class).desc("Got it")), LONG_TIMEOUT);
if (splashScreen != null) {
splashScreen.clickAndWait(Until.newWindow(), SHORT_TIMEOUT);
}
@@ -137,10 +139,20 @@
rightArrow.click();
--counter;
}
+
+ Pattern pattern = Pattern.compile("GOT IT", Pattern.CASE_INSENSITIVE);
UiObject2 gotIt = mDevice.wait(Until.findObject(
- By.res(PACKAGE_NAME, "done_button").text("Got it")), LONG_TIMEOUT);
+ By.res(PACKAGE_NAME, "done_button").text(pattern)), LONG_TIMEOUT);
if (gotIt != null) {
gotIt.click();
}
+
+ pattern = Pattern.compile("DISMISS", Pattern.CASE_INSENSITIVE);
+ UiObject2 dismissSync = mDevice.wait(Until.findObject(
+ By.res(PACKAGE_NAME, "button_dismiss").text(pattern)), LONG_TIMEOUT);
+ if (dismissSync != null) {
+ dismissSync.click();
+ }
+
}
}
diff --git a/tests/jank/sysapp/src/com/android/sysapp/janktests/ChromeJankTests.java b/tests/jank/sysapp/src/com/android/sysapp/janktests/ChromeJankTests.java
index 3659dda..9fe6ae1 100644
--- a/tests/jank/sysapp/src/com/android/sysapp/janktests/ChromeJankTests.java
+++ b/tests/jank/sysapp/src/com/android/sysapp/janktests/ChromeJankTests.java
@@ -36,6 +36,7 @@
import android.support.test.uiautomator.UiObjectNotFoundException;
import android.support.test.uiautomator.Until;
import junit.framework.Assert;
+import android.platform.test.helpers.ChromeHelperImpl;
import android.support.test.timeresulthelper.TimeResultLogger;
/**
@@ -50,6 +51,7 @@
private static final int EXPECTED_FRAMES = 100;
private static final String PACKAGE_NAME = "com.android.chrome";
private UiDevice mDevice;
+ private ChromeHelperImpl chromeHelper;
private static final File TIMESTAMP_FILE = new File(Environment.getExternalStorageDirectory()
.getAbsolutePath(), "autotester.log");
private static final File RESULTS_FILE = new File(Environment.getExternalStorageDirectory()
@@ -82,6 +84,8 @@
public void launchChrome() throws UiObjectNotFoundException, IOException{
launchApp(PACKAGE_NAME);
+ chromeHelper = new ChromeHelperImpl(getInstrumentation());
+ chromeHelper.dismissInitialDialogs();
getOverflowMenu();
TimeResultLogger.writeTimeStampLogStart(String.format("%s-%s",
getClass().getSimpleName(), getName()), TIMESTAMP_FILE);
diff --git a/tests/jank/sysapp/src/com/android/sysapp/janktests/GMailJankTests.java b/tests/jank/sysapp/src/com/android/sysapp/janktests/GMailJankTests.java
index 4a5e96c..8f4b8a6 100644
--- a/tests/jank/sysapp/src/com/android/sysapp/janktests/GMailJankTests.java
+++ b/tests/jank/sysapp/src/com/android/sysapp/janktests/GMailJankTests.java
@@ -38,7 +38,9 @@
import android.support.test.uiautomator.Until;
import android.widget.ImageButton;
import junit.framework.Assert;
+import android.platform.test.helpers.GmailHelperImpl;
import android.support.test.timeresulthelper.TimeResultLogger;
+import java.util.regex.Pattern;
/**
* Jank test for scrolling gmail inbox mails
@@ -53,6 +55,7 @@
private static final String PACKAGE_NAME = "com.google.android.gm";
private static final String RES_PACKAGE_NAME = "android";
private UiDevice mDevice;
+ private GmailHelperImpl mGmailHelper;
private static final File TIMESTAMP_FILE = new File(Environment.getExternalStorageDirectory()
.getAbsolutePath(), "autotester.log");
private static final File RESULTS_FILE = new File(Environment.getExternalStorageDirectory()
@@ -62,6 +65,7 @@
public void setUp() throws Exception {
super.setUp();
mDevice = UiDevice.getInstance(getInstrumentation());
+ mGmailHelper = new GmailHelperImpl(getInstrumentation());
mDevice.setOrientationNatural();
}
@@ -81,9 +85,7 @@
public void launchGMail () throws UiObjectNotFoundException {
launchApp(PACKAGE_NAME);
- dismissClings();
- // Need any check for account-name??
- waitForEmailSync();
+ mGmailHelper.dismissInitialDialogs();
}
public void prepGMailInboxFling() throws UiObjectNotFoundException, IOException {
@@ -156,13 +158,21 @@
Assert.assertNotNull("Failed to locate Nav Drawer Openner", navDrawer);
navDrawer.click();
// Ensure test is ready to be executed
- UiObject2 container = getNavigationDrawerContainer();
- Assert.assertNotNull("Failed to locate Nav drawer container", container);
+ UiObject2 acctListBtn = mDevice.wait(
+ Until.findObject(By.res(PACKAGE_NAME, "account_list_button")),
+ SHORT_TIMEOUT);
+ Assert.assertNotNull("Failed to locate Nav drawer ", acctListBtn);
TimeResultLogger.writeTimeStampLogStart(String.format("%s-%s",
getClass().getSimpleName(), getName()), TIMESTAMP_FILE);
}
public void afterTestFlingNavDrawer(Bundle metrics) throws IOException {
+ if (!mGmailHelper.closeNavigationDrawer()) {
+ UiObject2 container = getNavigationDrawerContainer();
+ if (container != null) {
+ container.fling(Direction.RIGHT);
+ }
+ }
TimeResultLogger.writeTimeStampLogEnd(String.format("%s-%s",
getClass().getSimpleName(), getName()), TIMESTAMP_FILE);
TimeResultLogger.writeResultToFile(String.format("%s-%s",
@@ -184,49 +194,14 @@
}
}
- private void dismissClings() {
- UiObject2 welcomeScreenGotIt = mDevice.wait(
- Until.findObject(By.res(PACKAGE_NAME, "welcome_tour_got_it")), SHORT_TIMEOUT);
- if (welcomeScreenGotIt != null) {
- welcomeScreenGotIt.clickAndWait(Until.newWindow(), SHORT_TIMEOUT);
- }
- UiObject2 welcomeScreenSkip = mDevice.wait(
- Until.findObject(By.res(PACKAGE_NAME, "welcome_tour_skip")), SHORT_TIMEOUT);
- if (welcomeScreenSkip != null) {
- welcomeScreenSkip.clickAndWait(Until.newWindow(), SHORT_TIMEOUT);
- }
- UiObject2 tutorialDone = mDevice.wait(
- Until.findObject(By.res(PACKAGE_NAME, "action_done")), 2 * SHORT_TIMEOUT);
- if (tutorialDone != null) {
- tutorialDone.clickAndWait(Until.newWindow(), SHORT_TIMEOUT);
- }
- mDevice.wait(Until.findObject(By.text("CONFIDENTIAL")), 2 * SHORT_TIMEOUT);
- UiObject2 splash = mDevice.findObject(By.text("Ok, got it"));
- if (splash != null) {
- splash.clickAndWait(Until.newWindow(), SHORT_TIMEOUT);
- }
- }
-
- public void waitForEmailSync() {
- // Wait up to 2 seconds for a "waiting" message to appear
- mDevice.wait(Until.hasObject(By.text("Waiting for sync")), 2 * SHORT_TIMEOUT);
- // Wait until any "waiting" messages are gone
- Assert.assertTrue("'Waiting for sync' timed out",
- mDevice.wait(Until.gone(By.text("Waiting for sync")), LONG_TIMEOUT * 6));
- Assert.assertTrue("'Loading' timed out",
- mDevice.wait(Until.gone(By.text("Loading")), LONG_TIMEOUT * 6));
- }
public UiObject2 openNavigationDrawer() {
- UiObject2 navDrawer = null;
- if (mDevice.getDisplaySizeDp().x < TAB_MIN_WIDTH) {
- navDrawer = mDevice.wait(Until.findObject(
- By.clazz(ImageButton.class).desc("Navigate up")), SHORT_TIMEOUT);
- } else {
- navDrawer = mDevice.wait(Until.findObject(
- By.clazz(ImageButton.class).desc("Open navigation drawer")), SHORT_TIMEOUT);
+ UiObject2 nav = mDevice.findObject(By.desc(Pattern.compile(
+ "(Open navigation drawer)|(Navigate up)")));
+ if (nav == null) {
+ throw new IllegalStateException("Could not find navigation drawer");
}
- return navDrawer;
+ return nav;
}
public UiObject2 getNavigationDrawerContainer() {
diff --git a/tests/jank/sysapp/src/com/android/sysapp/janktests/YouTubeJankTests.java b/tests/jank/sysapp/src/com/android/sysapp/janktests/YouTubeJankTests.java
index d0614d4..e51f2b9 100644
--- a/tests/jank/sysapp/src/com/android/sysapp/janktests/YouTubeJankTests.java
+++ b/tests/jank/sysapp/src/com/android/sysapp/janktests/YouTubeJankTests.java
@@ -71,7 +71,7 @@
super.tearDown();
}
- public void launchApp(String packageName) throws UiObjectNotFoundException{
+ public void launchApp(String packageName) throws UiObjectNotFoundException {
PackageManager pm = getInstrumentation().getContext().getPackageManager();
Intent appIntent = pm.getLaunchIntentForPackage(packageName);
appIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
@@ -115,7 +115,7 @@
private void dismissCling() {
// Dismiss the dogfood splash screen that might appear on first start
UiObject2 newNavigationDoneBtn = mDevice.wait(Until.findObject(
- By.res(PACKAGE_NAME, "done_button").text("Done")), LONG_TIMEOUT);
+ By.res(PACKAGE_NAME, "done_button")), LONG_TIMEOUT);
if (newNavigationDoneBtn != null) {
newNavigationDoneBtn.click();
}
@@ -136,6 +136,11 @@
Until.findObject(By.res(PACKAGE_NAME, "ok").text("OK")), LONG_TIMEOUT);
Assert.assertNotNull("No 'ok' button to bypass music", ok);
ok.click();
- }
+ }
+ UiObject2 laterButton = mDevice.wait(
+ Until.findObject(By.res(PACKAGE_NAME, "later_button")), LONG_TIMEOUT);
+ if (laterButton != null) {
+ laterButton.click();
+ }
}
}
diff --git a/tests/jank/sysapp_wear/AndroidManifest.xml b/tests/jank/sysapp_wear/AndroidManifest.xml
index 78d3567..cb90c58 100644
--- a/tests/jank/sysapp_wear/AndroidManifest.xml
+++ b/tests/jank/sysapp_wear/AndroidManifest.xml
@@ -16,11 +16,12 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.android.wearable.sysapp.janktests">
-
<application>
<uses-library android:name="android.test.runner" />
</application>
-
+ <uses-sdk android:minSdkVersion="23" android:targetSdkVersion="23" />
+ <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
+ <uses-permission android:name="android.permission.WAKE_LOCK" />
<instrumentation
android:name="android.test.InstrumentationTestRunner"
android:targetPackage="com.android.wearable.sysapp.janktests"
diff --git a/tests/jank/sysapp_wear/src/com/android/wearable/sysapp/janktests/AppLauncherFlingJankTest.java b/tests/jank/sysapp_wear/src/com/android/wearable/sysapp/janktests/AppLauncherFlingJankTest.java
new file mode 100644
index 0000000..fceae07
--- /dev/null
+++ b/tests/jank/sysapp_wear/src/com/android/wearable/sysapp/janktests/AppLauncherFlingJankTest.java
@@ -0,0 +1,102 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.wearable.sysapp.janktests;
+
+import android.content.Context;
+import android.os.Bundle;
+import android.os.RemoteException;
+import android.os.PowerManager;
+import android.os.PowerManager.WakeLock;
+import android.support.test.jank.GfxMonitor;
+import android.support.test.jank.JankTest;
+import android.support.test.jank.JankTestBase;
+import android.support.test.uiautomator.By;
+import android.support.test.uiautomator.Direction;
+import android.support.test.uiautomator.UiDevice;
+import android.support.test.uiautomator.UiObject2;
+import android.support.test.uiautomator.Until;
+
+import java.util.concurrent.TimeoutException;
+
+/**
+ * Jank tests to fling through apps available in the launcher [1st page]
+ */
+public class AppLauncherFlingJankTest extends JankTestBase {
+
+ private UiDevice mDevice;
+ private SysAppTestHelper mHelper;
+ private PowerManager mPm;
+ private WakeLock mWakeLock;
+
+ /*
+ * (non-Javadoc)
+ * @see junit.framework.TestCase#setUp()
+ */
+ @Override
+ protected void setUp() throws Exception {
+ super.setUp();
+ mDevice = UiDevice.getInstance(getInstrumentation());
+ mHelper = SysAppTestHelper.getInstance(mDevice, this.getInstrumentation());
+ mPm = (PowerManager) getInstrumentation().
+ getContext().getSystemService(Context.POWER_SERVICE);
+ mWakeLock = mPm.newWakeLock(PowerManager.FULL_WAKE_LOCK,
+ AppLauncherFlingJankTest.class.getSimpleName());
+ mWakeLock.acquire();
+ }
+
+ /**
+ * This method ensures the device is taken to Home and launch the apps page (also known as 1st
+ * page) before running the fling test on apps.
+ * @throws RemoteException
+ * @throws TimeoutException
+ */
+ public void openLauncher() throws RemoteException, TimeoutException {
+ mHelper.gotoAppLauncher();
+ }
+
+ /**
+ * Test the jank by flinging in apps screen.
+ * @throws TimeoutException
+ *
+ */
+ @JankTest(beforeTest = "openLauncher", afterTest = "goBackHome",
+ expectedFrames = SysAppTestHelper.EXPECTED_FRAMES)
+ @GfxMonitor(processName = "com.google.android.wearable.app")
+ public void testFlingApps() throws TimeoutException {
+ UiObject2 recyclerViewContents = mDevice.wait(Until.findObject(
+ By.res("com.google.android.wearable.app","launcher_view")), SysAppTestHelper.SHORT_TIMEOUT);
+ for (int i = 0; i < 3; i++) {
+ recyclerViewContents.fling(Direction.DOWN, SysAppTestHelper.FLING_SPEED);
+ recyclerViewContents.fling(Direction.UP, SysAppTestHelper.FLING_SPEED);
+ }
+ }
+
+ // Ensuring that we head back to the first screen before launching the app again
+ public void goBackHome(Bundle metrics) throws RemoteException {
+ mHelper.goBackHome();
+ super.afterTest(metrics);
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see android.test.InstrumentationTestCase#tearDown()
+ */
+ @Override
+ protected void tearDown() throws Exception {
+ super.tearDown();
+ mWakeLock.release();
+ }
+}
diff --git a/tests/jank/sysapp_wear/src/com/android/wearable/sysapp/janktests/CardsJankTest.java b/tests/jank/sysapp_wear/src/com/android/wearable/sysapp/janktests/CardsJankTest.java
index 10f3bfb..14507d3 100644
--- a/tests/jank/sysapp_wear/src/com/android/wearable/sysapp/janktests/CardsJankTest.java
+++ b/tests/jank/sysapp_wear/src/com/android/wearable/sysapp/janktests/CardsJankTest.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2015 The Android Open Source Project
+ * Copyright (C) 2016 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -17,15 +17,13 @@
package com.android.wearable.sysapp.janktests;
import android.os.Bundle;
-import android.os.RemoteException;
-import android.os.SystemClock;
import android.support.test.jank.GfxMonitor;
import android.support.test.jank.JankTest;
import android.support.test.jank.JankTestBase;
import android.support.test.uiautomator.UiDevice;
/**
- * Janks tests for scrolling & swiping off notification cards on wear
+ * Jank tests for scrolling & swiping off notification cards on wear
*/
public class CardsJankTest extends JankTestBase {
@@ -40,24 +38,22 @@
protected void setUp() throws Exception {
super.setUp();
mDevice = UiDevice.getInstance(getInstrumentation());
- mHelper = SysAppTestHelper.getInstance(mDevice, this.getInstrumentation().getContext());
+ mHelper = SysAppTestHelper.getInstance(mDevice, this.getInstrumentation());
mDevice.wakeUp();
- SystemClock.sleep(SysAppTestHelper.SHORT_TIMEOUT);
}
// Prepare device to start scrolling by tapping on the screen
// As this is done using demo cards a tap on screen will stop animation and show
// home screen
public void openScrollCard() throws Exception {
- mHelper.goBackHome();
mHelper.hasDemoCards();
- SystemClock.sleep(SysAppTestHelper.SHORT_TIMEOUT);
+ mHelper.swipeUp();
}
// Measure card scroll jank
- @JankTest(beforeTest = "openScrollCard", afterTest = "goBackHome",
- expectedFrames = SysAppTestHelper.MIN_FRAMES)
+ @JankTest(beforeLoop = "openScrollCard", afterTest = "goBackHome",
+ expectedFrames = SysAppTestHelper.EXPECTED_FRAMES_CARDS_TEST)
@GfxMonitor(processName = "com.google.android.wearable.app")
public void testScrollCard() {
mHelper.swipeUp();
@@ -69,23 +65,21 @@
mHelper.hasDemoCards();
mHelper.swipeUp();
mHelper.swipeUp();
- SystemClock.sleep(SysAppTestHelper.SHORT_TIMEOUT);
}
// Measure jank when dismissing a card
- @JankTest(beforeTest = "openSwipeCard", afterTest = "goBackHome",
- expectedFrames = SysAppTestHelper.MIN_FRAMES)
+ @JankTest(beforeLoop = "openSwipeCard", afterTest = "goBackHome",
+ expectedFrames = SysAppTestHelper.EXPECTED_FRAMES_CARDS_TEST)
@GfxMonitor(processName = "com.google.android.wearable.app")
public void testSwipeCard() {
mHelper.swipeRight();
}
// Ensuring that we head back to the first screen before launching the app again
- public void goBackHome(Bundle metrics) throws RemoteException {
+ public void goBackHome(Bundle metrics) {
mHelper.goBackHome();
super.afterTest(metrics);
- SystemClock.sleep(SysAppTestHelper.LONG_TIMEOUT);
}
/*
diff --git a/tests/jank/sysapp_wear/src/com/android/wearable/sysapp/janktests/QuickSettingsJankTest.java b/tests/jank/sysapp_wear/src/com/android/wearable/sysapp/janktests/QuickSettingsJankTest.java
new file mode 100644
index 0000000..ed9c069
--- /dev/null
+++ b/tests/jank/sysapp_wear/src/com/android/wearable/sysapp/janktests/QuickSettingsJankTest.java
@@ -0,0 +1,99 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wearable.sysapp.janktests;
+
+import android.os.Bundle;
+import android.os.SystemClock;
+import android.support.test.jank.GfxMonitor;
+import android.support.test.jank.JankTest;
+import android.support.test.jank.JankTestBase;
+import android.support.test.uiautomator.By;
+import android.support.test.uiautomator.UiDevice;
+import android.support.test.uiautomator.UiObject2;
+import android.support.test.uiautomator.Until;
+
+import junit.framework.Assert;
+
+import java.util.concurrent.TimeoutException;
+
+/**
+ * Jank tests for Quick settings when pulling down, pulling up the shade. And also when swiping in
+ * quick settings options.
+ */
+public class QuickSettingsJankTest extends JankTestBase {
+
+ private UiDevice mDevice;
+ private SysAppTestHelper mHelper;
+
+ private static final String WEARABLE_APP_PACKAGE = "com.google.android.wearable.app";
+ private static final String QUICK_SETTINGS_LAUNCHED_INDICATOR = "settings_icon";
+
+ /*
+ * (non-Javadoc)
+ * @see junit.framework.TestCase#setUp()
+ */
+ @Override
+ protected void setUp() throws Exception {
+ mDevice = UiDevice.getInstance(getInstrumentation());
+ mHelper = SysAppTestHelper.getInstance(mDevice, this.getInstrumentation());
+ mDevice.wakeUp();
+ super.setUp();
+ }
+
+ private void isQuickSettingShadeLaunched() throws TimeoutException {
+ SystemClock.sleep(SysAppTestHelper.SHORT_TIMEOUT + SysAppTestHelper.SHORT_TIMEOUT); //Wait until date & battery info transitions to page indicator
+ UiObject2 quickSettingsShade = mDevice.wait(
+ Until.findObject(By.res(WEARABLE_APP_PACKAGE, QUICK_SETTINGS_LAUNCHED_INDICATOR)),
+ SysAppTestHelper.SHORT_TIMEOUT);
+ Assert.assertNotNull("Quick settings shade not launched", quickSettingsShade);
+
+ }
+
+ // Prepare device to be on Home before pulling down Quick settings shade
+ public void startFromHome() {
+ mHelper.goBackHome();
+ }
+
+ // Verify jank while pulling down quick settings
+ @JankTest(beforeLoop = "startFromHome", afterTest = "goBackHome",
+ expectedFrames = SysAppTestHelper.EXPECTED_FRAMES_CARDS_TEST)
+ @GfxMonitor(processName = WEARABLE_APP_PACKAGE)
+ public void testPullDownQuickSettings() {
+ mHelper.swipeDown();
+ }
+
+ // Prepare device by pulling down the quick settings shade.
+ public void openPullUpQuickSettings() throws TimeoutException {
+ mHelper.goBackHome();
+ mHelper.swipeDown();
+ isQuickSettingShadeLaunched();
+ }
+
+ // Verify jank while pulling up quick settings
+ @JankTest(beforeLoop = "openPullUpQuickSettings", afterTest = "goBackHome",
+ expectedFrames = SysAppTestHelper.EXPECTED_FRAMES_CARDS_TEST)
+ @GfxMonitor(processName = WEARABLE_APP_PACKAGE)
+ public void testPullUpQuickSettings() {
+ mHelper.swipeUp();
+ }
+
+ // Ensuring that we head back to the first screen before launching the app again
+ public void goBackHome(Bundle metrics) {
+ mHelper.goBackHome();
+ super.afterTest(metrics);
+ }
+}
diff --git a/tests/jank/sysapp_wear/src/com/android/wearable/sysapp/janktests/SettingsFlingJankTest.java b/tests/jank/sysapp_wear/src/com/android/wearable/sysapp/janktests/SettingsFlingJankTest.java
new file mode 100644
index 0000000..5311426
--- /dev/null
+++ b/tests/jank/sysapp_wear/src/com/android/wearable/sysapp/janktests/SettingsFlingJankTest.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.wearable.sysapp.janktests;
+
+import android.os.Bundle;
+import android.os.SystemClock;
+import android.support.test.jank.GfxMonitor;
+import android.support.test.jank.JankTest;
+import android.support.test.jank.JankTestBase;
+import android.support.test.uiautomator.By;
+import android.support.test.uiautomator.Direction;
+import android.support.test.uiautomator.UiDevice;
+import android.support.test.uiautomator.UiObject2;
+import android.support.test.uiautomator.Until;
+
+import java.util.concurrent.TimeoutException;
+
+/**
+ * Jank tests to fling through Settings app on clockwork device
+ */
+public class SettingsFlingJankTest extends JankTestBase {
+
+ private UiDevice mDevice;
+ private SysAppTestHelper mHelper;
+
+ // Settings app resources
+ private static final String CLOCK_SETTINGS_PACKAGE =
+ "com.google.android.apps.wearable.settings";
+ private static final String CLOCK_SETTINGS_ACTIVITY =
+ "com.google.android.clockwork.settings.SettingsActivity";
+
+ /*
+ * (non-Javadoc)
+ * @see junit.framework.TestCase#setUp()
+ */
+ @Override
+ protected void setUp() throws Exception {
+ mDevice = UiDevice.getInstance(getInstrumentation());
+ mHelper = SysAppTestHelper.getInstance(mDevice, this.getInstrumentation());
+ mDevice.wakeUp();
+ super.setUp();
+ }
+
+ // Prepare device to launch Settings app and scroll through bottom to start fling test
+ public void openSettingsApp() {
+ mHelper.launchActivity(CLOCK_SETTINGS_PACKAGE, CLOCK_SETTINGS_ACTIVITY);
+ SystemClock.sleep(SysAppTestHelper.SHORT_TIMEOUT);
+ }
+
+ /**
+ * Test the jank by flinging in settings screen.
+ * @throws TimeoutException
+ *
+ */
+ @JankTest(beforeTest = "openSettingsApp", afterTest = "goBackHome",
+ expectedFrames = SysAppTestHelper.EXPECTED_FRAMES)
+ @GfxMonitor(processName = CLOCK_SETTINGS_PACKAGE)
+ public void testSettingsApp() throws TimeoutException {
+ UiObject2 recyclerViewContents = mDevice.wait(Until.findObject(
+ By.res(CLOCK_SETTINGS_PACKAGE,"wheel")), SysAppTestHelper.SHORT_TIMEOUT);
+ for (int i = 0; i < 3; i++) {
+ recyclerViewContents.fling(Direction.DOWN, SysAppTestHelper.FLING_SPEED);
+ recyclerViewContents.fling(Direction.UP, SysAppTestHelper.FLING_SPEED);
+ }
+ }
+
+ // Ensuring that we head back to the first screen before launching the app again
+ public void goBackHome(Bundle metrics) {
+ mHelper.goBackHome();
+ super.afterTest(metrics);
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see android.test.InstrumentationTestCase#tearDown()
+ */
+ @Override
+ protected void tearDown() throws Exception {
+ super.tearDown();
+ }
+}
diff --git a/tests/jank/sysapp_wear/src/com/android/wearable/sysapp/janktests/SysAppTestHelper.java b/tests/jank/sysapp_wear/src/com/android/wearable/sysapp/janktests/SysAppTestHelper.java
index 5421602..9901a0b 100644
--- a/tests/jank/sysapp_wear/src/com/android/wearable/sysapp/janktests/SysAppTestHelper.java
+++ b/tests/jank/sysapp_wear/src/com/android/wearable/sysapp/janktests/SysAppTestHelper.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2015 The Android Open Source Project
+ * Copyright (C) 2016 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -16,27 +16,41 @@
package com.android.wearable.sysapp.janktests;
+import android.app.Instrumentation;
import android.content.ComponentName;
-import android.content.Context;
import android.content.Intent;
-import android.os.RemoteException;
import android.os.SystemClock;
+import android.support.test.uiautomator.By;
import android.support.test.uiautomator.UiDevice;
import android.support.test.uiautomator.UiObject;
+import android.support.test.uiautomator.UiObject2;
import android.support.test.uiautomator.UiSelector;
+import android.support.test.uiautomator.Until;
+import android.util.Log;
+import android.view.KeyEvent;
import junit.framework.Assert;
+import java.util.concurrent.TimeoutException;
+
/**
- * Helper for all they system apps tests
+ * Helper for all the system apps jank tests
*/
public class SysAppTestHelper {
- public static final int MIN_FRAMES = 20;
+ private static final String LOG_TAG = SysAppTestHelper.class.getSimpleName();
+ public static final int EXPECTED_FRAMES_CARDS_TEST = 20;
+ public static final int EXPECTED_FRAMES = 100;
public static final int LONG_TIMEOUT = 5000;
public static final int SHORT_TIMEOUT = 500;
- private static final long NEW_CARD_TIMEOUT_MS = 10 * 1000; // 10s
- private static final int CARD_SWIPE_STEPS = 20;
+ public static final int FLING_SPEED = 5000;
+ private static final long NEW_CARD_TIMEOUT_MS = 5 * 1000; // 5s
+ private static final String RELOAD_NOTIFICATION_CARD_INTENT = "com.google.android.wearable."
+ + "support.wearnotificationgenerator.SHOW_NOTIFICATION";
+ private static final String HOME_INDICATOR = "charging_icon";
+ private static final String LAUNCHER_VIEW_NAME = "launcher_view";
+ private static final String CARD_VIEW_NAME = "activity_view";
+ private static final String QUICKSETTING_VIEW_NAME = "settings_icon";
// Demo card selectors
private static final UiSelector CARD_SELECTOR = new UiSelector()
@@ -45,29 +59,44 @@
.resourceId("com.google.android.wearable.app:id/title");
private static final UiSelector CLOCK_SELECTOR = new UiSelector()
.resourceId("com.google.android.wearable.app:id/clock_bar");
+ private static final UiSelector ICON_SELECTOR = new UiSelector()
+ .resourceId("com.google.android.wearable.app:id/icon");
+ private static final UiSelector TEXT_SELECTOR = new UiSelector()
+ .resourceId("com.google.android.wearable.app:id/text");
+ private static final UiSelector STATUS_BAR_SELECTOR = new UiSelector()
+ .resourceId("com.google.android.wearable.app:id/status_bar_icons");
private UiDevice mDevice = null;
- private Context mContext = null; // Currently not used but for further tests may be useful.
+ private Instrumentation instrumentation = null;
private UiObject mCard = null;
private UiObject mTitle = null;
private UiObject mClock = null;
+ private UiObject mIcon = null;
+ private UiObject mText = null;
+ private UiObject mStatus = null;
private Intent mIntent = null;
private static SysAppTestHelper sysAppTestHelperInstance;
/**
* @param mDevice
- * @param mContext
+ * @param instrumentation
*/
- private SysAppTestHelper(UiDevice mDevice, Context mContext) {
+ private SysAppTestHelper(UiDevice mDevice, Instrumentation instrumentation) {
super();
this.mDevice = mDevice;
- this.mContext = mContext;
+ this.instrumentation = instrumentation;
mIntent = new Intent();
+ mCard = mDevice.findObject(CARD_SELECTOR);
+ mTitle = mDevice.findObject(TITLE_SELECTOR);
+ mClock = mDevice.findObject(CLOCK_SELECTOR);
+ mIcon = mDevice.findObject(ICON_SELECTOR);
+ mText = mDevice.findObject(TEXT_SELECTOR);
+ mStatus = mDevice.findObject(STATUS_BAR_SELECTOR);
}
- public static SysAppTestHelper getInstance(UiDevice device, Context context) {
+ public static SysAppTestHelper getInstance(UiDevice device, Instrumentation instrumentation) {
if (sysAppTestHelperInstance == null) {
- sysAppTestHelperInstance = new SysAppTestHelper(device, context);
+ sysAppTestHelperInstance = new SysAppTestHelper(device, instrumentation);
}
return sysAppTestHelperInstance;
}
@@ -100,6 +129,7 @@
public void flingUp() {
mDevice.swipe(mDevice.getDisplayWidth() / 2, mDevice.getDisplayHeight() / 2 + 50,
mDevice.getDisplayWidth() / 2, 0, 5); // fast speed
+ SystemClock.sleep(SHORT_TIMEOUT);
}
public void flingDown() {
@@ -109,16 +139,33 @@
}
// Helper method to go back to home screen
- public void goBackHome() throws RemoteException {
+ public void goBackHome() {
+ String launcherPackage = mDevice.getLauncherPackageName();
+ UiObject2 homeScreen = mDevice.findObject(By.res(launcherPackage, HOME_INDICATOR));
int count = 0;
- mClock = null;
- while (mClock == null && count++ < 5) {
- mDevice.sleep();
- SystemClock.sleep(LONG_TIMEOUT);
- mDevice.wakeUp();
- SystemClock.sleep(SHORT_TIMEOUT + SHORT_TIMEOUT);
- mClock = mDevice.findObject(CLOCK_SELECTOR); // Ensure device is really on Home screen
+ while (homeScreen == null && count < 5) {
+ mDevice.pressBack();
+ homeScreen = mDevice.findObject(By.res(launcherPackage, HOME_INDICATOR));
+ count ++;
}
+
+ // TODO (yuanlang@) Delete the following hacky codes after charging icon issue fixed
+ // Make sure we're not in the launcher
+ homeScreen = mDevice.findObject(By.res(launcherPackage, LAUNCHER_VIEW_NAME));
+ if (homeScreen != null) {
+ mDevice.pressBack();
+ }
+ // Make sure we're not in cards view
+ homeScreen = mDevice.findObject(By.res(launcherPackage, CARD_VIEW_NAME));
+ if (homeScreen != null) {
+ mDevice.pressBack();
+ }
+ // Make sure we're not in the quick settings
+ homeScreen = mDevice.findObject(By.res(launcherPackage, QUICKSETTING_VIEW_NAME));
+ if (homeScreen != null) {
+ mDevice.pressBack();
+ }
+ SystemClock.sleep(LONG_TIMEOUT);
}
// Helper method to verify if there are any Demo cards.
@@ -127,29 +174,58 @@
// more than one card.
public void hasDemoCards() throws Exception {
// Device should be pre-loaded with demo cards.
- // Start the intent to go to home screen
- mCard = mDevice.findObject(CARD_SELECTOR);
- mTitle = mDevice.findObject(TITLE_SELECTOR);
- mClock = mDevice.findObject(CLOCK_SELECTOR);
- if (mClock.waitForExists(NEW_CARD_TIMEOUT_MS)) {
- mClock.swipeUp(CARD_SWIPE_STEPS);
+ goBackHome(); // Start by going to Home.
+
+ if (!mTitle.waitForExists(NEW_CARD_TIMEOUT_MS)) {
+ Log.d(LOG_TAG, "Card previews not available, swiping up");
+ swipeUp();
+ // For few devices, demo card preview is hidden by default. So swipe once to bring up
+ // the card.
}
// First card from the pre-loaded demo cards could be either in peek view
// or in full view(e.g Dory) or no peek view(Sturgeon). Ensure to check for demo cards
- // existence in
- // both cases.
+ // existence in both cases.
+ if (!(mCard.waitForExists(NEW_CARD_TIMEOUT_MS)
+ || mTitle.waitForExists(NEW_CARD_TIMEOUT_MS)
+ || mIcon.waitForExists(NEW_CARD_TIMEOUT_MS)
+ || mText.waitForExists(NEW_CARD_TIMEOUT_MS))) {
+ Log.d(LOG_TAG, "Demo cards not found, going to reload the cards");
+ // If there are no Demo cards, reload them.
+ reloadDemoCards();
+ if (!mTitle.waitForExists(NEW_CARD_TIMEOUT_MS)) {
+ swipeUp(); // For few devices, demo card preview is hidden by
+ // default. So swipe once to bring up the card.
+ }
+ }
Assert.assertTrue("no cards available for testing",
- (mCard.waitForExists(NEW_CARD_TIMEOUT_MS)
- || mTitle.waitForExists(NEW_CARD_TIMEOUT_MS)));
+ (mTitle.waitForExists(NEW_CARD_TIMEOUT_MS)
+ || mIcon.waitForExists(NEW_CARD_TIMEOUT_MS)
+ || mText.waitForExists(NEW_CARD_TIMEOUT_MS)));
+ }
+
+ // This will ensure to reload notification cards by launching NotificationsGeneratorWear app
+ // when there are insufficient cards.
+ private void reloadDemoCards() {
+ mIntent.setAction(RELOAD_NOTIFICATION_CARD_INTENT);
+ instrumentation.getContext().sendBroadcast(mIntent);
+ SystemClock.sleep(LONG_TIMEOUT);
}
public void launchActivity(String appPackage, String activityToLaunch) {
mIntent.setAction("android.intent.action.MAIN");
mIntent.setComponent(new ComponentName(appPackage, activityToLaunch));
mIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
- mContext.startActivity(mIntent);
+ instrumentation.getContext().startActivity(mIntent);
}
+ // Helper method to goto app launcher and verifies you are there.
+ public void gotoAppLauncher() throws TimeoutException {
+ goBackHome();
+ mDevice.pressKeyCode(KeyEvent.KEYCODE_BACK);
+ UiObject2 appLauncher = mDevice.wait(Until.findObject(By.text("Agenda")),
+ SysAppTestHelper.LONG_TIMEOUT);
+ Assert.assertNotNull("App launcher not launched", appLauncher);
+ }
}
diff --git a/tests/jank/uibench/Android.mk b/tests/jank/uibench/Android.mk
new file mode 100644
index 0000000..c46630b
--- /dev/null
+++ b/tests/jank/uibench/Android.mk
@@ -0,0 +1,26 @@
+# Copyright 2015 Google Inc. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT 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 := UiBenchJankTests
+LOCAL_SRC_FILES := $(call all-java-files-under, src)
+LOCAL_MODULE_TAGS := tests
+
+LOCAL_STATIC_JAVA_LIBRARIES := ub-uiautomator ub-janktesthelper
+
+LOCAL_SDK_VERSION := current
+
+include $(BUILD_PACKAGE)
diff --git a/tests/jank/uibench/AndroidManifest.xml b/tests/jank/uibench/AndroidManifest.xml
new file mode 100644
index 0000000..4cf4e01
--- /dev/null
+++ b/tests/jank/uibench/AndroidManifest.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2015 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.android.uibench.janktests">
+ <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
+ <application>
+ <uses-library android:name="android.test.runner" />
+ </application>
+
+ <uses-sdk android:minSdkVersion="19"
+ android:targetSdkVersion="23"/>
+
+ <instrumentation
+ android:name="android.test.InstrumentationTestRunner"
+ android:targetPackage="com.android.uibench.janktests"
+ android:label="Platform UiBench Jank Tests" />
+</manifest>
diff --git a/tests/jank/uibench/src/com/android/uibench/janktests/UiBenchJankTests.java b/tests/jank/uibench/src/com/android/uibench/janktests/UiBenchJankTests.java
new file mode 100644
index 0000000..7990f7d
--- /dev/null
+++ b/tests/jank/uibench/src/com/android/uibench/janktests/UiBenchJankTests.java
@@ -0,0 +1,173 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.uibench.janktests;
+
+import static com.android.uibench.janktests.UiBenchJankTestsHelper.EXPECTED_FRAMES;
+import static com.android.uibench.janktests.UiBenchJankTestsHelper.PACKAGE_NAME;
+
+import android.os.SystemClock;
+import android.support.test.jank.GfxMonitor;
+import android.support.test.jank.JankTest;
+import android.support.test.jank.JankTestBase;
+import android.support.test.uiautomator.By;
+import android.support.test.uiautomator.UiDevice;
+import android.support.test.uiautomator.Until;
+import android.widget.ListView;
+import junit.framework.Assert;
+
+/**
+ * Jank benchmark General tests for UiBench app
+ */
+
+public class UiBenchJankTests extends JankTestBase {
+
+ private UiDevice mDevice;
+ private UiBenchJankTestsHelper mHelper;
+
+ @Override
+ public void setUp() throws Exception {
+ super.setUp();
+ mDevice = UiDevice.getInstance(getInstrumentation());
+ mDevice.setOrientationNatural();
+ mHelper = UiBenchJankTestsHelper.getInstance(
+ this.getInstrumentation().getContext(), mDevice);
+ }
+
+ @Override
+ protected void tearDown() throws Exception {
+ mDevice.unfreezeRotation();
+ super.tearDown();
+ }
+
+ // Open dialog list from General
+ public void openDialogList() {
+ mHelper.launchActivity("DialogListActivity", "Dialog");
+ mHelper.mContents = mDevice.wait(Until.findObject(
+ By.clazz(ListView.class)), mHelper.TIMEOUT);
+ Assert.assertNotNull("Dialog List View isn't found", mHelper.mContents);
+ }
+
+ // Test dialoglist fling
+ @JankTest(beforeTest = "openDialogList", expectedFrames = EXPECTED_FRAMES)
+ @GfxMonitor(processName = PACKAGE_NAME)
+ public void testDialogListFling() {
+ mHelper.flingUpDown(mHelper.mContents, mHelper.SHORT_TIMEOUT, 1);
+ }
+
+ // Open Fullscreen Overdraw from General
+ public void openFullscreenOverdraw() {
+ mHelper.launchActivity("FullscreenOverdrawActivity",
+ "General/Fullscreen Overdraw");
+ }
+
+ // Measure fullscreen overdraw jank
+ @JankTest(beforeTest = "openFullscreenOverdraw", expectedFrames = EXPECTED_FRAMES)
+ @GfxMonitor(processName = PACKAGE_NAME)
+ public void testFullscreenOverdraw() {
+ SystemClock.sleep(mHelper.LONG_TIMEOUT * 5);
+ }
+
+ // Open GL TextureView from General
+ public void openGLTextureView() {
+ mHelper.launchActivity("GlTextureViewActivity",
+ "General/GL TextureView");
+ }
+
+ // Measure GL TextureView jank metrics
+ @JankTest(beforeTest = "openGLTextureView", expectedFrames = EXPECTED_FRAMES)
+ @GfxMonitor(processName = PACKAGE_NAME)
+ public void testGLTextureView() {
+ SystemClock.sleep(mHelper.LONG_TIMEOUT * 5);
+ }
+
+ // Open Invalidate from General
+ public void openInvalidate() {
+ mHelper.launchActivity("InvalidateActivity",
+ "General/Invalidate");
+ }
+
+ // Measure Invalidate jank metrics
+ @JankTest(beforeTest = "openInvalidate", expectedFrames = EXPECTED_FRAMES)
+ @GfxMonitor(processName = PACKAGE_NAME)
+ public void testInvalidate() {
+ SystemClock.sleep(mHelper.LONG_TIMEOUT * 5);
+ }
+
+ // Open Trivial Animation from General
+ public void openTrivialAnimation() {
+ mHelper.launchActivity("TrivialAnimationActivity",
+ "General/Trivial Animation");
+ }
+
+ // Measure TrivialAnimation jank metrics
+ @JankTest(beforeTest = "openTrivialAnimation", expectedFrames = EXPECTED_FRAMES)
+ @GfxMonitor(processName = PACKAGE_NAME)
+ public void testTrivialAnimation() {
+ SystemClock.sleep(mHelper.LONG_TIMEOUT * 5);
+ }
+
+ // Open Trivial listview from General
+ public void openTrivialListView() {
+ mHelper.launchActivity("TrivialListActivity",
+ "General/Trivial ListView");
+ mHelper.mContents = mDevice.wait(Until.findObject(
+ By.res("android", "content")), mHelper.TIMEOUT);
+ Assert.assertNotNull("Trivial ListView isn't found in General", mHelper.mContents);
+ }
+
+ // Test trivialListView fling
+ @JankTest(beforeTest = "openTrivialListView", expectedFrames = EXPECTED_FRAMES)
+ @GfxMonitor(processName = PACKAGE_NAME)
+ public void testTrivialListViewFling() {
+ mHelper.flingUpDown(mHelper.mContents, mHelper.SHORT_TIMEOUT, 2);
+ }
+
+ // Open Trivial Recycler List View from General
+ public void openTrivialRecyclerListView() {
+ mHelper.launchActivity("TrivialRecyclerViewActivity",
+ "General/Trivial Recycler ListView");
+ mHelper.mContents = mDevice.wait(Until.findObject(
+ By.res("android", "content")), mHelper.TIMEOUT);
+ Assert.assertNotNull("Trivial Recycler ListView isn't found in General",
+ mHelper.mContents);
+ }
+
+ // Test trivialRecyclerListView fling
+ @JankTest(beforeTest = "openTrivialRecyclerListView", expectedFrames = EXPECTED_FRAMES)
+ @GfxMonitor(processName = PACKAGE_NAME)
+ public void testTrivialRecyclerListViewFling() {
+ mHelper.flingUpDown(mHelper.mContents, mHelper.SHORT_TIMEOUT, 2);
+ }
+
+ // Open Inflation Listview contents
+ public void openInflatingListView() {
+ mHelper.launchActivity("InflatingListActivity",
+ "Inflation/Inflating ListView");
+ mHelper.mContents = mDevice.wait(Until.findObject(
+ By.res("android", "content")), mHelper.TIMEOUT);
+ Assert.assertNotNull("Inflating ListView isn't found in Inflation",
+ mHelper.mContents);
+ }
+
+ // Test Inflating List View fling
+ @JankTest(beforeTest = "openInflatingListView", expectedFrames = EXPECTED_FRAMES)
+ @GfxMonitor(processName = PACKAGE_NAME)
+ public void testInflatingListViewFling() {
+ mHelper.flingUpDown(mHelper.mContents, mHelper.SHORT_TIMEOUT, 2);
+ }
+
+}
diff --git a/tests/jank/uibench/src/com/android/uibench/janktests/UiBenchJankTestsHelper.java b/tests/jank/uibench/src/com/android/uibench/janktests/UiBenchJankTestsHelper.java
new file mode 100644
index 0000000..aca7d1f
--- /dev/null
+++ b/tests/jank/uibench/src/com/android/uibench/janktests/UiBenchJankTestsHelper.java
@@ -0,0 +1,94 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.uibench.janktests;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.os.SystemClock;
+import android.support.test.uiautomator.By;
+import android.support.test.uiautomator.Direction;
+import android.support.test.uiautomator.UiDevice;
+import android.support.test.uiautomator.UiObject2;
+import android.support.test.uiautomator.Until;
+import junit.framework.Assert;
+/**
+ * Jank benchmark tests helper for UiBench app
+ */
+
+public class UiBenchJankTestsHelper {
+ public static final int LONG_TIMEOUT = 5000;
+ public static final int TIMEOUT = 250;
+ public static final int SHORT_TIMEOUT = 2000;
+ public static final int EXPECTED_FRAMES = 100;
+
+ public static final String PACKAGE_NAME = "com.android.test.uibench";
+
+ private static UiBenchJankTestsHelper mInstance;
+ private static UiDevice mDevice;
+ private Context mContext;
+ protected UiObject2 mContents;
+
+ private UiBenchJankTestsHelper(Context context, UiDevice device) {
+ mContext = context;
+ mDevice = device;
+ }
+
+ public static UiBenchJankTestsHelper getInstance(Context context, UiDevice device) {
+ if (mInstance == null) {
+ mInstance = new UiBenchJankTestsHelper(context, device);
+ }
+ return mInstance;
+ }
+
+ /**
+ * Launch activity using intent
+ * @param activityName
+ * @param verifyText
+ */
+ public void launchActivity(String activityName, String verifyText) {
+ ComponentName cn = new ComponentName(PACKAGE_NAME,
+ String.format("%s.%s", PACKAGE_NAME, activityName));
+ Intent intent = new Intent(Intent.ACTION_MAIN);
+ intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK);
+ intent.setComponent(cn);
+ // Launch the activity
+ mContext.startActivity(intent);
+ UiObject2 expectedTextCmp = mDevice.wait(Until.findObject(
+ By.text(verifyText)), LONG_TIMEOUT);
+ Assert.assertNotNull(String.format("Issue in opening %s", activityName),
+ expectedTextCmp);
+ }
+
+ /**
+ * To perform the fling down and up on given content for flingCount number
+ * of times
+ * @param content
+ * @param timeout
+ * @param flingCount
+ */
+ public void flingUpDown(UiObject2 content, long timeout, int flingCount) {
+ for (int count = 0; count < flingCount; count++) {
+ SystemClock.sleep(timeout);
+ content.fling(Direction.DOWN);
+ SystemClock.sleep(timeout);
+ content.fling(Direction.UP);
+ }
+ }
+
+}
diff --git a/tests/jank/uibench/src/com/android/uibench/janktests/UiBenchRenderingJankTests.java b/tests/jank/uibench/src/com/android/uibench/janktests/UiBenchRenderingJankTests.java
new file mode 100644
index 0000000..c825116
--- /dev/null
+++ b/tests/jank/uibench/src/com/android/uibench/janktests/UiBenchRenderingJankTests.java
@@ -0,0 +1,85 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.uibench.janktests;
+
+import static com.android.uibench.janktests.UiBenchJankTestsHelper.EXPECTED_FRAMES;
+import static com.android.uibench.janktests.UiBenchJankTestsHelper.PACKAGE_NAME;
+
+import android.os.SystemClock;
+import android.support.test.jank.GfxMonitor;
+import android.support.test.jank.JankTest;
+import android.support.test.jank.JankTestBase;
+import android.support.test.uiautomator.By;
+import android.support.test.uiautomator.UiDevice;
+import android.support.test.uiautomator.Until;
+import android.widget.ListView;
+import junit.framework.Assert;
+
+/**
+ * Jank benchmark Rendering tests for UiBench app
+ */
+
+public class UiBenchRenderingJankTests extends JankTestBase {
+
+ private UiDevice mDevice;
+ private UiBenchJankTestsHelper mHelper;
+
+ @Override
+ public void setUp() throws Exception {
+ super.setUp();
+ mDevice = UiDevice.getInstance(getInstrumentation());
+ mDevice.setOrientationNatural();
+ mHelper = UiBenchJankTestsHelper.getInstance(this.getInstrumentation().getContext(),
+ mDevice);
+ }
+
+ @Override
+ protected void tearDown() throws Exception {
+ mDevice.unfreezeRotation();
+ super.tearDown();
+ }
+
+ // Open Bitmap Upload
+ public void openBitmapUpload() {
+ mHelper.launchActivity("BitmapUploadActivity",
+ "Rendering/Bitmap Upload");
+ }
+
+ // Test Bitmap Upload jank
+ @JankTest(beforeTest = "openBitmapUpload", expectedFrames = EXPECTED_FRAMES)
+ @GfxMonitor(processName = PACKAGE_NAME)
+ public void testBitmapUploadJank() {
+ SystemClock.sleep(mHelper.LONG_TIMEOUT * 5);
+ }
+
+ // Open Shadow Grid
+ public void openRenderingList() {
+ mHelper.launchActivity("ShadowGridActivity",
+ "Rendering/Shadow Grid");
+ mHelper.mContents = mDevice.wait(Until.findObject(
+ By.clazz(ListView.class)), mHelper.TIMEOUT);
+ Assert.assertNotNull("Shadow Grid list isn't found", mHelper.mContents);
+ }
+
+ // Test Shadow Grid fling
+ @JankTest(beforeTest = "openRenderingList", expectedFrames = EXPECTED_FRAMES)
+ @GfxMonitor(processName = PACKAGE_NAME)
+ public void testShadowGridListFling() {
+ mHelper.flingUpDown(mHelper.mContents, mHelper.SHORT_TIMEOUT, 1);
+ }
+
+}
diff --git a/tests/jank/uibench/src/com/android/uibench/janktests/UiBenchTextJankTests.java b/tests/jank/uibench/src/com/android/uibench/janktests/UiBenchTextJankTests.java
new file mode 100644
index 0000000..b730aec
--- /dev/null
+++ b/tests/jank/uibench/src/com/android/uibench/janktests/UiBenchTextJankTests.java
@@ -0,0 +1,103 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.uibench.janktests;
+
+import static com.android.uibench.janktests.UiBenchJankTestsHelper.EXPECTED_FRAMES;
+import static com.android.uibench.janktests.UiBenchJankTestsHelper.PACKAGE_NAME;
+
+import android.os.SystemClock;
+import android.support.test.jank.GfxMonitor;
+import android.support.test.jank.JankTest;
+import android.support.test.jank.JankTestBase;
+import android.support.test.uiautomator.By;
+import android.support.test.uiautomator.UiDevice;
+import android.support.test.uiautomator.Until;
+import android.widget.ListView;
+import junit.framework.Assert;
+
+/**
+ * Jank benchmark Text tests for UiBench app
+ */
+
+public class UiBenchTextJankTests extends JankTestBase {
+
+ private UiDevice mDevice;
+ private UiBenchJankTestsHelper mHelper;
+
+ @Override
+ public void setUp() throws Exception {
+ super.setUp();
+ mDevice = UiDevice.getInstance(getInstrumentation());
+ mDevice.setOrientationNatural();
+ mHelper = UiBenchJankTestsHelper.getInstance(
+ this.getInstrumentation().getContext(), mDevice);
+ }
+
+ @Override
+ protected void tearDown() throws Exception {
+ mDevice.unfreezeRotation();
+ super.tearDown();
+ }
+
+ // Open EditText Typing
+ public void openEditTextTyping() {
+ mHelper.launchActivity("EditTextTypeActivity",
+ "Text/EditText Typing");
+ }
+
+ // Measure jank metrics for EditText Typing
+ @JankTest(beforeTest = "openEditTextTyping", expectedFrames = EXPECTED_FRAMES)
+ @GfxMonitor(processName = PACKAGE_NAME)
+ public void testEditTextTyping() {
+ SystemClock.sleep(mHelper.LONG_TIMEOUT * 2);
+ }
+
+ // Open Layout Cache High Hitrate
+ public void openLayoutCacheHighHitrate() {
+ mHelper.launchActivity("TextCacheHighHitrateActivity",
+ "Text/Layout Cache High Hitrate");
+ mHelper.mContents = mDevice.wait(Until.findObject(
+ By.clazz(ListView.class)), mHelper.TIMEOUT);
+ Assert.assertNotNull("LayoutCacheHighHitrateContents isn't found",
+ mHelper.mContents);
+ }
+
+ // Test Layout Cache High Hitrate fling
+ @JankTest(beforeTest = "openLayoutCacheHighHitrate", expectedFrames = EXPECTED_FRAMES)
+ @GfxMonitor(processName = PACKAGE_NAME)
+ public void testLayoutCacheHighHitrateFling() {
+ mHelper.flingUpDown(mHelper.mContents, mHelper.SHORT_TIMEOUT, 3);
+ }
+
+ // Open Layout Cache Low Hitrate
+ public void openLayoutCacheLowHitrate() {
+ mHelper.launchActivity("TextCacheLowHitrateActivity",
+ "Text/Layout Cache Low Hitrate");
+ mHelper.mContents = mDevice.wait(Until.findObject(
+ By.clazz(ListView.class)), mHelper.TIMEOUT);
+ Assert.assertNotNull("LayoutCacheLowHitrateContents isn't found",
+ mHelper.mContents);
+ }
+
+ // Test Layout Cache Low Hitrate fling
+ @JankTest(beforeTest = "openLayoutCacheLowHitrate", expectedFrames = EXPECTED_FRAMES)
+ @GfxMonitor(processName = PACKAGE_NAME)
+ public void testLayoutCacheLowHitrateFling() {
+ mHelper.flingUpDown(mHelper.mContents, mHelper.SHORT_TIMEOUT, 3);
+ }
+
+}
diff --git a/tests/jank/uibench/src/com/android/uibench/janktests/UiBenchTransitionsJankTests.java b/tests/jank/uibench/src/com/android/uibench/janktests/UiBenchTransitionsJankTests.java
new file mode 100644
index 0000000..fccb8c6
--- /dev/null
+++ b/tests/jank/uibench/src/com/android/uibench/janktests/UiBenchTransitionsJankTests.java
@@ -0,0 +1,83 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.uibench.janktests;
+
+import static com.android.uibench.janktests.UiBenchJankTestsHelper.EXPECTED_FRAMES;
+import static com.android.uibench.janktests.UiBenchJankTestsHelper.PACKAGE_NAME;
+
+import android.support.test.jank.GfxMonitor;
+import android.support.test.jank.JankTest;
+import android.support.test.jank.JankTestBase;
+import android.support.test.uiautomator.By;
+import android.support.test.uiautomator.UiDevice;
+import android.support.test.uiautomator.UiObject2;
+import android.support.test.uiautomator.Until;
+import junit.framework.Assert;
+
+/**
+ * Jank benchmark Text tests for UiBench app
+ */
+
+public class UiBenchTransitionsJankTests extends JankTestBase {
+
+ private UiDevice mDevice;
+ private UiBenchJankTestsHelper mHelper;
+
+ @Override
+ public void setUp() throws Exception {
+ super.setUp();
+ mDevice = UiDevice.getInstance(getInstrumentation());
+ mDevice.setOrientationNatural();
+ mHelper = UiBenchJankTestsHelper.getInstance(
+ this.getInstrumentation().getContext(), mDevice);
+ }
+
+ @Override
+ protected void tearDown() throws Exception {
+ mDevice.unfreezeRotation();
+ super.tearDown();
+ }
+
+ // Open Transitions
+ public void openActivityTransition() {
+ mHelper.launchActivity("ActivityTransition",
+ "Transitions/Activity Transition");
+ }
+
+ // Get the image to click
+ public void clickImage(String imageName) {
+ UiObject2 image = mDevice.wait(Until.findObject(
+ By.res(mHelper.PACKAGE_NAME, imageName)), mHelper.TIMEOUT);
+ Assert.assertNotNull(imageName + "Image not found", image);
+ image.clickAndWait(Until.newWindow(), mHelper.TIMEOUT);
+ mDevice.pressBack();
+ }
+
+ // Measures jank for activity transition animation
+ @JankTest(beforeTest = "openActivityTransition", expectedFrames = EXPECTED_FRAMES)
+ @GfxMonitor(processName = PACKAGE_NAME)
+ public void testActivityTransitionsAnimation() {
+ clickImage("ducky");
+ clickImage("woot");
+ clickImage("ball");
+ clickImage("block");
+ clickImage("jellies");
+ clickImage("mug");
+ clickImage("pencil");
+ clickImage("scissors");
+ }
+}
diff --git a/tests/jank/uibench/src/com/android/uibench/janktests/UiBenchWebView.java b/tests/jank/uibench/src/com/android/uibench/janktests/UiBenchWebView.java
new file mode 100644
index 0000000..659577c
--- /dev/null
+++ b/tests/jank/uibench/src/com/android/uibench/janktests/UiBenchWebView.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.uibench.janktests;
+
+import static com.android.uibench.janktests.UiBenchJankTestsHelper.EXPECTED_FRAMES;
+import static com.android.uibench.janktests.UiBenchJankTestsHelper.PACKAGE_NAME;
+
+import android.support.test.jank.GfxMonitor;
+import android.support.test.jank.JankTest;
+import android.support.test.jank.JankTestBase;
+import android.support.test.uiautomator.By;
+import android.support.test.uiautomator.UiDevice;
+import android.support.test.uiautomator.Until;
+
+/**
+ * Jank benchmark WebView tests from UiBench app
+ */
+
+public class UiBenchWebView extends JankTestBase {
+
+ private UiDevice mDevice;
+ private UiBenchJankTestsHelper mHelper;
+
+ @Override
+ public void setUp() throws Exception {
+ super.setUp();
+ mDevice = UiDevice.getInstance(getInstrumentation());
+ mDevice.setOrientationNatural();
+ mHelper = UiBenchJankTestsHelper.getInstance(this.getInstrumentation().getContext(),
+ mDevice);
+ }
+
+ @Override
+ protected void tearDown() throws Exception {
+ mDevice.unfreezeRotation();
+ super.tearDown();
+ }
+
+ // Open Scrollable WebView from WebView test
+ public void openScrollableWebView() {
+ mHelper.launchActivity("ScrollableWebViewActivity",
+ "WebView/Scrollable WebView");
+ mHelper.mContents = mDevice.wait(Until.findObject(
+ By.res("android", "content")), mHelper.TIMEOUT);
+ }
+
+ // Test Scrollable WebView fling
+ @JankTest(beforeTest = "openScrollableWebView", expectedFrames = EXPECTED_FRAMES)
+ @GfxMonitor(processName = PACKAGE_NAME)
+ public void testWebViewFling() {
+ mHelper.flingUpDown(mHelper.mContents, mHelper.SHORT_TIMEOUT, 1);
+ }
+
+}
diff --git a/tests/jank/uibench_wear/AndroidManifest.xml b/tests/jank/uibench_wear/AndroidManifest.xml
index 9e71e81..231d519 100644
--- a/tests/jank/uibench_wear/AndroidManifest.xml
+++ b/tests/jank/uibench_wear/AndroidManifest.xml
@@ -20,6 +20,8 @@
<application>
<uses-library android:name="android.test.runner" />
</application>
+ <uses-sdk android:minSdkVersion="23" android:targetSdkVersion="23" />
+ <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<instrumentation
android:name="android.test.InstrumentationTestRunner"
diff --git a/tests/jank/uibench_wear/src/com/android/wearable/uibench/janktests/UiBenchJankTests.java b/tests/jank/uibench_wear/src/com/android/wearable/uibench/janktests/UiBenchJankTests.java
index 8355c31..d61ddf9 100644
--- a/tests/jank/uibench_wear/src/com/android/wearable/uibench/janktests/UiBenchJankTests.java
+++ b/tests/jank/uibench_wear/src/com/android/wearable/uibench/janktests/UiBenchJankTests.java
@@ -193,18 +193,8 @@
// Open Inflation Listview contents
public void openInflatingListView() {
mHelper.launchUiBench();
- UiObject2 inflation = mDevice.wait(Until.findObject(
- By.res(mHelper.RES_PACKAGE_NAME, "text1").text("Inflation")), mHelper.TIMEOUT);
- Assert.assertNotNull("Inflation isn't found in UiBench", inflation);
- inflation.click();
- SystemClock.sleep(mHelper.TIMEOUT);
- UiObject2 inflatingListView = mDevice.wait(Until.findObject(
- By.res(mHelper.RES_PACKAGE_NAME, "text1").text("Inflating ListView")),
- mHelper.TIMEOUT);
- Assert.assertNotNull("Inflating ListView Contents isn't found in Inflation",
- inflatingListView);
- inflatingListView.click();
- SystemClock.sleep(mHelper.TIMEOUT);
+ mHelper.openTextInList("Inflation");
+ mHelper.openTextInList("Inflating ListView");
}
// Test Inflating List View fling
diff --git a/tests/jank/uibench_wear/src/com/android/wearable/uibench/janktests/UiBenchJankTestsHelper.java b/tests/jank/uibench_wear/src/com/android/wearable/uibench/janktests/UiBenchJankTestsHelper.java
index 1d2f43e..a1f4902 100644
--- a/tests/jank/uibench_wear/src/com/android/wearable/uibench/janktests/UiBenchJankTestsHelper.java
+++ b/tests/jank/uibench_wear/src/com/android/wearable/uibench/janktests/UiBenchJankTestsHelper.java
@@ -42,7 +42,8 @@
public static final String RES_PACKAGE_NAME = "android";
public static final String PACKAGE_NAME = "com.android.test.uibench";
- public static final String CLOCK_BAR_NAME = "clock_bar";
+ public static final String ROOT_NAME = "root";
+ public static final String LAUNCHER_VIEW_NAME = "launcher_view";
public static final String TEXT_OBJECT_NAME = "text1";
public static final String UIBENCH_OBJECT_NAME = "UiBench";
@@ -77,22 +78,26 @@
UiObject2 initScreen = mDevice.wait(Until.findObject(By.text(UIBENCH_OBJECT_NAME)), 2000);
int counter = 3;
while (initScreen == null && --counter > 0) {
- swipeRight();
+ mDevice.pressBack();
initScreen = mDevice.wait(Until.findObject(By.text(UIBENCH_OBJECT_NAME)), 2000);
}
}
// Helper method to go back to home screen
public void goBackHome() throws UiObjectNotFoundException {
-
String launcherPackage = mDevice.getLauncherPackageName();
- UiObject2 homeScreen = null;
+ UiObject2 homeScreen = mDevice.findObject(By.res(launcherPackage, ROOT_NAME));
int count = 0;
while (homeScreen == null && count < 5) {
- swipeRight();
- homeScreen = mDevice.findObject(By.res(launcherPackage,CLOCK_BAR_NAME));
+ mDevice.pressBack();
+ homeScreen = mDevice.findObject(By.res(launcherPackage, ROOT_NAME));
count ++;
}
+ // Make sure we're not in the launcher
+ homeScreen = mDevice.findObject(By.res(launcherPackage, LAUNCHER_VIEW_NAME));
+ if (homeScreen != null) {
+ mDevice.pressBack();
+ }
}
public void openTextInList(String itemName) {
@@ -114,7 +119,6 @@
Assert.assertNotNull(itemName + ": isn't found", component);
component.clickAndWait(Until.newWindow(), LONG_TIMEOUT);
SystemClock.sleep(TIMEOUT);
-
}
public void swipeRight() {
diff --git a/tests/jank/uibench_wear/src/com/android/wearable/uibench/janktests/UiBenchTextJankTests.java b/tests/jank/uibench_wear/src/com/android/wearable/uibench/janktests/UiBenchTextJankTests.java
index 1a2ee84..afabd0d 100644
--- a/tests/jank/uibench_wear/src/com/android/wearable/uibench/janktests/UiBenchTextJankTests.java
+++ b/tests/jank/uibench_wear/src/com/android/wearable/uibench/janktests/UiBenchTextJankTests.java
@@ -16,7 +16,10 @@
package com.android.wearable.uibench.janktests;
-import android.content.Intent;
+import static com.android.wearable.uibench.janktests.UiBenchJankTestsHelper.EXPECTED_FRAMES;
+import static com.android.wearable.uibench.janktests.UiBenchJankTestsHelper.PACKAGE_NAME;
+
+import android.os.Build.VERSION;
import android.os.Bundle;
import android.os.RemoteException;
import android.os.SystemClock;
@@ -24,17 +27,17 @@
import android.support.test.jank.JankTest;
import android.support.test.jank.JankTestBase;
import android.support.test.uiautomator.By;
-import android.support.test.uiautomator.StaleObjectException;
import android.support.test.uiautomator.Direction;
+import android.support.test.uiautomator.StaleObjectException;
import android.support.test.uiautomator.UiDevice;
import android.support.test.uiautomator.UiObject2;
import android.support.test.uiautomator.UiObjectNotFoundException;
import android.support.test.uiautomator.Until;
+import android.view.KeyEvent;
import android.widget.ListView;
import com.android.wearable.uibench.janktests.UiBenchJankTestsHelper;
-import static com.android.wearable.uibench.janktests.UiBenchJankTestsHelper.PACKAGE_NAME;
-import static com.android.wearable.uibench.janktests.UiBenchJankTestsHelper.EXPECTED_FRAMES;
+
import junit.framework.Assert;
/**
@@ -61,20 +64,39 @@
super.tearDown();
}
+ // TODO(kneas): After b/27897448 is fixed, remove method or TODO
+ public void forceDeviceHome() throws RemoteException {
+ // Put device to sleep to go back home
+ if (VERSION.SDK_INT >= 20) {
+ mDevice.pressKeyCode(KeyEvent.KEYCODE_SLEEP);
+ } else {
+ mDevice.sleep();
+ }
+ SystemClock.sleep(mHelper.LONG_TIMEOUT);
+ mDevice.wakeUp();
+ }
+
// Open Text Components
- public void openTextComponents(String componentName) {
+ public void openTextComponents(String componentName) throws RemoteException {
+ // TODO(kneas): Remove if statement after b/27897448 is fixed
+ // Needed in case the EditTextTyping tests fails, leaving it in the test with the keyboard
+ // open
+ if (mDevice.getProductName().equals("nemo")) {
+ forceDeviceHome();
+ }
mHelper.launchUiBench();
mHelper.openTextInList("Text");
mHelper.openTextInList(componentName);
}
// Open EditText Typing
- public void openEditTextTyping() {
+ public void openEditTextTyping() throws RemoteException {
openTextComponents("EditText Typing");
}
// Measure jank metrics for EditText Typing
- @JankTest(beforeTest="openEditTextTyping", afterTest="goBackHome",
+ // TODO(kneas): Change afterTest to "goBackHome" after b/27897448 is fixed
+ @JankTest(beforeTest="openEditTextTyping", afterTest="goBackHomeEditText",
expectedFrames=EXPECTED_FRAMES)
@GfxMonitor(processName=PACKAGE_NAME)
public void testEditTextTyping() {
@@ -82,7 +104,7 @@
}
// Open Layout Cache High Hitrate
- public void openLayoutCacheHighHitrate() {
+ public void openLayoutCacheHighHitrate() throws RemoteException {
openTextComponents("Layout Cache High Hitrate");
}
@@ -93,7 +115,8 @@
public void testLayoutCacheHighHitrateFling() {
UiObject2 layoutCacheHighHitrateContents = mDevice.wait(Until.findObject(
By.clazz(ListView.class)), mHelper.TIMEOUT);
- Assert.assertNotNull("LayoutCacheHighHitrateContents isn't found", layoutCacheHighHitrateContents);
+ Assert.assertNotNull("LayoutCacheHighHitrateContents isn't found",
+ layoutCacheHighHitrateContents);
for (int i = 0; i < mHelper.INNER_LOOP; i++) {
try {
@@ -114,7 +137,7 @@
}
// Open Layout Cache Low Hitrate
- public void openLayoutCacheLowHitrate() {
+ public void openLayoutCacheLowHitrate() throws RemoteException {
openTextComponents("Layout Cache Low Hitrate");
}
@@ -125,7 +148,8 @@
public void testLayoutCacheLowHitrateFling() {
UiObject2 layoutCacheLowHitrateContents = mDevice.wait(Until.findObject(
By.clazz(ListView.class)), mHelper.TIMEOUT);
- Assert.assertNotNull("LayoutCacheLowHitrateContents isn't found", layoutCacheLowHitrateContents);
+ Assert.assertNotNull("LayoutCacheLowHitrateContents isn't found",
+ layoutCacheLowHitrateContents);
for (int i = 0; i < mHelper.INNER_LOOP; i++) {
try {
@@ -150,4 +174,22 @@
mHelper.goBackHome();
super.afterTest(metrics);
}
+
+ // Workaround for b/27897448 until investigation is complete
+ // TODO(kneas): Remove once b/27897448 is fixed
+ public void goBackHomeEditText(Bundle metrics)
+ throws RemoteException, UiObjectNotFoundException {
+ if (mDevice.getProductName() == "nemo") {
+ forceDeviceHome();
+ super.afterTest(metrics);
+ // Relaunch the app. Ideally we're still in EditText, but it's no longer typing
+ // goBackHome will now be able to use the back button, since the keyboard is hidden
+ SystemClock.sleep(mHelper.SHORT_TIMEOUT + mHelper.SHORT_TIMEOUT);
+ mHelper.launchUiBench();
+ mHelper.goBackHome();
+ }
+ else {
+ goBackHome(metrics);
+ }
+ }
}
diff --git a/tests/perf/PerformanceLaunch/AndroidManifest.xml b/tests/perf/PerformanceLaunch/AndroidManifest.xml
index d4aca87..48461a2 100644
--- a/tests/perf/PerformanceLaunch/AndroidManifest.xml
+++ b/tests/perf/PerformanceLaunch/AndroidManifest.xml
@@ -74,6 +74,12 @@
android:autoRemoveFromRecents="true"
android:exported="true"
android:screenOrientation="nosensor" />
+ <activity
+ android:name=".ManyConfigResourceActivity"
+ android:label="@string/app_name"
+ android:autoRemoveFromRecents="true"
+ android:exported="true"
+ android:screenOrientation="nosensor" />
</application>
</manifest>
diff --git a/tests/perf/PerformanceLaunch/gen_locales.py b/tests/perf/PerformanceLaunch/gen_locales.py
new file mode 100644
index 0000000..ade471a
--- /dev/null
+++ b/tests/perf/PerformanceLaunch/gen_locales.py
@@ -0,0 +1,34 @@
+#!/usr/bin/env python
+
+#
+# Generates a series of res/values-{locale}/ directories with a strings.xml
+# file in each containing 4 resources, many_config_1, many_config_2, many_config_3,
+# and many_config_4.
+#
+
+import os
+
+template = """<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="many_config_1">ManyConfig1-{0}</string>
+ <string name="many_config_2">ManyConfig1-{0}</string>
+ <string name="many_config_3">ManyConfig1-{0}</string>
+ <string name="many_config_4">ManyConfig1-{0}</string>
+</resources>"""
+
+localeStr = "en_US af_ZA am_ET ar_EG bg_BG bn_BD ca_ES cs_CZ da_DK de_DE el_GR en_AU en_GB en_IN es_ES es_US et_EE eu_ES \
+fa_IR fi_FI fr_CA fr_FR gl_ES hi_IN hr_HR hu_HU hy_AM in_ID is_IS it_IT iw_IL ja_JP ka_GE km_KH ko_KR ky_KG lo_LA lt_LT \
+lv_LV km_MH kn_IN mn_MN ml_IN mk_MK mr_IN ms_MY my_MM ne_NP nb_NO nl_NL pl_PL pt_BR pt_PT ro_RO ru_RU si_LK sk_SK sl_SI \
+sr_RS sv_SE sw_TZ ta_IN te_IN th_TH tl_PH tr_TR uk_UA vi_VN zh_CN zh_HK zh_TW zu_ZA en_XA ar_XB"
+
+locales = [locale.replace("_", "-r") for locale in localeStr.split(" ")]
+
+for locale in locales:
+ try:
+ os.mkdir("res/values-{0}".format(locale))
+ except:
+ pass
+
+ with open("res/values-{0}/strings.xml".format(locale), "w") as f:
+ f.write(template.format(locale))
+
diff --git a/tests/perf/PerformanceLaunch/res/values-af-rZA/strings.xml b/tests/perf/PerformanceLaunch/res/values-af-rZA/strings.xml
new file mode 100644
index 0000000..8123d61
--- /dev/null
+++ b/tests/perf/PerformanceLaunch/res/values-af-rZA/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="many_config_1">ManyConfig1-af-rZA</string>
+ <string name="many_config_2">ManyConfig1-af-rZA</string>
+ <string name="many_config_3">ManyConfig1-af-rZA</string>
+ <string name="many_config_4">ManyConfig1-af-rZA</string>
+</resources>
\ No newline at end of file
diff --git a/tests/perf/PerformanceLaunch/res/values-am-rET/strings.xml b/tests/perf/PerformanceLaunch/res/values-am-rET/strings.xml
new file mode 100644
index 0000000..ee4fe92
--- /dev/null
+++ b/tests/perf/PerformanceLaunch/res/values-am-rET/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="many_config_1">ManyConfig1-am-rET</string>
+ <string name="many_config_2">ManyConfig1-am-rET</string>
+ <string name="many_config_3">ManyConfig1-am-rET</string>
+ <string name="many_config_4">ManyConfig1-am-rET</string>
+</resources>
\ No newline at end of file
diff --git a/tests/perf/PerformanceLaunch/res/values-ar-rEG/strings.xml b/tests/perf/PerformanceLaunch/res/values-ar-rEG/strings.xml
new file mode 100644
index 0000000..fdd7830
--- /dev/null
+++ b/tests/perf/PerformanceLaunch/res/values-ar-rEG/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="many_config_1">ManyConfig1-ar-rEG</string>
+ <string name="many_config_2">ManyConfig1-ar-rEG</string>
+ <string name="many_config_3">ManyConfig1-ar-rEG</string>
+ <string name="many_config_4">ManyConfig1-ar-rEG</string>
+</resources>
\ No newline at end of file
diff --git a/tests/perf/PerformanceLaunch/res/values-ar-rXB/strings.xml b/tests/perf/PerformanceLaunch/res/values-ar-rXB/strings.xml
new file mode 100644
index 0000000..8f90e7c
--- /dev/null
+++ b/tests/perf/PerformanceLaunch/res/values-ar-rXB/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="many_config_1">ManyConfig1-ar-rXB</string>
+ <string name="many_config_2">ManyConfig1-ar-rXB</string>
+ <string name="many_config_3">ManyConfig1-ar-rXB</string>
+ <string name="many_config_4">ManyConfig1-ar-rXB</string>
+</resources>
\ No newline at end of file
diff --git a/tests/perf/PerformanceLaunch/res/values-bg-rBG/strings.xml b/tests/perf/PerformanceLaunch/res/values-bg-rBG/strings.xml
new file mode 100644
index 0000000..07823e9
--- /dev/null
+++ b/tests/perf/PerformanceLaunch/res/values-bg-rBG/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="many_config_1">ManyConfig1-bg-rBG</string>
+ <string name="many_config_2">ManyConfig1-bg-rBG</string>
+ <string name="many_config_3">ManyConfig1-bg-rBG</string>
+ <string name="many_config_4">ManyConfig1-bg-rBG</string>
+</resources>
\ No newline at end of file
diff --git a/tests/perf/PerformanceLaunch/res/values-bn-rBD/strings.xml b/tests/perf/PerformanceLaunch/res/values-bn-rBD/strings.xml
new file mode 100644
index 0000000..90d3456
--- /dev/null
+++ b/tests/perf/PerformanceLaunch/res/values-bn-rBD/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="many_config_1">ManyConfig1-bn-rBD</string>
+ <string name="many_config_2">ManyConfig1-bn-rBD</string>
+ <string name="many_config_3">ManyConfig1-bn-rBD</string>
+ <string name="many_config_4">ManyConfig1-bn-rBD</string>
+</resources>
\ No newline at end of file
diff --git a/tests/perf/PerformanceLaunch/res/values-ca-rES/strings.xml b/tests/perf/PerformanceLaunch/res/values-ca-rES/strings.xml
new file mode 100644
index 0000000..40aad1f
--- /dev/null
+++ b/tests/perf/PerformanceLaunch/res/values-ca-rES/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="many_config_1">ManyConfig1-ca-rES</string>
+ <string name="many_config_2">ManyConfig1-ca-rES</string>
+ <string name="many_config_3">ManyConfig1-ca-rES</string>
+ <string name="many_config_4">ManyConfig1-ca-rES</string>
+</resources>
\ No newline at end of file
diff --git a/tests/perf/PerformanceLaunch/res/values-cs-rCZ/strings.xml b/tests/perf/PerformanceLaunch/res/values-cs-rCZ/strings.xml
new file mode 100644
index 0000000..78745a1
--- /dev/null
+++ b/tests/perf/PerformanceLaunch/res/values-cs-rCZ/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="many_config_1">ManyConfig1-cs-rCZ</string>
+ <string name="many_config_2">ManyConfig1-cs-rCZ</string>
+ <string name="many_config_3">ManyConfig1-cs-rCZ</string>
+ <string name="many_config_4">ManyConfig1-cs-rCZ</string>
+</resources>
\ No newline at end of file
diff --git a/tests/perf/PerformanceLaunch/res/values-da-rDK/strings.xml b/tests/perf/PerformanceLaunch/res/values-da-rDK/strings.xml
new file mode 100644
index 0000000..4474449
--- /dev/null
+++ b/tests/perf/PerformanceLaunch/res/values-da-rDK/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="many_config_1">ManyConfig1-da-rDK</string>
+ <string name="many_config_2">ManyConfig1-da-rDK</string>
+ <string name="many_config_3">ManyConfig1-da-rDK</string>
+ <string name="many_config_4">ManyConfig1-da-rDK</string>
+</resources>
\ No newline at end of file
diff --git a/tests/perf/PerformanceLaunch/res/values-de-rDE/strings.xml b/tests/perf/PerformanceLaunch/res/values-de-rDE/strings.xml
new file mode 100644
index 0000000..3e4eede
--- /dev/null
+++ b/tests/perf/PerformanceLaunch/res/values-de-rDE/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="many_config_1">ManyConfig1-de-rDE</string>
+ <string name="many_config_2">ManyConfig1-de-rDE</string>
+ <string name="many_config_3">ManyConfig1-de-rDE</string>
+ <string name="many_config_4">ManyConfig1-de-rDE</string>
+</resources>
\ No newline at end of file
diff --git a/tests/perf/PerformanceLaunch/res/values-el-rGR/strings.xml b/tests/perf/PerformanceLaunch/res/values-el-rGR/strings.xml
new file mode 100644
index 0000000..600cb02
--- /dev/null
+++ b/tests/perf/PerformanceLaunch/res/values-el-rGR/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="many_config_1">ManyConfig1-el-rGR</string>
+ <string name="many_config_2">ManyConfig1-el-rGR</string>
+ <string name="many_config_3">ManyConfig1-el-rGR</string>
+ <string name="many_config_4">ManyConfig1-el-rGR</string>
+</resources>
\ No newline at end of file
diff --git a/tests/perf/PerformanceLaunch/res/values-en-rAU/strings.xml b/tests/perf/PerformanceLaunch/res/values-en-rAU/strings.xml
new file mode 100644
index 0000000..28541e3
--- /dev/null
+++ b/tests/perf/PerformanceLaunch/res/values-en-rAU/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="many_config_1">ManyConfig1-en-rAU</string>
+ <string name="many_config_2">ManyConfig1-en-rAU</string>
+ <string name="many_config_3">ManyConfig1-en-rAU</string>
+ <string name="many_config_4">ManyConfig1-en-rAU</string>
+</resources>
\ No newline at end of file
diff --git a/tests/perf/PerformanceLaunch/res/values-en-rGB/strings.xml b/tests/perf/PerformanceLaunch/res/values-en-rGB/strings.xml
new file mode 100644
index 0000000..8fa5df5
--- /dev/null
+++ b/tests/perf/PerformanceLaunch/res/values-en-rGB/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="many_config_1">ManyConfig1-en-rGB</string>
+ <string name="many_config_2">ManyConfig1-en-rGB</string>
+ <string name="many_config_3">ManyConfig1-en-rGB</string>
+ <string name="many_config_4">ManyConfig1-en-rGB</string>
+</resources>
\ No newline at end of file
diff --git a/tests/perf/PerformanceLaunch/res/values-en-rIN/strings.xml b/tests/perf/PerformanceLaunch/res/values-en-rIN/strings.xml
new file mode 100644
index 0000000..e3ed318
--- /dev/null
+++ b/tests/perf/PerformanceLaunch/res/values-en-rIN/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="many_config_1">ManyConfig1-en-rIN</string>
+ <string name="many_config_2">ManyConfig1-en-rIN</string>
+ <string name="many_config_3">ManyConfig1-en-rIN</string>
+ <string name="many_config_4">ManyConfig1-en-rIN</string>
+</resources>
\ No newline at end of file
diff --git a/tests/perf/PerformanceLaunch/res/values-en-rUS/strings.xml b/tests/perf/PerformanceLaunch/res/values-en-rUS/strings.xml
new file mode 100644
index 0000000..aa6e9ab
--- /dev/null
+++ b/tests/perf/PerformanceLaunch/res/values-en-rUS/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="many_config_1">ManyConfig1-en-rUS</string>
+ <string name="many_config_2">ManyConfig1-en-rUS</string>
+ <string name="many_config_3">ManyConfig1-en-rUS</string>
+ <string name="many_config_4">ManyConfig1-en-rUS</string>
+</resources>
\ No newline at end of file
diff --git a/tests/perf/PerformanceLaunch/res/values-en-rXA/strings.xml b/tests/perf/PerformanceLaunch/res/values-en-rXA/strings.xml
new file mode 100644
index 0000000..67cf292
--- /dev/null
+++ b/tests/perf/PerformanceLaunch/res/values-en-rXA/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="many_config_1">ManyConfig1-en-rXA</string>
+ <string name="many_config_2">ManyConfig1-en-rXA</string>
+ <string name="many_config_3">ManyConfig1-en-rXA</string>
+ <string name="many_config_4">ManyConfig1-en-rXA</string>
+</resources>
\ No newline at end of file
diff --git a/tests/perf/PerformanceLaunch/res/values-es-rES/strings.xml b/tests/perf/PerformanceLaunch/res/values-es-rES/strings.xml
new file mode 100644
index 0000000..a40dfaf
--- /dev/null
+++ b/tests/perf/PerformanceLaunch/res/values-es-rES/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="many_config_1">ManyConfig1-es-rES</string>
+ <string name="many_config_2">ManyConfig1-es-rES</string>
+ <string name="many_config_3">ManyConfig1-es-rES</string>
+ <string name="many_config_4">ManyConfig1-es-rES</string>
+</resources>
\ No newline at end of file
diff --git a/tests/perf/PerformanceLaunch/res/values-es-rUS/strings.xml b/tests/perf/PerformanceLaunch/res/values-es-rUS/strings.xml
new file mode 100644
index 0000000..8003c4a
--- /dev/null
+++ b/tests/perf/PerformanceLaunch/res/values-es-rUS/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="many_config_1">ManyConfig1-es-rUS</string>
+ <string name="many_config_2">ManyConfig1-es-rUS</string>
+ <string name="many_config_3">ManyConfig1-es-rUS</string>
+ <string name="many_config_4">ManyConfig1-es-rUS</string>
+</resources>
\ No newline at end of file
diff --git a/tests/perf/PerformanceLaunch/res/values-et-rEE/strings.xml b/tests/perf/PerformanceLaunch/res/values-et-rEE/strings.xml
new file mode 100644
index 0000000..bf3c141
--- /dev/null
+++ b/tests/perf/PerformanceLaunch/res/values-et-rEE/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="many_config_1">ManyConfig1-et-rEE</string>
+ <string name="many_config_2">ManyConfig1-et-rEE</string>
+ <string name="many_config_3">ManyConfig1-et-rEE</string>
+ <string name="many_config_4">ManyConfig1-et-rEE</string>
+</resources>
\ No newline at end of file
diff --git a/tests/perf/PerformanceLaunch/res/values-eu-rES/strings.xml b/tests/perf/PerformanceLaunch/res/values-eu-rES/strings.xml
new file mode 100644
index 0000000..4ea21d6
--- /dev/null
+++ b/tests/perf/PerformanceLaunch/res/values-eu-rES/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="many_config_1">ManyConfig1-eu-rES</string>
+ <string name="many_config_2">ManyConfig1-eu-rES</string>
+ <string name="many_config_3">ManyConfig1-eu-rES</string>
+ <string name="many_config_4">ManyConfig1-eu-rES</string>
+</resources>
\ No newline at end of file
diff --git a/tests/perf/PerformanceLaunch/res/values-fa-rIR/strings.xml b/tests/perf/PerformanceLaunch/res/values-fa-rIR/strings.xml
new file mode 100644
index 0000000..02156e0
--- /dev/null
+++ b/tests/perf/PerformanceLaunch/res/values-fa-rIR/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="many_config_1">ManyConfig1-fa-rIR</string>
+ <string name="many_config_2">ManyConfig1-fa-rIR</string>
+ <string name="many_config_3">ManyConfig1-fa-rIR</string>
+ <string name="many_config_4">ManyConfig1-fa-rIR</string>
+</resources>
\ No newline at end of file
diff --git a/tests/perf/PerformanceLaunch/res/values-fi-rFI/strings.xml b/tests/perf/PerformanceLaunch/res/values-fi-rFI/strings.xml
new file mode 100644
index 0000000..1fe2c98
--- /dev/null
+++ b/tests/perf/PerformanceLaunch/res/values-fi-rFI/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="many_config_1">ManyConfig1-fi-rFI</string>
+ <string name="many_config_2">ManyConfig1-fi-rFI</string>
+ <string name="many_config_3">ManyConfig1-fi-rFI</string>
+ <string name="many_config_4">ManyConfig1-fi-rFI</string>
+</resources>
\ No newline at end of file
diff --git a/tests/perf/PerformanceLaunch/res/values-fr-rCA/strings.xml b/tests/perf/PerformanceLaunch/res/values-fr-rCA/strings.xml
new file mode 100644
index 0000000..a1a8bfd
--- /dev/null
+++ b/tests/perf/PerformanceLaunch/res/values-fr-rCA/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="many_config_1">ManyConfig1-fr-rCA</string>
+ <string name="many_config_2">ManyConfig1-fr-rCA</string>
+ <string name="many_config_3">ManyConfig1-fr-rCA</string>
+ <string name="many_config_4">ManyConfig1-fr-rCA</string>
+</resources>
\ No newline at end of file
diff --git a/tests/perf/PerformanceLaunch/res/values-fr-rFR/strings.xml b/tests/perf/PerformanceLaunch/res/values-fr-rFR/strings.xml
new file mode 100644
index 0000000..6c3405b
--- /dev/null
+++ b/tests/perf/PerformanceLaunch/res/values-fr-rFR/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="many_config_1">ManyConfig1-fr-rFR</string>
+ <string name="many_config_2">ManyConfig1-fr-rFR</string>
+ <string name="many_config_3">ManyConfig1-fr-rFR</string>
+ <string name="many_config_4">ManyConfig1-fr-rFR</string>
+</resources>
\ No newline at end of file
diff --git a/tests/perf/PerformanceLaunch/res/values-gl-rES/strings.xml b/tests/perf/PerformanceLaunch/res/values-gl-rES/strings.xml
new file mode 100644
index 0000000..bbaa529
--- /dev/null
+++ b/tests/perf/PerformanceLaunch/res/values-gl-rES/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="many_config_1">ManyConfig1-gl-rES</string>
+ <string name="many_config_2">ManyConfig1-gl-rES</string>
+ <string name="many_config_3">ManyConfig1-gl-rES</string>
+ <string name="many_config_4">ManyConfig1-gl-rES</string>
+</resources>
\ No newline at end of file
diff --git a/tests/perf/PerformanceLaunch/res/values-hi-rIN/strings.xml b/tests/perf/PerformanceLaunch/res/values-hi-rIN/strings.xml
new file mode 100644
index 0000000..bc56098
--- /dev/null
+++ b/tests/perf/PerformanceLaunch/res/values-hi-rIN/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="many_config_1">ManyConfig1-hi-rIN</string>
+ <string name="many_config_2">ManyConfig1-hi-rIN</string>
+ <string name="many_config_3">ManyConfig1-hi-rIN</string>
+ <string name="many_config_4">ManyConfig1-hi-rIN</string>
+</resources>
\ No newline at end of file
diff --git a/tests/perf/PerformanceLaunch/res/values-hr-rHR/strings.xml b/tests/perf/PerformanceLaunch/res/values-hr-rHR/strings.xml
new file mode 100644
index 0000000..82afaee
--- /dev/null
+++ b/tests/perf/PerformanceLaunch/res/values-hr-rHR/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="many_config_1">ManyConfig1-hr-rHR</string>
+ <string name="many_config_2">ManyConfig1-hr-rHR</string>
+ <string name="many_config_3">ManyConfig1-hr-rHR</string>
+ <string name="many_config_4">ManyConfig1-hr-rHR</string>
+</resources>
\ No newline at end of file
diff --git a/tests/perf/PerformanceLaunch/res/values-hu-rHU/strings.xml b/tests/perf/PerformanceLaunch/res/values-hu-rHU/strings.xml
new file mode 100644
index 0000000..8316f2c
--- /dev/null
+++ b/tests/perf/PerformanceLaunch/res/values-hu-rHU/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="many_config_1">ManyConfig1-hu-rHU</string>
+ <string name="many_config_2">ManyConfig1-hu-rHU</string>
+ <string name="many_config_3">ManyConfig1-hu-rHU</string>
+ <string name="many_config_4">ManyConfig1-hu-rHU</string>
+</resources>
\ No newline at end of file
diff --git a/tests/perf/PerformanceLaunch/res/values-hy-rAM/strings.xml b/tests/perf/PerformanceLaunch/res/values-hy-rAM/strings.xml
new file mode 100644
index 0000000..cec6901
--- /dev/null
+++ b/tests/perf/PerformanceLaunch/res/values-hy-rAM/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="many_config_1">ManyConfig1-hy-rAM</string>
+ <string name="many_config_2">ManyConfig1-hy-rAM</string>
+ <string name="many_config_3">ManyConfig1-hy-rAM</string>
+ <string name="many_config_4">ManyConfig1-hy-rAM</string>
+</resources>
\ No newline at end of file
diff --git a/tests/perf/PerformanceLaunch/res/values-in-rID/strings.xml b/tests/perf/PerformanceLaunch/res/values-in-rID/strings.xml
new file mode 100644
index 0000000..7bceac1
--- /dev/null
+++ b/tests/perf/PerformanceLaunch/res/values-in-rID/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="many_config_1">ManyConfig1-in-rID</string>
+ <string name="many_config_2">ManyConfig1-in-rID</string>
+ <string name="many_config_3">ManyConfig1-in-rID</string>
+ <string name="many_config_4">ManyConfig1-in-rID</string>
+</resources>
\ No newline at end of file
diff --git a/tests/perf/PerformanceLaunch/res/values-is-rIS/strings.xml b/tests/perf/PerformanceLaunch/res/values-is-rIS/strings.xml
new file mode 100644
index 0000000..020f94f
--- /dev/null
+++ b/tests/perf/PerformanceLaunch/res/values-is-rIS/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="many_config_1">ManyConfig1-is-rIS</string>
+ <string name="many_config_2">ManyConfig1-is-rIS</string>
+ <string name="many_config_3">ManyConfig1-is-rIS</string>
+ <string name="many_config_4">ManyConfig1-is-rIS</string>
+</resources>
\ No newline at end of file
diff --git a/tests/perf/PerformanceLaunch/res/values-it-rIT/strings.xml b/tests/perf/PerformanceLaunch/res/values-it-rIT/strings.xml
new file mode 100644
index 0000000..2116434
--- /dev/null
+++ b/tests/perf/PerformanceLaunch/res/values-it-rIT/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="many_config_1">ManyConfig1-it-rIT</string>
+ <string name="many_config_2">ManyConfig1-it-rIT</string>
+ <string name="many_config_3">ManyConfig1-it-rIT</string>
+ <string name="many_config_4">ManyConfig1-it-rIT</string>
+</resources>
\ No newline at end of file
diff --git a/tests/perf/PerformanceLaunch/res/values-iw-rIL/strings.xml b/tests/perf/PerformanceLaunch/res/values-iw-rIL/strings.xml
new file mode 100644
index 0000000..b5a6375
--- /dev/null
+++ b/tests/perf/PerformanceLaunch/res/values-iw-rIL/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="many_config_1">ManyConfig1-iw-rIL</string>
+ <string name="many_config_2">ManyConfig1-iw-rIL</string>
+ <string name="many_config_3">ManyConfig1-iw-rIL</string>
+ <string name="many_config_4">ManyConfig1-iw-rIL</string>
+</resources>
\ No newline at end of file
diff --git a/tests/perf/PerformanceLaunch/res/values-ja-rJP/strings.xml b/tests/perf/PerformanceLaunch/res/values-ja-rJP/strings.xml
new file mode 100644
index 0000000..a5ec3e2
--- /dev/null
+++ b/tests/perf/PerformanceLaunch/res/values-ja-rJP/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="many_config_1">ManyConfig1-ja-rJP</string>
+ <string name="many_config_2">ManyConfig1-ja-rJP</string>
+ <string name="many_config_3">ManyConfig1-ja-rJP</string>
+ <string name="many_config_4">ManyConfig1-ja-rJP</string>
+</resources>
\ No newline at end of file
diff --git a/tests/perf/PerformanceLaunch/res/values-ka-rGE/strings.xml b/tests/perf/PerformanceLaunch/res/values-ka-rGE/strings.xml
new file mode 100644
index 0000000..8656e74
--- /dev/null
+++ b/tests/perf/PerformanceLaunch/res/values-ka-rGE/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="many_config_1">ManyConfig1-ka-rGE</string>
+ <string name="many_config_2">ManyConfig1-ka-rGE</string>
+ <string name="many_config_3">ManyConfig1-ka-rGE</string>
+ <string name="many_config_4">ManyConfig1-ka-rGE</string>
+</resources>
\ No newline at end of file
diff --git a/tests/perf/PerformanceLaunch/res/values-km-rKH/strings.xml b/tests/perf/PerformanceLaunch/res/values-km-rKH/strings.xml
new file mode 100644
index 0000000..188197d
--- /dev/null
+++ b/tests/perf/PerformanceLaunch/res/values-km-rKH/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="many_config_1">ManyConfig1-km-rKH</string>
+ <string name="many_config_2">ManyConfig1-km-rKH</string>
+ <string name="many_config_3">ManyConfig1-km-rKH</string>
+ <string name="many_config_4">ManyConfig1-km-rKH</string>
+</resources>
\ No newline at end of file
diff --git a/tests/perf/PerformanceLaunch/res/values-km-rMH/strings.xml b/tests/perf/PerformanceLaunch/res/values-km-rMH/strings.xml
new file mode 100644
index 0000000..fea5400
--- /dev/null
+++ b/tests/perf/PerformanceLaunch/res/values-km-rMH/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="many_config_1">ManyConfig1-km-rMH</string>
+ <string name="many_config_2">ManyConfig1-km-rMH</string>
+ <string name="many_config_3">ManyConfig1-km-rMH</string>
+ <string name="many_config_4">ManyConfig1-km-rMH</string>
+</resources>
\ No newline at end of file
diff --git a/tests/perf/PerformanceLaunch/res/values-kn-rIN/strings.xml b/tests/perf/PerformanceLaunch/res/values-kn-rIN/strings.xml
new file mode 100644
index 0000000..3490363
--- /dev/null
+++ b/tests/perf/PerformanceLaunch/res/values-kn-rIN/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="many_config_1">ManyConfig1-kn-rIN</string>
+ <string name="many_config_2">ManyConfig1-kn-rIN</string>
+ <string name="many_config_3">ManyConfig1-kn-rIN</string>
+ <string name="many_config_4">ManyConfig1-kn-rIN</string>
+</resources>
\ No newline at end of file
diff --git a/tests/perf/PerformanceLaunch/res/values-ko-rKR/strings.xml b/tests/perf/PerformanceLaunch/res/values-ko-rKR/strings.xml
new file mode 100644
index 0000000..d9fac7d
--- /dev/null
+++ b/tests/perf/PerformanceLaunch/res/values-ko-rKR/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="many_config_1">ManyConfig1-ko-rKR</string>
+ <string name="many_config_2">ManyConfig1-ko-rKR</string>
+ <string name="many_config_3">ManyConfig1-ko-rKR</string>
+ <string name="many_config_4">ManyConfig1-ko-rKR</string>
+</resources>
\ No newline at end of file
diff --git a/tests/perf/PerformanceLaunch/res/values-ky-rKG/strings.xml b/tests/perf/PerformanceLaunch/res/values-ky-rKG/strings.xml
new file mode 100644
index 0000000..ec66c79
--- /dev/null
+++ b/tests/perf/PerformanceLaunch/res/values-ky-rKG/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="many_config_1">ManyConfig1-ky-rKG</string>
+ <string name="many_config_2">ManyConfig1-ky-rKG</string>
+ <string name="many_config_3">ManyConfig1-ky-rKG</string>
+ <string name="many_config_4">ManyConfig1-ky-rKG</string>
+</resources>
\ No newline at end of file
diff --git a/tests/perf/PerformanceLaunch/res/values-lo-rLA/strings.xml b/tests/perf/PerformanceLaunch/res/values-lo-rLA/strings.xml
new file mode 100644
index 0000000..94d676c
--- /dev/null
+++ b/tests/perf/PerformanceLaunch/res/values-lo-rLA/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="many_config_1">ManyConfig1-lo-rLA</string>
+ <string name="many_config_2">ManyConfig1-lo-rLA</string>
+ <string name="many_config_3">ManyConfig1-lo-rLA</string>
+ <string name="many_config_4">ManyConfig1-lo-rLA</string>
+</resources>
\ No newline at end of file
diff --git a/tests/perf/PerformanceLaunch/res/values-lt-rLT/strings.xml b/tests/perf/PerformanceLaunch/res/values-lt-rLT/strings.xml
new file mode 100644
index 0000000..7659da8
--- /dev/null
+++ b/tests/perf/PerformanceLaunch/res/values-lt-rLT/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="many_config_1">ManyConfig1-lt-rLT</string>
+ <string name="many_config_2">ManyConfig1-lt-rLT</string>
+ <string name="many_config_3">ManyConfig1-lt-rLT</string>
+ <string name="many_config_4">ManyConfig1-lt-rLT</string>
+</resources>
\ No newline at end of file
diff --git a/tests/perf/PerformanceLaunch/res/values-lv-rLV/strings.xml b/tests/perf/PerformanceLaunch/res/values-lv-rLV/strings.xml
new file mode 100644
index 0000000..cf2f3bb
--- /dev/null
+++ b/tests/perf/PerformanceLaunch/res/values-lv-rLV/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="many_config_1">ManyConfig1-lv-rLV</string>
+ <string name="many_config_2">ManyConfig1-lv-rLV</string>
+ <string name="many_config_3">ManyConfig1-lv-rLV</string>
+ <string name="many_config_4">ManyConfig1-lv-rLV</string>
+</resources>
\ No newline at end of file
diff --git a/tests/perf/PerformanceLaunch/res/values-mk-rMK/strings.xml b/tests/perf/PerformanceLaunch/res/values-mk-rMK/strings.xml
new file mode 100644
index 0000000..9e65f29
--- /dev/null
+++ b/tests/perf/PerformanceLaunch/res/values-mk-rMK/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="many_config_1">ManyConfig1-mk-rMK</string>
+ <string name="many_config_2">ManyConfig1-mk-rMK</string>
+ <string name="many_config_3">ManyConfig1-mk-rMK</string>
+ <string name="many_config_4">ManyConfig1-mk-rMK</string>
+</resources>
\ No newline at end of file
diff --git a/tests/perf/PerformanceLaunch/res/values-ml-rIN/strings.xml b/tests/perf/PerformanceLaunch/res/values-ml-rIN/strings.xml
new file mode 100644
index 0000000..c5aede0
--- /dev/null
+++ b/tests/perf/PerformanceLaunch/res/values-ml-rIN/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="many_config_1">ManyConfig1-ml-rIN</string>
+ <string name="many_config_2">ManyConfig1-ml-rIN</string>
+ <string name="many_config_3">ManyConfig1-ml-rIN</string>
+ <string name="many_config_4">ManyConfig1-ml-rIN</string>
+</resources>
\ No newline at end of file
diff --git a/tests/perf/PerformanceLaunch/res/values-mn-rMN/strings.xml b/tests/perf/PerformanceLaunch/res/values-mn-rMN/strings.xml
new file mode 100644
index 0000000..2d369f0
--- /dev/null
+++ b/tests/perf/PerformanceLaunch/res/values-mn-rMN/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="many_config_1">ManyConfig1-mn-rMN</string>
+ <string name="many_config_2">ManyConfig1-mn-rMN</string>
+ <string name="many_config_3">ManyConfig1-mn-rMN</string>
+ <string name="many_config_4">ManyConfig1-mn-rMN</string>
+</resources>
\ No newline at end of file
diff --git a/tests/perf/PerformanceLaunch/res/values-mr-rIN/strings.xml b/tests/perf/PerformanceLaunch/res/values-mr-rIN/strings.xml
new file mode 100644
index 0000000..63517b8
--- /dev/null
+++ b/tests/perf/PerformanceLaunch/res/values-mr-rIN/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="many_config_1">ManyConfig1-mr-rIN</string>
+ <string name="many_config_2">ManyConfig1-mr-rIN</string>
+ <string name="many_config_3">ManyConfig1-mr-rIN</string>
+ <string name="many_config_4">ManyConfig1-mr-rIN</string>
+</resources>
\ No newline at end of file
diff --git a/tests/perf/PerformanceLaunch/res/values-ms-rMY/strings.xml b/tests/perf/PerformanceLaunch/res/values-ms-rMY/strings.xml
new file mode 100644
index 0000000..4d2b27b
--- /dev/null
+++ b/tests/perf/PerformanceLaunch/res/values-ms-rMY/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="many_config_1">ManyConfig1-ms-rMY</string>
+ <string name="many_config_2">ManyConfig1-ms-rMY</string>
+ <string name="many_config_3">ManyConfig1-ms-rMY</string>
+ <string name="many_config_4">ManyConfig1-ms-rMY</string>
+</resources>
\ No newline at end of file
diff --git a/tests/perf/PerformanceLaunch/res/values-my-rMM/strings.xml b/tests/perf/PerformanceLaunch/res/values-my-rMM/strings.xml
new file mode 100644
index 0000000..1949158
--- /dev/null
+++ b/tests/perf/PerformanceLaunch/res/values-my-rMM/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="many_config_1">ManyConfig1-my-rMM</string>
+ <string name="many_config_2">ManyConfig1-my-rMM</string>
+ <string name="many_config_3">ManyConfig1-my-rMM</string>
+ <string name="many_config_4">ManyConfig1-my-rMM</string>
+</resources>
\ No newline at end of file
diff --git a/tests/perf/PerformanceLaunch/res/values-nb-rNO/strings.xml b/tests/perf/PerformanceLaunch/res/values-nb-rNO/strings.xml
new file mode 100644
index 0000000..1f2eaa7
--- /dev/null
+++ b/tests/perf/PerformanceLaunch/res/values-nb-rNO/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="many_config_1">ManyConfig1-nb-rNO</string>
+ <string name="many_config_2">ManyConfig1-nb-rNO</string>
+ <string name="many_config_3">ManyConfig1-nb-rNO</string>
+ <string name="many_config_4">ManyConfig1-nb-rNO</string>
+</resources>
\ No newline at end of file
diff --git a/tests/perf/PerformanceLaunch/res/values-ne-rNP/strings.xml b/tests/perf/PerformanceLaunch/res/values-ne-rNP/strings.xml
new file mode 100644
index 0000000..fa1d743
--- /dev/null
+++ b/tests/perf/PerformanceLaunch/res/values-ne-rNP/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="many_config_1">ManyConfig1-ne-rNP</string>
+ <string name="many_config_2">ManyConfig1-ne-rNP</string>
+ <string name="many_config_3">ManyConfig1-ne-rNP</string>
+ <string name="many_config_4">ManyConfig1-ne-rNP</string>
+</resources>
\ No newline at end of file
diff --git a/tests/perf/PerformanceLaunch/res/values-nl-rNL/strings.xml b/tests/perf/PerformanceLaunch/res/values-nl-rNL/strings.xml
new file mode 100644
index 0000000..440f648
--- /dev/null
+++ b/tests/perf/PerformanceLaunch/res/values-nl-rNL/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="many_config_1">ManyConfig1-nl-rNL</string>
+ <string name="many_config_2">ManyConfig1-nl-rNL</string>
+ <string name="many_config_3">ManyConfig1-nl-rNL</string>
+ <string name="many_config_4">ManyConfig1-nl-rNL</string>
+</resources>
\ No newline at end of file
diff --git a/tests/perf/PerformanceLaunch/res/values-pl-rPL/strings.xml b/tests/perf/PerformanceLaunch/res/values-pl-rPL/strings.xml
new file mode 100644
index 0000000..4920307
--- /dev/null
+++ b/tests/perf/PerformanceLaunch/res/values-pl-rPL/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="many_config_1">ManyConfig1-pl-rPL</string>
+ <string name="many_config_2">ManyConfig1-pl-rPL</string>
+ <string name="many_config_3">ManyConfig1-pl-rPL</string>
+ <string name="many_config_4">ManyConfig1-pl-rPL</string>
+</resources>
\ No newline at end of file
diff --git a/tests/perf/PerformanceLaunch/res/values-pt-rBR/strings.xml b/tests/perf/PerformanceLaunch/res/values-pt-rBR/strings.xml
new file mode 100644
index 0000000..cb8d260
--- /dev/null
+++ b/tests/perf/PerformanceLaunch/res/values-pt-rBR/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="many_config_1">ManyConfig1-pt-rBR</string>
+ <string name="many_config_2">ManyConfig1-pt-rBR</string>
+ <string name="many_config_3">ManyConfig1-pt-rBR</string>
+ <string name="many_config_4">ManyConfig1-pt-rBR</string>
+</resources>
\ No newline at end of file
diff --git a/tests/perf/PerformanceLaunch/res/values-pt-rPT/strings.xml b/tests/perf/PerformanceLaunch/res/values-pt-rPT/strings.xml
new file mode 100644
index 0000000..c74ad76
--- /dev/null
+++ b/tests/perf/PerformanceLaunch/res/values-pt-rPT/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="many_config_1">ManyConfig1-pt-rPT</string>
+ <string name="many_config_2">ManyConfig1-pt-rPT</string>
+ <string name="many_config_3">ManyConfig1-pt-rPT</string>
+ <string name="many_config_4">ManyConfig1-pt-rPT</string>
+</resources>
\ No newline at end of file
diff --git a/tests/perf/PerformanceLaunch/res/values-ro-rRO/strings.xml b/tests/perf/PerformanceLaunch/res/values-ro-rRO/strings.xml
new file mode 100644
index 0000000..08699c5
--- /dev/null
+++ b/tests/perf/PerformanceLaunch/res/values-ro-rRO/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="many_config_1">ManyConfig1-ro-rRO</string>
+ <string name="many_config_2">ManyConfig1-ro-rRO</string>
+ <string name="many_config_3">ManyConfig1-ro-rRO</string>
+ <string name="many_config_4">ManyConfig1-ro-rRO</string>
+</resources>
\ No newline at end of file
diff --git a/tests/perf/PerformanceLaunch/res/values-ru-rRU/strings.xml b/tests/perf/PerformanceLaunch/res/values-ru-rRU/strings.xml
new file mode 100644
index 0000000..afe80bb
--- /dev/null
+++ b/tests/perf/PerformanceLaunch/res/values-ru-rRU/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="many_config_1">ManyConfig1-ru-rRU</string>
+ <string name="many_config_2">ManyConfig1-ru-rRU</string>
+ <string name="many_config_3">ManyConfig1-ru-rRU</string>
+ <string name="many_config_4">ManyConfig1-ru-rRU</string>
+</resources>
\ No newline at end of file
diff --git a/tests/perf/PerformanceLaunch/res/values-si-rLK/strings.xml b/tests/perf/PerformanceLaunch/res/values-si-rLK/strings.xml
new file mode 100644
index 0000000..5e9d6a4
--- /dev/null
+++ b/tests/perf/PerformanceLaunch/res/values-si-rLK/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="many_config_1">ManyConfig1-si-rLK</string>
+ <string name="many_config_2">ManyConfig1-si-rLK</string>
+ <string name="many_config_3">ManyConfig1-si-rLK</string>
+ <string name="many_config_4">ManyConfig1-si-rLK</string>
+</resources>
\ No newline at end of file
diff --git a/tests/perf/PerformanceLaunch/res/values-sk-rSK/strings.xml b/tests/perf/PerformanceLaunch/res/values-sk-rSK/strings.xml
new file mode 100644
index 0000000..4a9ad58
--- /dev/null
+++ b/tests/perf/PerformanceLaunch/res/values-sk-rSK/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="many_config_1">ManyConfig1-sk-rSK</string>
+ <string name="many_config_2">ManyConfig1-sk-rSK</string>
+ <string name="many_config_3">ManyConfig1-sk-rSK</string>
+ <string name="many_config_4">ManyConfig1-sk-rSK</string>
+</resources>
\ No newline at end of file
diff --git a/tests/perf/PerformanceLaunch/res/values-sl-rSI/strings.xml b/tests/perf/PerformanceLaunch/res/values-sl-rSI/strings.xml
new file mode 100644
index 0000000..41afd9d
--- /dev/null
+++ b/tests/perf/PerformanceLaunch/res/values-sl-rSI/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="many_config_1">ManyConfig1-sl-rSI</string>
+ <string name="many_config_2">ManyConfig1-sl-rSI</string>
+ <string name="many_config_3">ManyConfig1-sl-rSI</string>
+ <string name="many_config_4">ManyConfig1-sl-rSI</string>
+</resources>
\ No newline at end of file
diff --git a/tests/perf/PerformanceLaunch/res/values-sr-rRS/strings.xml b/tests/perf/PerformanceLaunch/res/values-sr-rRS/strings.xml
new file mode 100644
index 0000000..d762a50
--- /dev/null
+++ b/tests/perf/PerformanceLaunch/res/values-sr-rRS/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="many_config_1">ManyConfig1-sr-rRS</string>
+ <string name="many_config_2">ManyConfig1-sr-rRS</string>
+ <string name="many_config_3">ManyConfig1-sr-rRS</string>
+ <string name="many_config_4">ManyConfig1-sr-rRS</string>
+</resources>
\ No newline at end of file
diff --git a/tests/perf/PerformanceLaunch/res/values-sv-rSE/strings.xml b/tests/perf/PerformanceLaunch/res/values-sv-rSE/strings.xml
new file mode 100644
index 0000000..5dbf904
--- /dev/null
+++ b/tests/perf/PerformanceLaunch/res/values-sv-rSE/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="many_config_1">ManyConfig1-sv-rSE</string>
+ <string name="many_config_2">ManyConfig1-sv-rSE</string>
+ <string name="many_config_3">ManyConfig1-sv-rSE</string>
+ <string name="many_config_4">ManyConfig1-sv-rSE</string>
+</resources>
\ No newline at end of file
diff --git a/tests/perf/PerformanceLaunch/res/values-sw-rTZ/strings.xml b/tests/perf/PerformanceLaunch/res/values-sw-rTZ/strings.xml
new file mode 100644
index 0000000..189ce86
--- /dev/null
+++ b/tests/perf/PerformanceLaunch/res/values-sw-rTZ/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="many_config_1">ManyConfig1-sw-rTZ</string>
+ <string name="many_config_2">ManyConfig1-sw-rTZ</string>
+ <string name="many_config_3">ManyConfig1-sw-rTZ</string>
+ <string name="many_config_4">ManyConfig1-sw-rTZ</string>
+</resources>
\ No newline at end of file
diff --git a/tests/perf/PerformanceLaunch/res/values-ta-rIN/strings.xml b/tests/perf/PerformanceLaunch/res/values-ta-rIN/strings.xml
new file mode 100644
index 0000000..ded7c28
--- /dev/null
+++ b/tests/perf/PerformanceLaunch/res/values-ta-rIN/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="many_config_1">ManyConfig1-ta-rIN</string>
+ <string name="many_config_2">ManyConfig1-ta-rIN</string>
+ <string name="many_config_3">ManyConfig1-ta-rIN</string>
+ <string name="many_config_4">ManyConfig1-ta-rIN</string>
+</resources>
\ No newline at end of file
diff --git a/tests/perf/PerformanceLaunch/res/values-te-rIN/strings.xml b/tests/perf/PerformanceLaunch/res/values-te-rIN/strings.xml
new file mode 100644
index 0000000..bd442e9
--- /dev/null
+++ b/tests/perf/PerformanceLaunch/res/values-te-rIN/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="many_config_1">ManyConfig1-te-rIN</string>
+ <string name="many_config_2">ManyConfig1-te-rIN</string>
+ <string name="many_config_3">ManyConfig1-te-rIN</string>
+ <string name="many_config_4">ManyConfig1-te-rIN</string>
+</resources>
\ No newline at end of file
diff --git a/tests/perf/PerformanceLaunch/res/values-th-rTH/strings.xml b/tests/perf/PerformanceLaunch/res/values-th-rTH/strings.xml
new file mode 100644
index 0000000..1fcb347
--- /dev/null
+++ b/tests/perf/PerformanceLaunch/res/values-th-rTH/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="many_config_1">ManyConfig1-th-rTH</string>
+ <string name="many_config_2">ManyConfig1-th-rTH</string>
+ <string name="many_config_3">ManyConfig1-th-rTH</string>
+ <string name="many_config_4">ManyConfig1-th-rTH</string>
+</resources>
\ No newline at end of file
diff --git a/tests/perf/PerformanceLaunch/res/values-tl-rPH/strings.xml b/tests/perf/PerformanceLaunch/res/values-tl-rPH/strings.xml
new file mode 100644
index 0000000..5d455f4
--- /dev/null
+++ b/tests/perf/PerformanceLaunch/res/values-tl-rPH/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="many_config_1">ManyConfig1-tl-rPH</string>
+ <string name="many_config_2">ManyConfig1-tl-rPH</string>
+ <string name="many_config_3">ManyConfig1-tl-rPH</string>
+ <string name="many_config_4">ManyConfig1-tl-rPH</string>
+</resources>
\ No newline at end of file
diff --git a/tests/perf/PerformanceLaunch/res/values-tr-rTR/strings.xml b/tests/perf/PerformanceLaunch/res/values-tr-rTR/strings.xml
new file mode 100644
index 0000000..1516193
--- /dev/null
+++ b/tests/perf/PerformanceLaunch/res/values-tr-rTR/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="many_config_1">ManyConfig1-tr-rTR</string>
+ <string name="many_config_2">ManyConfig1-tr-rTR</string>
+ <string name="many_config_3">ManyConfig1-tr-rTR</string>
+ <string name="many_config_4">ManyConfig1-tr-rTR</string>
+</resources>
\ No newline at end of file
diff --git a/tests/perf/PerformanceLaunch/res/values-uk-rUA/strings.xml b/tests/perf/PerformanceLaunch/res/values-uk-rUA/strings.xml
new file mode 100644
index 0000000..a2d1dfe
--- /dev/null
+++ b/tests/perf/PerformanceLaunch/res/values-uk-rUA/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="many_config_1">ManyConfig1-uk-rUA</string>
+ <string name="many_config_2">ManyConfig1-uk-rUA</string>
+ <string name="many_config_3">ManyConfig1-uk-rUA</string>
+ <string name="many_config_4">ManyConfig1-uk-rUA</string>
+</resources>
\ No newline at end of file
diff --git a/tests/perf/PerformanceLaunch/res/values-v11/styles.xml b/tests/perf/PerformanceLaunch/res/values-v11/styles.xml
deleted file mode 100644
index 3c02242..0000000
--- a/tests/perf/PerformanceLaunch/res/values-v11/styles.xml
+++ /dev/null
@@ -1,11 +0,0 @@
-<resources>
-
- <!--
- Base application theme for API 11+. This theme completely replaces
- AppBaseTheme from res/values/styles.xml on API 11+ devices.
- -->
- <style name="AppBaseTheme" parent="android:Theme.Holo.Light">
- <!-- API 11 theme customizations can go here. -->
- </style>
-
-</resources>
diff --git a/tests/perf/PerformanceLaunch/res/values-v14/styles.xml b/tests/perf/PerformanceLaunch/res/values-v14/styles.xml
deleted file mode 100644
index a91fd03..0000000
--- a/tests/perf/PerformanceLaunch/res/values-v14/styles.xml
+++ /dev/null
@@ -1,12 +0,0 @@
-<resources>
-
- <!--
- Base application theme for API 14+. This theme completely replaces
- AppBaseTheme from BOTH res/values/styles.xml and
- res/values-v11/styles.xml on API 14+ devices.
- -->
- <style name="AppBaseTheme" parent="android:Theme.Holo.Light.DarkActionBar">
- <!-- API 14 theme customizations can go here. -->
- </style>
-
-</resources>
diff --git a/tests/perf/PerformanceLaunch/res/values-v21/styles.xml b/tests/perf/PerformanceLaunch/res/values-v21/styles.xml
new file mode 100644
index 0000000..ea65cee
--- /dev/null
+++ b/tests/perf/PerformanceLaunch/res/values-v21/styles.xml
@@ -0,0 +1,4 @@
+<resources>
+ <style name="AppBaseTheme" parent="android:Theme.Material.Light">
+ </style>
+</resources>
diff --git a/tests/perf/PerformanceLaunch/res/values-vi-rVN/strings.xml b/tests/perf/PerformanceLaunch/res/values-vi-rVN/strings.xml
new file mode 100644
index 0000000..a5d9543
--- /dev/null
+++ b/tests/perf/PerformanceLaunch/res/values-vi-rVN/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="many_config_1">ManyConfig1-vi-rVN</string>
+ <string name="many_config_2">ManyConfig1-vi-rVN</string>
+ <string name="many_config_3">ManyConfig1-vi-rVN</string>
+ <string name="many_config_4">ManyConfig1-vi-rVN</string>
+</resources>
\ No newline at end of file
diff --git a/tests/perf/PerformanceLaunch/res/values-zh-rCN/strings.xml b/tests/perf/PerformanceLaunch/res/values-zh-rCN/strings.xml
new file mode 100644
index 0000000..f48c4d9
--- /dev/null
+++ b/tests/perf/PerformanceLaunch/res/values-zh-rCN/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="many_config_1">ManyConfig1-zh-rCN</string>
+ <string name="many_config_2">ManyConfig1-zh-rCN</string>
+ <string name="many_config_3">ManyConfig1-zh-rCN</string>
+ <string name="many_config_4">ManyConfig1-zh-rCN</string>
+</resources>
\ No newline at end of file
diff --git a/tests/perf/PerformanceLaunch/res/values-zh-rHK/strings.xml b/tests/perf/PerformanceLaunch/res/values-zh-rHK/strings.xml
new file mode 100644
index 0000000..6b043a8
--- /dev/null
+++ b/tests/perf/PerformanceLaunch/res/values-zh-rHK/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="many_config_1">ManyConfig1-zh-rHK</string>
+ <string name="many_config_2">ManyConfig1-zh-rHK</string>
+ <string name="many_config_3">ManyConfig1-zh-rHK</string>
+ <string name="many_config_4">ManyConfig1-zh-rHK</string>
+</resources>
\ No newline at end of file
diff --git a/tests/perf/PerformanceLaunch/res/values-zh-rTW/strings.xml b/tests/perf/PerformanceLaunch/res/values-zh-rTW/strings.xml
new file mode 100644
index 0000000..6992c18
--- /dev/null
+++ b/tests/perf/PerformanceLaunch/res/values-zh-rTW/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="many_config_1">ManyConfig1-zh-rTW</string>
+ <string name="many_config_2">ManyConfig1-zh-rTW</string>
+ <string name="many_config_3">ManyConfig1-zh-rTW</string>
+ <string name="many_config_4">ManyConfig1-zh-rTW</string>
+</resources>
\ No newline at end of file
diff --git a/tests/perf/PerformanceLaunch/res/values-zu-rZA/strings.xml b/tests/perf/PerformanceLaunch/res/values-zu-rZA/strings.xml
new file mode 100644
index 0000000..35ec37b
--- /dev/null
+++ b/tests/perf/PerformanceLaunch/res/values-zu-rZA/strings.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <string name="many_config_1">ManyConfig1-zu-rZA</string>
+ <string name="many_config_2">ManyConfig1-zu-rZA</string>
+ <string name="many_config_3">ManyConfig1-zu-rZA</string>
+ <string name="many_config_4">ManyConfig1-zu-rZA</string>
+</resources>
\ No newline at end of file
diff --git a/tests/perf/PerformanceLaunch/res/values/strings.xml b/tests/perf/PerformanceLaunch/res/values/strings.xml
index 59b0466..93f5a3e 100644
--- a/tests/perf/PerformanceLaunch/res/values/strings.xml
+++ b/tests/perf/PerformanceLaunch/res/values/strings.xml
@@ -7,4 +7,17 @@
<string name="pass">Password:</string>
<string name="Login">Sign In</string>
-</resources>
\ No newline at end of file
+ <!-- Used in ManyConfigResourceActivity -->
+
+ <string-array name="many_configs">
+ <item>@string/many_config_1</item>
+ <item>@string/many_config_2</item>
+ <item>@string/many_config_3</item>
+ <item>@string/many_config_4</item>
+ </string-array>
+
+ <string name="many_config_1">ManyConfig1</string>
+ <string name="many_config_2">ManyConfig2</string>
+ <string name="many_config_3">ManyConfig3</string>
+ <string name="many_config_4">ManyConfig4</string>
+</resources>
diff --git a/tests/perf/PerformanceLaunch/src/com/android/performanceLaunch/ManyConfigResourceActivity.java b/tests/perf/PerformanceLaunch/src/com/android/performanceLaunch/ManyConfigResourceActivity.java
new file mode 100644
index 0000000..d82457a
--- /dev/null
+++ b/tests/perf/PerformanceLaunch/src/com/android/performanceLaunch/ManyConfigResourceActivity.java
@@ -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.
+ */
+package com.android.performanceLaunch;
+
+import android.annotation.Nullable;
+import android.app.Activity;
+import android.os.Bundle;
+
+public class ManyConfigResourceActivity extends Activity {
+ @Override
+ protected void onCreate(@Nullable Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ for (int i = 0; i < 1000; i++) {
+ getResources().getStringArray(R.array.many_configs);
+ }
+ }
+}
diff --git a/tests/smokefast/.gitignore b/tests/smokefast/.gitignore
new file mode 100644
index 0000000..77120d7
--- /dev/null
+++ b/tests/smokefast/.gitignore
@@ -0,0 +1,5 @@
+bin/
+gen/
+.settings/
+.classpath
+.project
diff --git a/tests/smokefast/Android.mk b/tests/smokefast/Android.mk
new file mode 100644
index 0000000..93910c6
--- /dev/null
+++ b/tests/smokefast/Android.mk
@@ -0,0 +1,18 @@
+LOCAL_PATH := $(call my-dir)
+include $(CLEAR_VARS)
+
+LOCAL_MODULE_TAGS := tests
+
+LOCAL_SDK_VERSION := current
+
+media_framework_app_base := frameworks/base/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest
+
+LOCAL_SRC_FILES := $(call all-subdir-java-files)
+LOCAL_JAVA_LIBRARIES := android.test.runner
+LOCAL_STATIC_JAVA_LIBRARIES := android-support-test ub-uiautomator
+# TODO: AuptLib and aupt-helpers should be static deps as well
+
+LOCAL_PACKAGE_NAME := SmokeFastTests
+LOCAL_CERTIFICATE := platform
+
+include $(BUILD_PACKAGE)
diff --git a/tests/smokefast/AndroidManifest.xml b/tests/smokefast/AndroidManifest.xml
new file mode 100644
index 0000000..a2ed58f
--- /dev/null
+++ b/tests/smokefast/AndroidManifest.xml
@@ -0,0 +1,51 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Copyright 2016 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.android.smokefast">
+
+ <uses-sdk android:minSdkVersion="23"
+ android:targetSdkVersion="23" />
+ <uses-feature android:name="android.hardware.camera"
+ android:required="true" />
+ <uses-feature android:name="android.hardware.telephony"
+ android:required="false" />
+ <uses-permission android:name="android.permission.CAMERA" />
+ <uses-permission android:name="android.permission.WAKE_LOCK" />
+ <uses-permission android:name="android.permission.SEND_SMS" />
+ <uses-permission android:name="android.permission.CAPTURE_VIDEO_OUTPUT" />
+ <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
+ <uses-permission android:name="android.permission.CALL_PHONE" />
+ <uses-permission android:name="android.permission.READ_PHONE_STATE" />
+ <uses-permission android:name="android.permission.INTERNET" />
+ <uses-permission android:name="android.permission.GET_ACCOUNTS" />
+ <application>
+ <uses-library android:name="android.test.runner" />
+ <activity android:label="MediaPlaybackTest"
+ android:name=".app.MediaPlaybackTestApp"
+ android:screenOrientation="landscape">
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN" />
+ <category android:name="android.intent.category.LAUNCHER" />
+ </intent-filter>
+ </activity>
+ </application>
+ <instrumentation
+ android:name="android.test.InstrumentationTestRunner"
+ android:targetPackage="com.android.smokefast"
+ android:label="SmokeFAST Tests" />
+</manifest>
diff --git a/tests/smokefast/project.properties b/tests/smokefast/project.properties
new file mode 100644
index 0000000..8451e89
--- /dev/null
+++ b/tests/smokefast/project.properties
@@ -0,0 +1 @@
+target=android-23
diff --git a/tests/smokefast/res/layout/surface_view.xml b/tests/smokefast/res/layout/surface_view.xml
new file mode 100644
index 0000000..4999e5d
--- /dev/null
+++ b/tests/smokefast/res/layout/surface_view.xml
@@ -0,0 +1,46 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2011 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical">
+
+ <FrameLayout
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <SurfaceView
+ android:id="@+id/surface_view"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_centerInParent="true"
+ />
+
+ <ImageView android:id="@+id/overlay_layer"
+ android:layout_width="0dip"
+ android:layout_height="392dip"/>
+
+ <VideoView
+ android:id="@+id/video_view"
+ android:layout_width="320px"
+ android:layout_height="240px"
+ />
+
+ </FrameLayout>
+
+</LinearLayout>
+
diff --git a/tests/smokefast/res/raw/bbb.mkv b/tests/smokefast/res/raw/bbb.mkv
new file mode 100644
index 0000000..e286e01
--- /dev/null
+++ b/tests/smokefast/res/raw/bbb.mkv
Binary files differ
diff --git a/tests/smokefast/src/com/android/smokefast/LockscreenTest.java b/tests/smokefast/src/com/android/smokefast/LockscreenTest.java
new file mode 100644
index 0000000..6a8dba6
--- /dev/null
+++ b/tests/smokefast/src/com/android/smokefast/LockscreenTest.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.smokefast;
+
+import android.os.SystemClock;
+import android.support.test.uiautomator.By;
+import android.support.test.uiautomator.Direction;
+import android.support.test.uiautomator.UiDevice;
+import android.support.test.uiautomator.UiObject2;
+import android.support.test.uiautomator.Until;
+import android.test.InstrumentationTestCase;
+import android.test.suitebuilder.annotation.LargeTest;
+
+public class LockscreenTest extends InstrumentationTestCase {
+ private static final String LAUNCHER_PACKAGE = "com.google.android.googlequicksearchbox";
+ private static final String SYSTEMUI_PACKAGE = "com.android.systemui";
+ private UiDevice mDevice;
+
+ @Override
+ public void setUp() throws Exception {
+ super.setUp();
+ mDevice = UiDevice.getInstance(getInstrumentation());
+ mDevice.freezeRotation();
+ }
+
+ @Override
+ public void tearDown() throws Exception {
+ mDevice.wakeUp();
+ mDevice.pressMenu();
+ mDevice.unfreezeRotation();
+ super.tearDown();
+ }
+
+ @LargeTest
+ public void testSlideUnlock() throws Exception {
+ sleepAndWakeUpDevice();
+ mDevice.wait(Until.findObject(
+ By.res(SYSTEMUI_PACKAGE, "notification_stack_scroller")), 2000)
+ .swipe(Direction.UP, 1.0f);
+ int counter = 6;
+ UiObject2 workspace = mDevice.findObject(By.res(LAUNCHER_PACKAGE, "workspace"));
+ while (counter-- > 0 && workspace == null) {
+ workspace = mDevice.findObject(By.res(LAUNCHER_PACKAGE, "workspace"));
+ SystemClock.sleep(500);
+ }
+ assertNotNull("Workspace wasn't found", workspace);
+ }
+
+ private void sleepAndWakeUpDevice() throws Exception {
+ mDevice.sleep();
+ SystemClock.sleep(1000);
+ mDevice.wakeUp();
+ }
+}
diff --git a/tests/smokefast/src/com/android/smokefast/MediaCaptureTest.java b/tests/smokefast/src/com/android/smokefast/MediaCaptureTest.java
new file mode 100644
index 0000000..e7b3798
--- /dev/null
+++ b/tests/smokefast/src/com/android/smokefast/MediaCaptureTest.java
@@ -0,0 +1,142 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.smokefast;
+
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Environment;
+import android.provider.MediaStore;
+import android.support.test.uiautomator.By;
+import android.support.test.uiautomator.UiDevice;
+import android.support.test.uiautomator.UiObject2;
+import android.support.test.uiautomator.Until;
+import android.test.InstrumentationTestCase;
+import android.test.suitebuilder.annotation.LargeTest;
+
+import java.io.File;
+import java.util.regex.Pattern;
+
+/**
+ * Basic tests for the Camera app.
+ */
+public class MediaCaptureTest extends InstrumentationTestCase {
+ private static final int CAPTURE_TIMEOUT = 6000;
+ private static final String DESC_BTN_CAPTURE_PHOTO = "Capture photo";
+ private static final String DESC_BTN_CAPTURE_VIDEO = "Capture video";
+ private static final String DESC_BTN_DONE = "Done";
+ private static final String DESC_BTN_PHOTO_MODE = "Open photo mode";
+ private static final String DESC_BTN_VIDEO_MODE = "Open video mode";
+ private static final int FILE_CHECK_ATTEMPTS = 5;
+ private static final int VIDEO_LENGTH = 2000;
+
+ private UiDevice mDevice;
+
+ @Override
+ public void setUp() throws Exception {
+ super.setUp();
+ mDevice = UiDevice.getInstance(getInstrumentation());
+ mDevice.freezeRotation();
+ // if there are any dialogues that pop up, dismiss them
+ UiObject2 maybeOkButton = mDevice.wait(Until.findObject(By.res("android:id/ok_button")),
+ CAPTURE_TIMEOUT);
+ if (maybeOkButton != null) {
+ maybeOkButton.click();
+ }
+ }
+
+ @Override
+ public void tearDown() throws Exception {
+ mDevice.unfreezeRotation();
+ super.tearDown();
+ }
+
+ /**
+ * Test that the device can capture a photo.
+ */
+ @LargeTest
+ public void testPhotoCapture() {
+ runCaptureTest(new Intent(MediaStore.ACTION_IMAGE_CAPTURE), "smoke.jpg", false);
+ }
+
+ /**
+ * Test that the device can capture a video.
+ */
+ @LargeTest
+ public void testVideoCapture() {
+ runCaptureTest(new Intent(MediaStore.ACTION_VIDEO_CAPTURE), "smoke.avi", true);
+ }
+
+ private void runCaptureTest(Intent intent, String tmpName, boolean isVideo) {
+ intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ if (intent.resolveActivity(
+ getInstrumentation().getContext().getPackageManager()) != null) {
+ File outputFile = null;
+ try {
+ outputFile = new File(Environment
+ .getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM), tmpName);
+ intent.putExtra(MediaStore.EXTRA_OUTPUT, Uri.fromFile(outputFile));
+ getInstrumentation().getContext().startActivity(intent);
+ switchCaptureMode(isVideo);
+ pressCaptureButton(isVideo);
+ if (isVideo) {
+ Thread.sleep(VIDEO_LENGTH);
+ pressCaptureButton(isVideo);
+ }
+ Thread.sleep(1000);
+ pushButton(DESC_BTN_DONE);
+ long fileLength = outputFile.length();
+ for (int i=0; i<FILE_CHECK_ATTEMPTS; i++) {
+ if ((fileLength = outputFile.length()) > 0) {
+ break;
+ }
+ Thread.sleep(1000);
+ }
+ assertTrue(fileLength > 0);
+ } catch (InterruptedException e) {
+ fail(e.getLocalizedMessage());
+ } finally {
+ if (outputFile != null) {
+ outputFile.delete();
+ }
+ }
+ }
+ }
+
+ private void switchCaptureMode(boolean isVideo) {
+ if (isVideo) {
+ pushButton(DESC_BTN_VIDEO_MODE);
+ } else {
+ pushButton(DESC_BTN_PHOTO_MODE);
+ }
+ }
+
+ private void pressCaptureButton(boolean isVideo) {
+ if (isVideo) {
+ pushButton(DESC_BTN_CAPTURE_VIDEO);
+ } else {
+ pushButton(DESC_BTN_CAPTURE_PHOTO);
+ }
+ }
+
+ private void pushButton(String desc) {
+ Pattern pattern = Pattern.compile(desc, Pattern.CASE_INSENSITIVE);
+ UiObject2 doneBtn = mDevice.wait(Until.findObject(By.desc(pattern)), CAPTURE_TIMEOUT);
+ if (null != doneBtn) {
+ doneBtn.clickAndWait(Until.newWindow(), 500);
+ }
+ }
+}
diff --git a/tests/smokefast/src/com/android/smokefast/MediaPlaybackTest.java b/tests/smokefast/src/com/android/smokefast/MediaPlaybackTest.java
new file mode 100644
index 0000000..156b65e
--- /dev/null
+++ b/tests/smokefast/src/com/android/smokefast/MediaPlaybackTest.java
@@ -0,0 +1,109 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.smokefast;
+
+import android.media.MediaPlayer;
+import android.os.Looper;
+import android.test.ActivityInstrumentationTestCase2;
+import android.test.suitebuilder.annotation.LargeTest;
+import android.util.Log;
+
+import com.android.smokefast.app.MediaPlaybackTestApp;
+
+/**
+ * Basic tests for video playback
+ */
+public class MediaPlaybackTest extends ActivityInstrumentationTestCase2<MediaPlaybackTestApp> {
+
+ private static final String TAG = "MediaPlaybackTest";
+ private static final int LOOP_START_BUFFER_MS = 10000;
+ private static final int PLAY_BUFFER_MS = 2000;
+ private final Object mCompletionLock = new Object();
+ private final Object mLooperLock = new Object();
+ private boolean mPlaybackSucceeded = false;
+ private boolean mPlaybackError = false;
+ private Looper mLooper;
+ private MediaPlayer mPlayer;
+
+ public MediaPlaybackTest() {
+ super(MediaPlaybackTestApp.class);
+ }
+
+ @Override
+ public void setUp() throws Exception {
+ // start activity
+ getActivity();
+ }
+
+ @LargeTest
+ public void testVideoPlayback() {
+ // start the MediaPlayer on a Looper thread, so it does not deadlock itself
+ new Thread() {
+ @Override
+ public void run() {
+ Looper.prepare();
+ mLooper = Looper.myLooper();
+ mPlayer = MediaPlayer.create(getInstrumentation().getContext(), R.raw.bbb);
+ mPlayer.setDisplay(getActivity().getSurfaceHolder());
+ synchronized (mLooperLock) {
+ mLooperLock.notify();
+ }
+ Looper.loop();
+ }
+ }.start();
+ // make sure the looper is really started before we proceed
+ synchronized (mLooperLock) {
+ try {
+ mLooperLock.wait(LOOP_START_BUFFER_MS);
+ } catch (InterruptedException e) {
+ fail("Loop thread start was interrupted");
+ }
+ }
+ mPlayer.setOnErrorListener(new MediaPlayer.OnErrorListener() {
+ @Override
+ public boolean onError(MediaPlayer mp, int what, int extra) {
+ mPlaybackError = true;
+ mp.reset();
+ return true;
+ }
+ });
+ mPlayer.setOnCompletionListener(new MediaPlayer.OnCompletionListener() {
+ @Override
+ public void onCompletion(MediaPlayer mp) {
+ synchronized (mCompletionLock) {
+ Log.w(TAG, "Hit onCompletion!");
+ mPlaybackSucceeded = true;
+ mCompletionLock.notifyAll();
+ }
+ }
+ });
+ mPlayer.start();
+ int duration = mPlayer.getDuration();
+ int currentPosition = mPlayer.getCurrentPosition();
+ synchronized (mCompletionLock) {
+ try {
+ mCompletionLock.wait(duration - currentPosition + PLAY_BUFFER_MS);
+ } catch (InterruptedException e) {
+ fail("Wait for playback was interrupted");
+ }
+ }
+ mLooper.quit();
+ mPlayer.release();
+ assertFalse(mPlaybackError);
+ assertTrue(mPlaybackSucceeded);
+ }
+}
diff --git a/tests/smokefast/src/com/android/smokefast/app/MediaPlaybackTestApp.java b/tests/smokefast/src/com/android/smokefast/app/MediaPlaybackTestApp.java
new file mode 100644
index 0000000..92cd04f
--- /dev/null
+++ b/tests/smokefast/src/com/android/smokefast/app/MediaPlaybackTestApp.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.smokefast.app;
+
+import android.app.Activity;
+import android.os.Bundle;
+import android.view.SurfaceHolder;
+import android.view.SurfaceView;
+
+import com.android.smokefast.R;
+
+public class MediaPlaybackTestApp extends Activity {
+
+ private SurfaceView mSurfaceView;
+
+ @Override
+ public void onCreate(Bundle icicle) {
+ super.onCreate(icicle);
+ setContentView(R.layout.surface_view);
+ mSurfaceView = (SurfaceView)findViewById(R.id.surface_view);
+ }
+
+ public SurfaceHolder getSurfaceHolder() {
+ return mSurfaceView.getHolder();
+ }
+}
diff --git a/utils/crashcollector/Android.mk b/utils/crashcollector/Android.mk
new file mode 100644
index 0000000..631ff61
--- /dev/null
+++ b/utils/crashcollector/Android.mk
@@ -0,0 +1,34 @@
+#
+# Copyright (C) 2016 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT 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 := crashcollector
+LOCAL_MODULE_PATH := $(TARGET_OUT_DATA)/local/tmp/crashcollector
+LOCAL_MODULE_TAGS := optional
+LOCAL_SRC_FILES := $(call all-java-files-under, src)
+
+include $(BUILD_JAVA_LIBRARY)
+
+include $(CLEAR_VARS)
+LOCAL_MODULE := crashcollector
+LOCAL_MODULE_CLASS := EXECUTABLES
+LOCAL_MODULE_TAGS := optional
+LOCAL_MODULE_PATH := $(TARGET_OUT_DATA)/local/tmp/crashcollector
+LOCAL_SRC_FILES := crashcollector
+
+include $(BUILD_PREBUILT)
diff --git a/utils/crashcollector/crashcollector b/utils/crashcollector/crashcollector
new file mode 100755
index 0000000..45c58fb
--- /dev/null
+++ b/utils/crashcollector/crashcollector
@@ -0,0 +1,19 @@
+#
+# Copyright (C) 2016 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+base=/data/local/tmp/crashcollector
+export CLASSPATH=$base/crashcollector.jar
+exec app_process $base android.test.crashcollector.Collector $*
diff --git a/utils/crashcollector/src/android/test/crashcollector/Collector.java b/utils/crashcollector/src/android/test/crashcollector/Collector.java
new file mode 100644
index 0000000..515b73b
--- /dev/null
+++ b/utils/crashcollector/src/android/test/crashcollector/Collector.java
@@ -0,0 +1,218 @@
+/*
+ * Copyright 2016, The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.test.crashcollector;
+
+import android.app.ActivityManagerNative;
+import android.app.IActivityController;
+import android.app.IActivityManager;
+import android.content.Context;
+import android.content.Intent;
+import android.os.IBinder;
+import android.os.IBinder.DeathRecipient;
+import android.os.Process;
+import android.os.RemoteException;
+import android.os.ServiceManager;
+import android.os.SystemClock;
+import android.util.Log;
+
+import java.io.File;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.HashSet;
+
+/**
+ * Main class for the crash collector that installs an activity controller to monitor app errors
+ */
+public class Collector {
+
+ private static final String LOG_TAG = "CrashCollector";
+ private static final long CHECK_AM_INTERVAL_MS = 5 * 1000;
+ private static final long MAX_CHECK_AM_TIMEOUT_MS = 30 * 1000;
+ private static final SimpleDateFormat TIME_FORMAT = new SimpleDateFormat("yyyy-MM-dd-HH.mm.ss");
+ private static final File TOMBSTONES_PATH = new File("/data/tombstones");
+ private HashSet<String> mTombstones = null;
+
+ /**
+ * Command-line entry point.
+ *
+ * @param args The command-line arguments
+ */
+ public static void main(String[] args) {
+ // Set the process name showing in "ps" or "top"
+ Process.setArgV0("android.test.crashcollector");
+
+ int resultCode = (new Collector()).run(args);
+ System.exit(resultCode);
+ }
+
+ /**
+ * Command execution entry point
+ * @param args
+ * @return
+ * @throws RemoteException
+ */
+ public int run(String[] args) {
+ // recipient for activity manager death so that command can survive runtime restart
+ final IBinder.DeathRecipient death = new DeathRecipient() {
+ @Override
+ public void binderDied() {
+ synchronized (this) {
+ notifyAll();
+ }
+ }
+ };
+ IBinder am = blockUntilSystemRunning(MAX_CHECK_AM_TIMEOUT_MS);
+ if (am == null) {
+ print("FATAL: Cannot get activity manager, is system running?");
+ return -1;
+ }
+ IActivityController controller = new CrashCollector();
+ do {
+ try {
+ // set activity controller
+ IActivityManager iam = ActivityManagerNative.asInterface(am);
+ iam.setActivityController(controller, false);
+ // register death recipient for activity manager
+ am.linkToDeath(death, 0);
+ } catch (RemoteException re) {
+ print("FATAL: cannot set activity controller, is system running?");
+ re.printStackTrace();
+ return -1;
+ }
+ // monitor runtime restart (crash/kill of system server)
+ synchronized (death) {
+ while (am.isBinderAlive()) {
+ try {
+ Log.d(LOG_TAG, "Monitoring death of system server.");
+ death.wait();
+ } catch (InterruptedException e) {
+ // ignore
+ }
+ }
+ Log.w(LOG_TAG, "Detected crash of system server.");
+ am = blockUntilSystemRunning(MAX_CHECK_AM_TIMEOUT_MS);
+ }
+ } while (true);
+ // for now running indefinitely, until a better mechanism is found to signal shutdown
+ }
+
+ private void print(String line) {
+ System.err.println(String.format("%s %s", TIME_FORMAT.format(new Date()), line));
+ }
+
+ /**
+ * Blocks until system server is running, or timeout has reached
+ * @param timeout
+ * @return
+ */
+ private IBinder blockUntilSystemRunning(long timeout) {
+ // waiting for activity manager to come back
+ long start = SystemClock.uptimeMillis();
+ IBinder am = null;
+ while (SystemClock.uptimeMillis() - start < MAX_CHECK_AM_TIMEOUT_MS) {
+ am = ServiceManager.checkService(Context.ACTIVITY_SERVICE);
+ if (am != null) {
+ break;
+ } else {
+ Log.d(LOG_TAG, "activity manager not ready yet, continue waiting.");
+ try {
+ Thread.sleep(CHECK_AM_INTERVAL_MS);
+ } catch (InterruptedException e) {
+ // break out of current loop upon interruption
+ break;
+ }
+ }
+ }
+ return am;
+ }
+
+ private boolean checkNativeCrashes() {
+ String[] tombstones = TOMBSTONES_PATH.list();
+
+ // shortcut path for usually empty directory, so we don't waste even
+ // more objects
+ if ((tombstones == null) || (tombstones.length == 0)) {
+ mTombstones = null;
+ return false;
+ }
+
+ // use set logic to look for new files
+ HashSet<String> newStones = new HashSet<String>();
+ for (String x : tombstones) {
+ newStones.add(x);
+ }
+
+ boolean result = (mTombstones == null) || !mTombstones.containsAll(newStones);
+
+ // keep the new list for the next time
+ mTombstones = newStones;
+
+ return result;
+ }
+
+ private class CrashCollector extends IActivityController.Stub {
+
+ @Override
+ public boolean activityStarting(Intent intent, String pkg) throws RemoteException {
+ // check native crashes when we have a chance
+ if (checkNativeCrashes()) {
+ print("NATIVE: new tombstones");
+ }
+ return true;
+ }
+
+ @Override
+ public boolean activityResuming(String pkg) throws RemoteException {
+ // check native crashes when we have a chance
+ if (checkNativeCrashes()) {
+ print("NATIVE: new tombstones");
+ }
+ return true;
+ }
+
+ @Override
+ public boolean appCrashed(String processName, int pid, String shortMsg, String longMsg,
+ long timeMillis, String stackTrace) throws RemoteException {
+ if (processName == null) {
+ print("CRASH: null process name, assuming system");
+ } else {
+ print("CRASH: " + processName);
+ }
+ return false;
+ }
+
+ @Override
+ public int appEarlyNotResponding(String processName, int pid, String annotation)
+ throws RemoteException {
+ // ignore
+ return 0;
+ }
+
+ @Override
+ public int appNotResponding(String processName, int pid, String processStats)
+ throws RemoteException {
+ print("ANR: " + processName);
+ return -1;
+ }
+
+ @Override
+ public int systemNotResponding(String msg) throws RemoteException {
+ print("WATCHDOG: " + msg);
+ return -1;
+ }
+ }
+}
diff --git a/utils/dialogs/Android.mk b/utils/dialogs/Android.mk
new file mode 100644
index 0000000..ae77bf2
--- /dev/null
+++ b/utils/dialogs/Android.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.
+#
+
+LOCAL_PATH := $(call my-dir)
+include $(CLEAR_VARS)
+
+LOCAL_MODULE_TAGS := tests
+
+LOCAL_PACKAGE_NAME := DialogDismissalUtil
+LOCAL_STATIC_JAVA_LIBRARIES := app-helpers ub-uiautomator AuptLib
+LOCAL_SRC_FILES := $(call all-java-files-under, src)
+LOCAL_SDK_VERSION := 23
+
+include $(BUILD_PACKAGE)
diff --git a/utils/dialogs/AndroidManifest.xml b/utils/dialogs/AndroidManifest.xml
new file mode 100644
index 0000000..3c1381b
--- /dev/null
+++ b/utils/dialogs/AndroidManifest.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+ * Copyright (c) 2016 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+-->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.android.test.util.dismissdialogs">
+ <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
+ <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
+ <uses-sdk android:minSdkVersion="23"
+ android:targetSdkVersion="23" />
+ <application>
+ <uses-library android:name="android.test.runner" />
+ </application>
+
+ <instrumentation android:name=".DismissDialogsInstrumentation"
+ android:targetPackage="com.android.test.util.dismissdialogs"
+ android:label="Dismiss Dialog Util">
+ </instrumentation>
+</manifest>
diff --git a/utils/dialogs/src/com/android/dialogutils/DismissDialogsInstrumentation.java b/utils/dialogs/src/com/android/dialogutils/DismissDialogsInstrumentation.java
new file mode 100644
index 0000000..5a42255
--- /dev/null
+++ b/utils/dialogs/src/com/android/dialogutils/DismissDialogsInstrumentation.java
@@ -0,0 +1,272 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.test.util.dismissdialogs;
+
+import android.app.Activity;
+import android.app.Instrumentation;
+import android.os.Bundle;
+import android.os.Environment;
+import android.os.RemoteException;
+import android.os.SystemClock;
+import android.support.test.aupt.UiWatchers;
+import android.support.test.uiautomator.UiDevice;
+import android.support.test.uiautomator.Until;
+import android.util.Log;
+
+import android.platform.test.helpers.IStandardAppHelper;
+import android.platform.test.helpers.ChromeHelperImpl;
+import android.platform.test.helpers.GoogleCameraHelperImpl;
+import android.platform.test.helpers.GoogleKeyboardHelperImpl;
+import android.platform.test.helpers.GmailHelperImpl;
+import android.platform.test.helpers.MapsHelperImpl;
+import android.platform.test.helpers.PhotosHelperImpl;
+import android.platform.test.helpers.PlayMoviesHelperImpl;
+import android.platform.test.helpers.PlayMusicHelperImpl;
+import android.platform.test.helpers.PlayStoreHelperImpl;
+import android.platform.test.helpers.YouTubeHelperImpl;
+import android.support.test.launcherhelper.ILauncherStrategy;
+import android.support.test.launcherhelper.LauncherStrategyFactory;
+
+import java.io.File;
+import java.io.IOException;
+import java.lang.NoSuchMethodException;
+import java.lang.InstantiationException;
+import java.lang.IllegalAccessException;
+import java.lang.ReflectiveOperationException;
+import java.lang.reflect.InvocationTargetException;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * A utility to dismiss all predictable, relevant one-time dialogs
+ */
+public class DismissDialogsInstrumentation extends Instrumentation {
+ private static final String LOG_TAG = DismissDialogsInstrumentation.class.getSimpleName();
+ private static final String IMAGE_SUBFOLDER = "dialog-dismissal";
+
+ private static final long INIT_TIMEOUT = 20000;
+ private static final long MAX_INIT_RETRIES = 5;
+
+ // Comma-separated value indicating for which apps to dismiss dialogs
+ private static final String PARAM_APP = "apps";
+ // Boolean to indicate if this should take screenshots to document dismissal
+ private static final String PARAM_SCREENSHOTS = "screenshots";
+ // Boolean to indicate if this should quit if any failure occurs
+ private static final String PARAM_QUIT_ON_ERROR = "quitOnError";
+
+ // Key for status bundles provided when running the preparer
+ private static final String BUNDLE_DISMISSED_APP_KEY = "dismissedApp";
+ private static final String BUNDLE_APP_ERROR_KEY = "appError";
+
+ private Map<String, Class<? extends IStandardAppHelper>> mKeyHelperMap;
+ private String[] mApps;
+ private boolean mScreenshots;
+ private boolean mQuitOnError;
+ private UiDevice mDevice;
+
+ @Override
+ public void onCreate(Bundle arguments) {
+ super.onCreate(arguments);
+
+ mKeyHelperMap = new HashMap<String, Class<? extends IStandardAppHelper>>();
+ mKeyHelperMap.put("Chrome", ChromeHelperImpl.class);
+ mKeyHelperMap.put("GoogleCamera", GoogleCameraHelperImpl.class);
+ mKeyHelperMap.put("GoogleKeyboard", GoogleKeyboardHelperImpl.class);
+ mKeyHelperMap.put("Gmail", GmailHelperImpl.class);
+ mKeyHelperMap.put("Maps", MapsHelperImpl.class);
+ mKeyHelperMap.put("Photos", PhotosHelperImpl.class);
+ mKeyHelperMap.put("PlayMovies", PlayMoviesHelperImpl.class);
+ mKeyHelperMap.put("PlayMusic", PlayMusicHelperImpl.class);
+ mKeyHelperMap.put("PlayStore", PlayStoreHelperImpl.class);
+ //mKeyHelperMap.put("Settings", SettingsHelperImpl.class);
+ mKeyHelperMap.put("YouTube", YouTubeHelperImpl.class);
+
+ String appsString = arguments.getString(PARAM_APP);
+ if (appsString == null) {
+ throw new IllegalArgumentException("Missing 'apps' parameter.");
+ }
+ mApps = appsString.split(",");
+
+ String screenshotsString = arguments.getString(PARAM_SCREENSHOTS);
+ if (screenshotsString == null) {
+ Log.i(LOG_TAG, "No 'screenshots' parameter. Defaulting to true.");
+ mScreenshots = true;
+ } else {
+ mScreenshots = "true".equals(screenshotsString);
+ }
+
+ String quitString = arguments.getString(PARAM_QUIT_ON_ERROR);
+ if (quitString == null) {
+ Log.i(LOG_TAG, "No 'quitOnError' parameter. Defaulting to quit on error.");
+ mQuitOnError = true;
+ } else {
+ mQuitOnError = "true".equals(quitString);
+ }
+
+ start();
+ }
+
+ @Override
+ public void onStart() {
+ super.onStart();
+
+ mDevice = UiDevice.getInstance(this);
+
+ UiWatchers watcherManager = new UiWatchers();
+ watcherManager.registerAnrAndCrashWatchers(this);
+
+ takeScreenDump("init", "pre-setup");
+
+ try {
+ mDevice.setOrientationNatural();
+ } catch (RemoteException e) {
+ Log.e(LOG_TAG, "Unable to set device orientation.", e);
+ }
+
+ for (int retry = 1; retry <= MAX_INIT_RETRIES; retry++) {
+ ILauncherStrategy launcherStrategy =
+ LauncherStrategyFactory.getInstance(mDevice).getLauncherStrategy();
+ boolean foundHome = mDevice.wait(Until.hasObject(
+ launcherStrategy.getWorkspaceSelector()), INIT_TIMEOUT);
+ if (foundHome) {
+ sendStatusUpdate(Activity.RESULT_OK, "launcher");
+ break;
+ } else {
+ takeScreenDump("init", String.format("launcher-selection-failure-%d", retry));
+ if (retry == MAX_INIT_RETRIES && mQuitOnError) {
+ throw new RuntimeException("Unable to select launcher workspace. Quitting.");
+ } else {
+ sendStatusUpdate(Activity.RESULT_CANCELED, "launcher");
+ Log.e(LOG_TAG, "Failed to find home selector; try #" + retry);
+ // HACK: Try to poke at UI to fix accessibility issue (b/21448825)
+ try {
+ mDevice.sleep();
+ SystemClock.sleep(1000);
+ mDevice.wakeUp();
+ mDevice.pressMenu();
+ UiDevice.getInstance(this).pressHome();
+ } catch (RemoteException e) {
+ Log.e(LOG_TAG, "Failed to avoid UI bug b/21448825.", e);
+ }
+ }
+ }
+ }
+
+ for (String app : mApps) {
+ Log.i(LOG_TAG, String.format("Dismissing dialogs for app, %s.", app));
+ try {
+ if (!dismissDialogs(app)) {
+ throw new IllegalArgumentException(
+ String.format("Unrecognized app \"%s\"", mApps));
+ } else {
+ sendStatusUpdate(Activity.RESULT_OK, app);
+ }
+ } catch (ReflectiveOperationException e) {
+ if (mQuitOnError) {
+ quitWithError(app, e);
+ } else {
+ sendStatusUpdate(Activity.RESULT_CANCELED, app);
+ Log.w(LOG_TAG, "ReflectiveOperationException. Continuing with dismissal.", e);
+ }
+ } catch (RuntimeException e) {
+ if (mQuitOnError) {
+ quitWithError(app, e);
+ } else {
+ sendStatusUpdate(Activity.RESULT_CANCELED, app);
+ Log.w(LOG_TAG, "RuntimeException. Continuing with dismissal.", e);
+ }
+ } catch (AssertionError e) {
+ if (mQuitOnError) {
+ quitWithError(app, new Exception(e));
+ } else {
+ sendStatusUpdate(Activity.RESULT_CANCELED, app);
+ Log.w(LOG_TAG, "AssertionError. Continuing with dismissal.", e);
+ }
+ }
+
+ // Always return to the home page after dismissal
+ UiDevice.getInstance(this).pressHome();
+ }
+
+ watcherManager.removeAnrAndCrashWatchers(this);
+
+ finish(Activity.RESULT_OK, new Bundle());
+ }
+
+ private boolean dismissDialogs(String app) throws NoSuchMethodException, InstantiationException,
+ IllegalAccessException, InvocationTargetException {
+ try {
+ if (mKeyHelperMap.containsKey(app)) {
+ Class<? extends IStandardAppHelper> appHelperClass = mKeyHelperMap.get(app);
+ IStandardAppHelper helper =
+ appHelperClass.getDeclaredConstructor(Instrumentation.class).newInstance(this);
+ takeScreenDump(app, "-dialog1-pre-open");
+ helper.open();
+ takeScreenDump(app, "-dialog2-pre-dismissal");
+ helper.dismissInitialDialogs();
+ takeScreenDump(app, "-dialog3-post-dismissal");
+ helper.exit();
+ takeScreenDump(app, "-dialog4-post-exit");
+ return true;
+ } else {
+ return false;
+ }
+ } catch (Exception | AssertionError e) {
+ takeScreenDump(app, "-exception");
+ throw e;
+ }
+ }
+
+ private void sendStatusUpdate(int code, String app) {
+ Bundle result = new Bundle();
+ result.putString(BUNDLE_DISMISSED_APP_KEY, app);
+ sendStatus(code, result);
+ }
+
+ private void quitWithError(String app, Exception exception) {
+ Log.e(LOG_TAG, "Quitting with exception.", exception);
+ // Pass Bundle with debugging information to TF
+ Bundle result = new Bundle();
+ result.putString(BUNDLE_DISMISSED_APP_KEY, app);
+ result.putString(BUNDLE_APP_ERROR_KEY, exception.toString());
+ finish(Activity.RESULT_CANCELED, result);
+ }
+
+ private void takeScreenDump(String app, String suffix) {
+ if (!mScreenshots) {
+ return;
+ }
+
+ try {
+ File dir = new File(Environment.getExternalStorageDirectory(), IMAGE_SUBFOLDER);
+ if (!dir.exists() && !dir.mkdirs()) {
+ throw new RuntimeException(String.format(
+ "Unable to create or find directory, %s.", dir.getPath()));
+ }
+ File scr = new File(dir, "dd-" + app + suffix + ".png");
+ File uix = new File(dir, "dd-" + app + suffix + ".uix");
+ Log.v(LOG_TAG, String.format("Screen file path: %s", scr.getPath()));
+ Log.v(LOG_TAG, String.format("UI XML file path: %s", uix.getPath()));
+ scr.createNewFile();
+ uix.createNewFile();
+ UiDevice.getInstance(this).takeScreenshot(scr);
+ UiDevice.getInstance(this).dumpWindowHierarchy(uix);
+ } catch (IOException e) {
+ Log.e(LOG_TAG, "Failed screen dump.", e);
+ }
+ }
+}