diff options
43 files changed, 1041 insertions, 1049 deletions
@@ -3,17 +3,14 @@ gen/ # gradle junk .gradle -gradle/ -gradlew -gradlew.bat build # Android Studio junk .idea/ *.iml -# Don't check in properties -*.properties +# Don't check in local.properties +local.properties .DS_Store diff --git a/Android.mk b/Android.mk deleted file mode 100644 index 2a752d6..0000000 --- a/Android.mk +++ /dev/null @@ -1,16 +0,0 @@ -LOCAL_PATH := $(call my-dir) -include $(CLEAR_VARS) - -LOCAL_SRC_FILES := $(call all-java-files-under, src) - -LOCAL_MODULE := droiddriver -LOCAL_MODULE_TAGS := optional -LOCAL_SDK_VERSION := 19 - -LOCAL_JAVACFLAGS += -Xlint:deprecation -Xlint:unchecked - -include $(BUILD_STATIC_JAVA_LIBRARY) - -include $(CLEAR_VARS) - -include $(call all-makefiles-under,$(LOCAL_PATH)) diff --git a/build.gradle b/build.gradle index 3af6346..8cc329e 100644 --- a/build.gradle +++ b/build.gradle @@ -2,7 +2,6 @@ // sdk.dir for the Android SDK path, you can run // $ ANDROID_HOME=/path/to/android-sdk gradle build -// Gradle >= 2.4 required buildscript { ext.bintrayUser = project.hasProperty('bintrayUser') ? project.bintrayUser : System.getenv('BINTRAY_USER') ext.bintrayKey = project.hasProperty('bintrayKey') ? project.bintrayKey : System.getenv('BINTRAY_KEY') @@ -12,7 +11,7 @@ buildscript { jcenter() } dependencies { - classpath 'com.android.tools.build:gradle:1.0.1' + classpath 'com.android.tools.build:gradle:1.3.0' classpath 'com.github.dcendents:android-maven-gradle-plugin:1.3' classpath 'com.jakewharton.sdkmanager:gradle-plugin:0.12.0' if (bintrayEnabled) { @@ -29,17 +28,25 @@ version = ddVersion apply plugin: 'android-sdk-manager' apply plugin: 'com.android.library' +repositories { + jcenter() +} + +dependencies { + compile 'com.android.support.test:runner:0.4.1' +} + tasks.withType(JavaCompile) { - options.compilerArgs << '-Xlint:deprecation' + options.compilerArgs << '-Xlint:deprecation' << '-Xlint:unchecked' } android { - compileSdkVersion 21 + compileSdkVersion 23 buildToolsVersion '21.1.2' defaultConfig { minSdkVersion 8 - targetSdkVersion 21 + targetSdkVersion 23 versionCode 1 versionName version } diff --git a/contributing.md b/contributing.md index f8ae829..645ef1b 100644 --- a/contributing.md +++ b/contributing.md @@ -6,18 +6,13 @@ The [`master` branch](https://github.com/appium/droiddriver/tree/master) on GitH Code changes should be [submitted to AOSP](contributing_aosp.md) and then they'll be synced to GitHub once they've passed code reivew on Gerrit. -#### Requirements +#### Build -Gradle 2.2.1 or better is required to be installed on the system. In Android Studio, you'll need to provide the gradle location. - -On Mac OSX with homebrew, `brew install gradle` will install gradle. To locate the path, use `brew info gradle` The homebrew path follows this format: `/usr/local/Cellar/gradle/2.2.1/libexec` - -If you installed gradle using the zip (`gradle-2.2.1-bin.zip`), then the path will be the `gradle-2.2.1` folder. +`./gradlew build` #### Import into Android Studio - Clone from git - Launch Android Studio and select `Open an existing Android Studio project` - Navigate to `droiddriver/build.gradle` and press Choose -- Select `Use local gradle distribution` and enter the Gradle path - Android Studio will now import the project successfully diff --git a/contributing_aosp.md b/contributing_aosp.md index c57a0d1..58bca60 100644 --- a/contributing_aosp.md +++ b/contributing_aosp.md @@ -16,9 +16,12 @@ $ repo sync ``` The code should be downloaded to the current dir. You may see some lines in the output like: -curl: (22) The requested URL returned error: 401 Unauthorized + +`curl: (22) The requested URL returned error: 401 Unauthorized` + These messages seem non-fatal and you should see these dirs after it is done: -build/ external/ frameworks/ Makefile prebuilts/ + +`build/ external/ frameworks/ Makefile prebuilts/` #### Submitting Patches @@ -51,16 +54,3 @@ When commenting on the code, posts will show up as drafts. Drafts are not visibl - `repo upload` The [`repo prune`](https://source.android.com/source/using-repo.html) command can be used to delete already merged branches. - -#### Building - -This sets up environment and some bash functions, particularly "tapas" -(the counterpart of "lunch" for unbundled projects) and "m". - -```bash -$ . build/envsetup.sh -$ tapas droiddriver ManualDD -$ m -``` - -ManualDD is an APK you can use to manually test DroidDriver. diff --git a/droiddriver-android_support_test/Android.mk b/droiddriver-android_support_test/Android.mk deleted file mode 100644 index 22c6c0a..0000000 --- a/droiddriver-android_support_test/Android.mk +++ /dev/null @@ -1,19 +0,0 @@ -LOCAL_PATH := $(call my-dir) -include $(CLEAR_VARS) - -LOCAL_SRC_FILES := $(call all-java-files-under, src) - -LOCAL_MODULE := droiddriver-android_support_test -LOCAL_MODULE_TAGS := optional -LOCAL_SDK_VERSION := 19 - -LOCAL_JAVACFLAGS += -Xlint:deprecation -Xlint:unchecked - -# android-support-test requires /frameworks/testing, /external/junit, /external/hamcrest -LOCAL_JAVA_LIBRARIES := droiddriver android-support-test - -include $(BUILD_STATIC_JAVA_LIBRARY) - -include $(CLEAR_VARS) - -include $(call all-makefiles-under,$(LOCAL_PATH)) diff --git a/droiddriver-android_support_test/AndroidManifest.xml b/droiddriver-android_support_test/AndroidManifest.xml deleted file mode 100644 index f9f47f8..0000000 --- a/droiddriver-android_support_test/AndroidManifest.xml +++ /dev/null @@ -1,5 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<manifest - package="io.appium.droiddriver.android_support_test"> - -</manifest> diff --git a/droiddriver-android_support_test/build.gradle b/droiddriver-android_support_test/build.gradle deleted file mode 100644 index 3f6120f..0000000 --- a/droiddriver-android_support_test/build.gradle +++ /dev/null @@ -1,70 +0,0 @@ -buildscript { - repositories { - jcenter() - } - dependencies { - // this requires Gradle 2 - classpath 'com.android.tools.build:gradle:1.0.1' - classpath 'com.jakewharton.sdkmanager:gradle-plugin:0.12.0' - } -} - -apply plugin: 'android-sdk-manager' -apply plugin: 'com.android.library' - -ext.ddSnapshot = hasProperty('ddSnapshot') - -repositories { - jcenter() - if (ddSnapshot) { - // For development only - droiddriver SNAPSHOTs published here - maven { url 'http://oss.jfrog.org/artifactory/oss-snapshot-local' } - } -} - -dependencies { - if (ddSnapshot) { - // For development only. - compile 'io.appium:droiddriver:1.0.0-SNAPSHOT' - } else { - // This is broken now b/c droiddriver-1.0.0 is not published yet - compile 'io.appium:droiddriver:1.0.0' - } - - compile 'com.android.support.test:testing-support-lib:0.1' -} - -tasks.withType(JavaCompile) { - options.compilerArgs << '-Xlint:deprecation' -} - -android { - compileSdkVersion 21 - buildToolsVersion '21.1.2' - - defaultConfig { - minSdkVersion 8 - targetSdkVersion 21 - versionCode 1 - } - - compileOptions { - sourceCompatibility JavaVersion.VERSION_1_7 - targetCompatibility JavaVersion.VERSION_1_7 - } - - sourceSets { - main { - manifest.srcFile 'AndroidManifest.xml' - java.srcDirs = ['src'] - } - } - - lintOptions { - // Aborting on lint errors prevents jenkins from processing the Lint output - // https://wiki.jenkins-ci.org/display/JENKINS/Android%20Lint%20Plugin - abortOnError false - } -} - -//TODO: add script for publishing diff --git a/droiddriver-android_support_test/readme.md b/droiddriver-android_support_test/readme.md deleted file mode 100644 index f4d7ebb..0000000 --- a/droiddriver-android_support_test/readme.md +++ /dev/null @@ -1,5 +0,0 @@ -# droiddriver-android_support_test - -An optional library that integrates DroidDriver with [the Android Support Test Library](https://code.google.com/p/android-test-kit/wiki/AndroidJUnitRunnerUserGuide). -This is an experimental library because the Android Support Test Library is at early stage and many -APIs are in internal packages.
\ No newline at end of file diff --git a/droiddriver-android_support_test/src/io/appium/droiddriver/android_support_test/D2AndroidJUnitRunner.java b/droiddriver-android_support_test/src/io/appium/droiddriver/android_support_test/D2AndroidJUnitRunner.java deleted file mode 100644 index 89e32d1..0000000 --- a/droiddriver-android_support_test/src/io/appium/droiddriver/android_support_test/D2AndroidJUnitRunner.java +++ /dev/null @@ -1,82 +0,0 @@ -/* - * Copyright (C) 2015 DroidDriver committers - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.appium.droiddriver.android_support_test; - -import android.app.Activity; -import android.os.Bundle; -import android.os.Looper; -import android.support.test.runner.AndroidJUnitRunner; -import android.support.test.runner.lifecycle.ActivityLifecycleMonitorRegistry; -import android.support.test.runner.lifecycle.Stage; -import android.util.Log; - -import java.util.Iterator; -import java.util.concurrent.Callable; - -import io.appium.droiddriver.util.ActivityUtils; -import io.appium.droiddriver.util.InstrumentationUtils; -import io.appium.droiddriver.util.Logs; - -/** - * Integrates DroidDriver with AndroidJUnitRunner. <p> TODO: support DroidDriver test filter - * annotations. - */ -public class D2AndroidJUnitRunner extends AndroidJUnitRunner { - private static final Callable<Activity> GET_RUNNING_ACTIVITY = new Callable<Activity>() { - @Override - public Activity call() { - Iterator<Activity> activityIterator = ActivityLifecycleMonitorRegistry.getInstance() - .getActivitiesInStage(Stage.RESUMED).iterator(); - return activityIterator.hasNext() ? activityIterator.next() : null; - } - }; - - /** - * {@inheritDoc} <p> Initializes {@link InstrumentationUtils}. - */ - @Override - public void onCreate(Bundle arguments) { - InstrumentationUtils.init(this, arguments); - super.onCreate(arguments); - } - - /** - * {@inheritDoc} <p> Hooks {@link ActivityUtils#setRunningActivitySupplier} to {@link - * ActivityLifecycleMonitorRegistry}. - */ - @Override - public void onStart() { - ActivityUtils.setRunningActivitySupplier(new ActivityUtils.Supplier<Activity>() { - @Override - public Activity get() { - try { - // If this is called on main (UI) thread, don't call runOnMainSync - if (Looper.myLooper() == Looper.getMainLooper()) { - return GET_RUNNING_ACTIVITY.call(); - } - - return InstrumentationUtils.runOnMainSyncWithTimeout(GET_RUNNING_ACTIVITY); - } catch (Exception e) { - Logs.log(Log.WARN, e); - return null; - } - } - }); - - super.onStart(); - } -} diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar Binary files differnew file mode 100644 index 0000000..c97a8bd --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.jar diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..80b332a --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Mon Mar 14 09:50:19 PDT 2016 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-2.8-all.zip @@ -0,0 +1,164 @@ +#!/usr/bin/env bash + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn ( ) { + echo "$*" +} + +die ( ) { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; +esac + +# For Cygwin, ensure paths are in UNIX format before anything is touched. +if $cygwin ; then + [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"` +fi + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >&- +APP_HOME="`pwd -P`" +cd "$SAVED" >&- + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules +function splitJvmOpts() { + JVM_OPTS=("$@") +} +eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS +JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" + +exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..aec9973 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,90 @@ +@if "%DEBUG%" == "" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS=
+
+set DIRNAME=%~dp0
+if "%DIRNAME%" == "" set DIRNAME=.
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if "%ERRORLEVEL%" == "0" goto init
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto init
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:init
+@rem Get command-line arguments, handling Windowz variants
+
+if not "%OS%" == "Windows_NT" goto win9xME_args
+if "%@eval[2+2]" == "4" goto 4NT_args
+
+:win9xME_args
+@rem Slurp the command line arguments.
+set CMD_LINE_ARGS=
+set _SKIP=2
+
+:win9xME_args_slurp
+if "x%~1" == "x" goto execute
+
+set CMD_LINE_ARGS=%*
+goto execute
+
+:4NT_args
+@rem Get arguments from the 4NT Shell from JP Software
+set CMD_LINE_ARGS=%$
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
+
+:end
+@rem End local scope for the variables with windows NT shell
+if "%ERRORLEVEL%"=="0" goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
+exit /b 1
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/manualtest/Android.mk b/manualtest/Android.mk deleted file mode 100644 index 6b52b73..0000000 --- a/manualtest/Android.mk +++ /dev/null @@ -1,17 +0,0 @@ -LOCAL_PATH := $(call my-dir) -include $(CLEAR_VARS) -LOCAL_PACKAGE_NAME := ManualDD - -LOCAL_MODULE_TAGS := optional -LOCAL_PROGUARD_ENABLED := disabled - -LOCAL_SRC_FILES := \ - $(call all-java-files-under, src) - -LOCAL_STATIC_JAVA_LIBRARIES := \ - droiddriver - -LOCAL_SDK_VERSION := 19 - -include $(BUILD_PACKAGE) - diff --git a/manualtest/AndroidManifest.xml b/manualtest/AndroidManifest.xml index 7e07f25..da5117a 100644 --- a/manualtest/AndroidManifest.xml +++ b/manualtest/AndroidManifest.xml @@ -4,7 +4,7 @@ package="io.appium.droiddriver.manualtest"> <instrumentation - android:name="io.appium.droiddriver.runner.TestRunner" + android:name="android.support.test.runner.AndroidJUnitRunner" android:targetPackage="io.appium.droiddriver.manualtest" /> <!-- Needed for Android.mk --> diff --git a/manualtest/build.gradle b/manualtest/build.gradle index a732fe3..22731e4 100644 --- a/manualtest/build.gradle +++ b/manualtest/build.gradle @@ -3,8 +3,7 @@ buildscript { jcenter() } dependencies { - // this requires Gradle 2 - classpath 'com.android.tools.build:gradle:1.0.1' + classpath 'com.android.tools.build:gradle:1.3.0' } } @@ -12,17 +11,13 @@ buildscript { apply plugin: 'com.android.application' android { - compileSdkVersion 21 + compileSdkVersion 23 buildToolsVersion '21.1.2' - defaultConfig { minSdkVersion 8 - targetSdkVersion 21 - // Force remove the suffix '.test' - testApplicationId 'io.appium.droiddriver.manualtest' - testInstrumentationRunner 'io.appium.droiddriver.runner.TestRunner' + targetSdkVersion 23 + testInstrumentationRunner 'android.support.test.runner.AndroidJUnitRunner' } - sourceSets { main { manifest.srcFile 'AndroidManifest.xml' @@ -31,6 +26,8 @@ android { java.srcDirs = ['src'] } } + productFlavors { + } } // Building with droiddriver source. Common tests should use droiddriver from jcenter by having @@ -39,7 +36,7 @@ android { // jcenter() // } // dependencies { -// androidTestCompile 'io.appium:droiddriver:0.9.1-BETA' // or another version +// androidTestCompile 'io.appium:droiddriver:1.0.0-BETA1' // or another version // } dependencies { androidTestCompile project(':droiddriver') diff --git a/manualtest/src/io/appium/droiddriver/manualtest/ManualTest.java b/manualtest/src/io/appium/droiddriver/manualtest/ManualTest.java index 83966f7..59beac4 100644 --- a/manualtest/src/io/appium/droiddriver/manualtest/ManualTest.java +++ b/manualtest/src/io/appium/droiddriver/manualtest/ManualTest.java @@ -16,10 +16,10 @@ import io.appium.droiddriver.uiautomation.UiAutomationDriver; * {@link #testSetTextForPassword} assumes the password_edit field is displayed * on screen. * <p> - * Run it as (optionally with -e debug true) + * Run it with * * <pre> - * adb shell am instrument -w io.appium.droiddriver.manualtest/io.appium.droiddriver.runner.TestRunner + * ../gradlew :connectedAndroidTest * </pre> */ public class ManualTest extends BaseDroidDriverTest<Activity> { diff --git a/src/io/appium/droiddriver/UiElement.java b/src/io/appium/droiddriver/UiElement.java index a003367..aa55d09 100644 --- a/src/io/appium/droiddriver/UiElement.java +++ b/src/io/appium/droiddriver/UiElement.java @@ -17,9 +17,7 @@ package io.appium.droiddriver; import android.graphics.Rect; - -import java.util.List; - +import android.view.accessibility.AccessibilityNodeInfo; import io.appium.droiddriver.actions.Action; import io.appium.droiddriver.actions.InputInjector; import io.appium.droiddriver.finders.Attribute; @@ -27,113 +25,114 @@ import io.appium.droiddriver.finders.Predicate; import io.appium.droiddriver.instrumentation.InstrumentationDriver; import io.appium.droiddriver.scroll.Direction.PhysicalDirection; import io.appium.droiddriver.uiautomation.UiAutomationDriver; +import java.util.List; /** * Represents an UI element within an Android App. - * <p> - * UI elements are generally views. Users can get attributes and perform - * actions. Note that actions often update UiElement, so users are advised not - * to store instances for later use -- the instances could become stale. + * + * <p>UI elements are generally views. Users can get attributes and perform actions. Note that + * actions often update UiElement, so users are advised not to store instances for later use -- the + * instances could become stale. */ public interface UiElement { - /** - * Gets the text of this element. - */ + /** Filters out invisible children. */ + Predicate<UiElement> VISIBLE = + new Predicate<UiElement>() { + @Override + public boolean apply(UiElement element) { + return element.isVisible(); + } + + @Override + public String toString() { + return "VISIBLE"; + } + }; + + /** Gets the text of this element. */ String getText(); /** - * Gets the content description of this element. + * Sets the text of this element. The implementation may not work on all UiElements if the + * underlying view is not EditText. + * + * <p>If this element already has text, it is cleared first if the device has API 11 or higher. + * + * <p>TODO: Support this behavior on older devices. + * + * <p>The soft keyboard may be shown after this call. If the {@code text} ends with {@code '\n'}, + * the IME may be closed automatically. If the soft keyboard is open, you can call {@link + * UiDevice#pressBack()} to close it. + * + * <p>If you are using {@link io.appium.droiddriver.instrumentation.InstrumentationDriver}, you + * may use {@link io.appium.droiddriver.actions.view.CloseKeyboardAction} to close it. The + * advantage of {@code CloseKeyboardAction} is that it is a no-op if the soft keyboard is hidden. + * This is useful when the state of the soft keyboard cannot be determined. + * + * @param text the text to enter */ + void setText(String text); + + /** Gets the content description of this element. */ String getContentDescription(); /** - * Gets the class name of the underlying view. The actual name could be - * overridden. - * - * @see io.appium.droiddriver.instrumentation.ViewElement#overrideClassName + * Gets the class name of the underlying view. The actual name could be overridden if viewed with + * uiautomatorviewer, which gets the name from {@link AccessibilityNodeInfo#getClassName}. If the + * app uses custom View classes that do not call {@link AccessibilityNodeInfo#setClassName} with + * the actual class name, uiautomatorviewer will report the wrong name. */ String getClassName(); - /** - * Gets the resource id of this element. - */ + /** Gets the resource id of this element. */ String getResourceId(); - /** - * Gets the package name of this element. - */ + /** Gets the package name of this element. */ String getPackageName(); - /** - * @return whether or not this element is visible on the device's display. - */ + /** @return whether or not this element is visible on the device's display. */ boolean isVisible(); - /** - * @return whether this element is checkable. - */ + /** @return whether this element is checkable. */ boolean isCheckable(); - /** - * @return whether this element is checked. - */ + /** @return whether this element is checked. */ boolean isChecked(); - /** - * @return whether this element is clickable. - */ + /** @return whether this element is clickable. */ boolean isClickable(); - /** - * @return whether this element is enabled. - */ + /** @return whether this element is enabled. */ boolean isEnabled(); - /** - * @return whether this element is focusable. - */ + /** @return whether this element is focusable. */ boolean isFocusable(); - /** - * @return whether this element is focused. - */ + /** @return whether this element is focused. */ boolean isFocused(); - /** - * @return whether this element is scrollable. - */ + /** @return whether this element is scrollable. */ boolean isScrollable(); - /** - * @return whether this element is long-clickable. - */ + /** @return whether this element is long-clickable. */ boolean isLongClickable(); - /** - * @return whether this element is password. - */ + /** @return whether this element is password. */ boolean isPassword(); - /** - * @return whether this element is selected. - */ + /** @return whether this element is selected. */ boolean isSelected(); /** - * Gets the UiElement bounds in screen coordinates. The coordinates may not be - * visible on screen. + * Gets the UiElement bounds in screen coordinates. The coordinates may not be visible on screen. */ Rect getBounds(); - /** - * Gets the UiElement bounds in screen coordinates. The coordinates will be - * visible on screen. - */ + /** Gets the UiElement bounds in screen coordinates. The coordinates will be visible on screen. */ Rect getVisibleBounds(); - /** - * @return value of the given attribute. - */ + /** @return value of the given attribute. */ + @SuppressWarnings("TypeParameterUnusedInFormals") <T> T get(Attribute attribute); /** @@ -144,37 +143,13 @@ public interface UiElement { */ boolean perform(Action action); - /** - * Sets the text of this element. The implementation may not work on all UiElements if the - * underlying view is not EditText. <p> If this element already has text, it is cleared first if - * the device has API 11 or higher. <p> TODO: Support this behavior on older devices. <p> The IME - * (soft keyboard) may be shown after this call. If the {@code text} ends with {@code '\n'}, the - * IME may be closed automatically. If the IME is open, you can call {@link UiDevice#pressBack()} - * to close it. <p> If you are using {@link io.appium.droiddriver.instrumentation.InstrumentationDriver}, - * you may use {@link io.appium.droiddriver.actions.view.CloseKeyboardAction} to close it. The - * advantage of {@code CloseKeyboardAction} is that it is a no-op if the IME is hidden. This is - * useful when the state of the IME cannot be determined. - * - * @param text the text to enter - */ - void setText(String text); - - /** - * Clicks this element. The click will be at the center of the visible - * element. - */ + /** Clicks this element. The click will be at the center of the visible element. */ void click(); - /** - * Long-clicks this element. The click will be at the center of the visible - * element. - */ + /** Long-clicks this element. The click will be at the center of the visible element. */ void longClick(); - /** - * Double-clicks this element. The click will be at the center of the visible - * element. - */ + /** Double-clicks this element. The click will be at the center of the visible element. */ void doubleClick(); /** @@ -185,50 +160,27 @@ public interface UiElement { void scroll(PhysicalDirection direction); /** - * Gets an immutable {@link List} of immediate children that satisfy - * {@code predicate}. It always filters children that are null. This gives a - * low level access to the underlying data. Do not use it unless you are sure - * about the subtle details. Note the count may not be what you expect. For - * instance, a dynamic list may show more items when scrolling beyond the end, - * varying the count. The count also depends on the driver implementation: + * Gets an immutable {@link List} of immediate children that satisfy {@code predicate}. It always + * filters children that are null. This gives a low level access to the underlying data. Do not + * use it unless you are sure about the subtle details. Note the count may not be what you expect. + * For instance, a dynamic list may show more items when scrolling beyond the end, varying the + * count. The count also depends on the driver implementation: + * * <ul> - * <li>{@link InstrumentationDriver} includes all.</li> - * <li>the Accessibility API (which {@link UiAutomationDriver} depends on) - * does not include off-screen children, but may include invisible on-screen - * children.</li> + * <li>{@link InstrumentationDriver} includes all. + * <li>the Accessibility API (which {@link UiAutomationDriver} depends on) does not include + * off-screen children, but may include invisible on-screen children. * </ul> - * <p> - * Another discrepancy between {@link InstrumentationDriver} - * {@link UiAutomationDriver} is the order of children. The Accessibility API - * returns children in the order of layout (see - * {@link android.view.ViewGroup#addChildrenForAccessibility}, which is added - * in API16). - * </p> + * + * <p>Another discrepancy between {@link InstrumentationDriver} {@link UiAutomationDriver} is the + * order of children. The Accessibility API returns children in the order of layout (see {@link + * android.view.ViewGroup#addChildrenForAccessibility}, which is added in API16). */ List<? extends UiElement> getChildren(Predicate<? super UiElement> predicate); - /** - * Filters out invisible children. - */ - Predicate<UiElement> VISIBLE = new Predicate<UiElement>() { - @Override - public boolean apply(UiElement element) { - return element.isVisible(); - } - - @Override - public String toString() { - return "VISIBLE"; - } - }; - - /** - * Gets the parent. - */ + /** Gets the parent. */ UiElement getParent(); - /** - * Gets the {@link InputInjector} for injecting InputEvent. - */ + /** Gets the {@link InputInjector} for injecting InputEvent. */ InputInjector getInjector(); } diff --git a/src/io/appium/droiddriver/actions/TextAction.java b/src/io/appium/droiddriver/actions/TextAction.java index b108b00..18b28e8 100644 --- a/src/io/appium/droiddriver/actions/TextAction.java +++ b/src/io/appium/droiddriver/actions/TextAction.java @@ -21,29 +21,26 @@ import android.os.Build; import android.os.SystemClock; import android.view.KeyCharacterMap; import android.view.KeyEvent; - +import android.view.ViewConfiguration; import io.appium.droiddriver.UiElement; import io.appium.droiddriver.exceptions.ActionException; import io.appium.droiddriver.util.Preconditions; import io.appium.droiddriver.util.Strings; -/** - * An action to type text. - */ +/** An action to type text. */ public class TextAction extends KeyAction { @SuppressLint("InlinedApi") @SuppressWarnings("deprecation") private static final KeyCharacterMap KEY_CHAR_MAP = - Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB ? KeyCharacterMap - .load(KeyCharacterMap.BUILT_IN_KEYBOARD) : KeyCharacterMap - .load(KeyCharacterMap.VIRTUAL_KEYBOARD); - + Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB + ? KeyCharacterMap.load(KeyCharacterMap.BUILT_IN_KEYBOARD) + : KeyCharacterMap.load(KeyCharacterMap.VIRTUAL_KEYBOARD); + // KeyRepeatDelay is a good heuristic for KeyInjectionDelay. + private static long keyInjectionDelayMillis = ViewConfiguration.getKeyRepeatDelay(); private final String text; - /** - * Defaults timeoutMillis to 100. - */ + /** Defaults timeoutMillis to 100. */ public TextAction(String text) { this(text, 100L, false); } @@ -53,13 +50,20 @@ public class TextAction extends KeyAction { this.text = Preconditions.checkNotNull(text); } + public static long getKeyInjectionDelayMillis() { + return keyInjectionDelayMillis; + } + + public static void setKeyInjectionDelayMillis(long keyInjectionDelayMillis) { + TextAction.keyInjectionDelayMillis = keyInjectionDelayMillis; + } + @Override public boolean perform(InputInjector injector, UiElement element) { maybeCheckFocused(element); // TODO: recycle events? KeyEvent[] events = KEY_CHAR_MAP.getEvents(text.toCharArray()); - boolean success = false; if (events != null) { for (KeyEvent event : events) { @@ -69,15 +73,15 @@ public class TextAction extends KeyAction { // possible for an event to become stale before it is injected if it // takes too long to inject the preceding ones. KeyEvent modifiedEvent = KeyEvent.changeTimeRepeat(event, SystemClock.uptimeMillis(), 0); - success = injector.injectInputEvent(modifiedEvent); - if (!success) { - break; + if (!injector.injectInputEvent(modifiedEvent)) { + throw new ActionException("Failed to inject " + event); } + SystemClock.sleep(keyInjectionDelayMillis); } } else { throw new ActionException("The given text is not supported: " + text); } - return success; + return true; } @Override diff --git a/src/io/appium/droiddriver/actions/accessibility/AccessibilityClickAction.java b/src/io/appium/droiddriver/actions/accessibility/AccessibilityClickAction.java index 8198059..5405d58 100644 --- a/src/io/appium/droiddriver/actions/accessibility/AccessibilityClickAction.java +++ b/src/io/appium/droiddriver/actions/accessibility/AccessibilityClickAction.java @@ -41,6 +41,7 @@ public abstract class AccessibilityClickAction extends AccessibilityAction { super(timeoutMillis); } + @SuppressWarnings("IdentityBinaryExpression") @Override protected boolean perform(AccessibilityNodeInfo node, UiElement element) { return SINGLE.perform(element) && SINGLE.perform(element); diff --git a/src/io/appium/droiddriver/base/AbstractDroidDriver.java b/src/io/appium/droiddriver/base/AbstractDroidDriver.java new file mode 100644 index 0000000..bf0df4b --- /dev/null +++ b/src/io/appium/droiddriver/base/AbstractDroidDriver.java @@ -0,0 +1,86 @@ +/* + * Copyright (C) 2016 DroidDriver committers + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.appium.droiddriver.base; + +import io.appium.droiddriver.DroidDriver; +import io.appium.droiddriver.Poller; +import io.appium.droiddriver.UiElement; +import io.appium.droiddriver.actions.InputInjector; +import io.appium.droiddriver.exceptions.ElementNotFoundException; +import io.appium.droiddriver.exceptions.TimeoutException; +import io.appium.droiddriver.finders.Finder; +import io.appium.droiddriver.util.Logs; + +/** + * Base DroidDriver that implements the common operations. + */ +public abstract class AbstractDroidDriver implements DroidDriver { + + private Poller poller = new DefaultPoller(); + + @Override + public boolean has(Finder finder) { + try { + refreshUiElementTree(); + find(finder); + return true; + } catch (ElementNotFoundException enfe) { + return false; + } + } + + @Override + public boolean has(Finder finder, long timeoutMillis) { + try { + getPoller().pollFor(this, finder, Poller.EXISTS, timeoutMillis); + return true; + } catch (TimeoutException e) { + return false; + } + } + + @Override + public UiElement on(Finder finder) { + Logs.call(this, "on", finder); + return getPoller().pollFor(this, finder, Poller.EXISTS); + } + + @Override + public void checkExists(Finder finder) { + Logs.call(this, "checkExists", finder); + getPoller().pollFor(this, finder, Poller.EXISTS); + } + + @Override + public void checkGone(Finder finder) { + Logs.call(this, "checkGone", finder); + getPoller().pollFor(this, finder, Poller.GONE); + } + + @Override + public Poller getPoller() { + return poller; + } + + @Override + public void setPoller(Poller poller) { + this.poller = poller; + } + + public abstract InputInjector getInjector(); + +}
\ No newline at end of file diff --git a/src/io/appium/droiddriver/base/BaseDroidDriver.java b/src/io/appium/droiddriver/base/BaseDroidDriver.java index e985a38..d6114c6 100644 --- a/src/io/appium/droiddriver/base/BaseDroidDriver.java +++ b/src/io/appium/droiddriver/base/BaseDroidDriver.java @@ -18,22 +18,16 @@ package io.appium.droiddriver.base; import android.util.Log; -import io.appium.droiddriver.DroidDriver; -import io.appium.droiddriver.Poller; import io.appium.droiddriver.UiElement; -import io.appium.droiddriver.actions.InputInjector; -import io.appium.droiddriver.exceptions.ElementNotFoundException; -import io.appium.droiddriver.exceptions.TimeoutException; import io.appium.droiddriver.finders.ByXPath; import io.appium.droiddriver.finders.Finder; import io.appium.droiddriver.util.Logs; /** - * Base DroidDriver that implements the common operations. + * Enhances AbstractDroidDriver to include basic element handling and matching operations. */ -public abstract class BaseDroidDriver<R, E extends BaseUiElement<R, E>> implements DroidDriver { +public abstract class BaseDroidDriver<R, E extends BaseUiElement<R, E>> extends AbstractDroidDriver { - private Poller poller = new DefaultPoller(); private E rootElement; @Override @@ -42,57 +36,6 @@ public abstract class BaseDroidDriver<R, E extends BaseUiElement<R, E>> implemen return finder.find(getRootElement()); } - @Override - public boolean has(Finder finder) { - try { - refreshUiElementTree(); - find(finder); - return true; - } catch (ElementNotFoundException enfe) { - return false; - } - } - - @Override - public boolean has(Finder finder, long timeoutMillis) { - try { - getPoller().pollFor(this, finder, Poller.EXISTS, timeoutMillis); - return true; - } catch (TimeoutException e) { - return false; - } - } - - @Override - public UiElement on(Finder finder) { - Logs.call(this, "on", finder); - return getPoller().pollFor(this, finder, Poller.EXISTS); - } - - @Override - public void checkExists(Finder finder) { - Logs.call(this, "checkExists", finder); - getPoller().pollFor(this, finder, Poller.EXISTS); - } - - @Override - public void checkGone(Finder finder) { - Logs.call(this, "checkGone", finder); - getPoller().pollFor(this, finder, Poller.GONE); - } - - @Override - public Poller getPoller() { - return poller; - } - - @Override - public void setPoller(Poller poller) { - this.poller = poller; - } - - public abstract InputInjector getInjector(); - protected abstract E newRootElement(); /** diff --git a/src/io/appium/droiddriver/base/BaseUiDevice.java b/src/io/appium/droiddriver/base/BaseUiDevice.java index 5b6d135..096de8b 100644 --- a/src/io/appium/droiddriver/base/BaseUiDevice.java +++ b/src/io/appium/droiddriver/base/BaseUiDevice.java @@ -22,14 +22,13 @@ import android.graphics.Bitmap.CompressFormat; import android.os.PowerManager; import android.util.Log; import android.view.KeyEvent; - -import java.io.BufferedOutputStream; - import io.appium.droiddriver.UiDevice; import io.appium.droiddriver.actions.Action; import io.appium.droiddriver.actions.SingleKeyAction; import io.appium.droiddriver.util.FileUtils; +import io.appium.droiddriver.util.InstrumentationUtils; import io.appium.droiddriver.util.Logs; +import java.io.BufferedOutputStream; /** * Base implementation of {@link UiDevice}. @@ -54,7 +53,14 @@ public abstract class BaseUiDevice implements UiDevice { @Override public void wakeUp() { if (!isScreenOn()) { - perform(POWER_ON); + // Cannot call perform(POWER_ON) because perform() checks the UiElement is visible. + POWER_ON.perform(getContext().getDriver().getInjector(), null); + InstrumentationUtils.tryWaitForIdleSync(POWER_ON.getTimeoutMillis()); + + Logs.log( + Log.WARN, + "After wakeUp, root AccessibilityNodeInfo may not be available. This is seen" + + " on api 23 devices, but could also happen on earlier devices."); } } diff --git a/src/io/appium/droiddriver/base/CompositeDroidDriver.java b/src/io/appium/droiddriver/base/CompositeDroidDriver.java new file mode 100644 index 0000000..c92c5c3 --- /dev/null +++ b/src/io/appium/droiddriver/base/CompositeDroidDriver.java @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2016 DroidDriver committers + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.appium.droiddriver.base; + +import io.appium.droiddriver.UiDevice; +import io.appium.droiddriver.UiElement; +import io.appium.droiddriver.actions.InputInjector; +import io.appium.droiddriver.finders.Finder; + +/** + * Helper class to ease creation of drivers that defer actions to other drivers. + */ +public abstract class CompositeDroidDriver extends AbstractDroidDriver { + /** + * Determines which DroidDriver should handle the current situation. + * + * @return The DroidDriver instance to use + */ + protected abstract AbstractDroidDriver getApplicableDriver(); + + @Override + public InputInjector getInjector() { + return getApplicableDriver().getInjector(); + } + + @Override + public UiDevice getUiDevice() { + return getApplicableDriver().getUiDevice(); + } + + @Override + public UiElement find(Finder finder) { + return getApplicableDriver().find(finder); + } + + @Override + public void refreshUiElementTree() { + getApplicableDriver().refreshUiElementTree(); + } + + @Override + public boolean dumpUiElementTree(String path) { + return getApplicableDriver().dumpUiElementTree(path); + } +} diff --git a/src/io/appium/droiddriver/duo/DuoDriver.java b/src/io/appium/droiddriver/duo/DuoDriver.java new file mode 100644 index 0000000..0ad84bf --- /dev/null +++ b/src/io/appium/droiddriver/duo/DuoDriver.java @@ -0,0 +1,57 @@ +/* + * Copyright (C) 2016 DroidDriver committers + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.appium.droiddriver.duo; + +import android.annotation.TargetApi; +import android.app.Activity; +import android.app.Instrumentation; + +import io.appium.droiddriver.base.AbstractDroidDriver; +import io.appium.droiddriver.base.CompositeDroidDriver; +import io.appium.droiddriver.instrumentation.InstrumentationDriver; +import io.appium.droiddriver.uiautomation.UiAutomationDriver; +import io.appium.droiddriver.util.ActivityUtils; +import io.appium.droiddriver.util.InstrumentationUtils; + +/** + * Implementation of DroidDriver that attempts to use the best driver for the current activity. + * If the activity is part of the application under instrumentation, the InstrumentationDriver is + * used. Otherwise, the UiAutomationDriver is used. + */ +@TargetApi(18) +public class DuoDriver extends CompositeDroidDriver { + private final String targetApkPackage; + private final UiAutomationDriver uiAutomationDriver; + private final InstrumentationDriver instrumentationDriver; + + public DuoDriver() { + Instrumentation instrumentation = InstrumentationUtils.getInstrumentation(); + targetApkPackage = InstrumentationUtils.getTargetContext().getPackageName(); + uiAutomationDriver = new UiAutomationDriver(instrumentation); + instrumentationDriver = new InstrumentationDriver(instrumentation); + } + + @Override + protected AbstractDroidDriver getApplicableDriver() { + Activity activity = ActivityUtils.getRunningActivity(); + if (activity != null && targetApkPackage.equals( + activity.getApplicationContext().getPackageName())) { + return instrumentationDriver; + } + return uiAutomationDriver; + } +} diff --git a/src/io/appium/droiddriver/finders/Attribute.java b/src/io/appium/droiddriver/finders/Attribute.java index 9dda497..c2aa83a 100644 --- a/src/io/appium/droiddriver/finders/Attribute.java +++ b/src/io/appium/droiddriver/finders/Attribute.java @@ -38,7 +38,7 @@ public enum Attribute { private final String name; - private Attribute(String name) { + Attribute(String name) { this.name = name; } diff --git a/src/io/appium/droiddriver/finders/By.java b/src/io/appium/droiddriver/finders/By.java index f8ac924..9a38622 100644 --- a/src/io/appium/droiddriver/finders/By.java +++ b/src/io/appium/droiddriver/finders/By.java @@ -16,26 +16,34 @@ package io.appium.droiddriver.finders; +import static io.appium.droiddriver.util.Preconditions.checkNotNull; + import android.content.Context; +import java.util.ArrayList; +import java.util.List; + import io.appium.droiddriver.UiElement; import io.appium.droiddriver.exceptions.ElementNotFoundException; import io.appium.droiddriver.util.InstrumentationUtils; -import static io.appium.droiddriver.util.Preconditions.checkNotNull; - /** * Convenience methods to create commonly used finders. */ public class By { + private static final MatchFinder ANY = new MatchFinder(null); - /** Matches any UiElement. */ + /** + * Matches any UiElement. + */ public static MatchFinder any() { return ANY; } - /** Matches a UiElement whose {@code attribute} is {@code true}. */ + /** + * Matches a UiElement whose {@code attribute} is {@code true}. + */ public static MatchFinder is(Attribute attribute) { return new MatchFinder(Predicates.attributeTrue(attribute)); } @@ -47,74 +55,91 @@ public class By { return new MatchFinder(Predicates.attributeFalse(attribute)); } - /** Matches a UiElement by a resource id defined in the AUT. */ + /** + * Matches a UiElement by a resource id defined in the AUT. + */ public static MatchFinder resourceId(int resourceId) { Context targetContext = InstrumentationUtils.getInstrumentation().getTargetContext(); return resourceId(targetContext.getResources().getResourceName(resourceId)); } /** - * Matches a UiElement by the string representation of a resource id. This works for resources - * not belonging to the AUT. + * Matches a UiElement by the string representation of a resource id. This works for resources not + * belonging to the AUT. */ public static MatchFinder resourceId(String resourceId) { return new MatchFinder(Predicates.attributeEquals(Attribute.RESOURCE_ID, resourceId)); } - /** Matches a UiElement by package name. */ + /** + * Matches a UiElement by package name. + */ public static MatchFinder packageName(String name) { return new MatchFinder(Predicates.attributeEquals(Attribute.PACKAGE, name)); } - /** Matches a UiElement by the exact text. */ + /** + * Matches a UiElement by the exact text. + */ public static MatchFinder text(String text) { return new MatchFinder(Predicates.attributeEquals(Attribute.TEXT, text)); } - /** Matches a UiElement whose text matches {@code regex}. */ + /** + * Matches a UiElement whose text matches {@code regex}. + */ public static MatchFinder textRegex(String regex) { return new MatchFinder(Predicates.attributeMatches(Attribute.TEXT, regex)); } - /** Matches a UiElement whose text contains {@code substring}. */ + /** + * Matches a UiElement whose text contains {@code substring}. + */ public static MatchFinder textContains(String substring) { return new MatchFinder(Predicates.attributeContains(Attribute.TEXT, substring)); } - /** Matches a UiElement by content description. */ + /** + * Matches a UiElement by content description. + */ public static MatchFinder contentDescription(String contentDescription) { return new MatchFinder(Predicates.attributeEquals(Attribute.CONTENT_DESC, contentDescription)); } - /** Matches a UiElement whose content description contains {@code substring}. */ + /** + * Matches a UiElement whose content description contains {@code substring}. + */ public static MatchFinder contentDescriptionContains(String substring) { return new MatchFinder(Predicates.attributeContains(Attribute.CONTENT_DESC, substring)); } - /** Matches a UiElement by class name. */ + /** + * Matches a UiElement by class name. + */ public static MatchFinder className(String className) { return new MatchFinder(Predicates.attributeEquals(Attribute.CLASS, className)); } - /** Matches a UiElement by class name. */ + /** + * Matches a UiElement by class name. + */ public static MatchFinder className(Class<?> clazz) { return className(clazz.getName()); } - /** Matches a UiElement that is selected. */ + /** + * Matches a UiElement that is selected. + */ public static MatchFinder selected() { return is(Attribute.SELECTED); } /** - * Matches by XPath. When applied on an non-root element, it will not evaluate - * above the context element. - * <p> - * XPath is the domain-specific-language for navigating a node tree. It is - * ideal if the UiElement to match has a complex relationship with surrounding - * nodes. For simple cases, {@link #withParent} or {@link #withAncestor} are - * preferred, which can combine with other {@link MatchFinder}s in - * {@link #allOf}. For complex cases like below, XPath is superior: + * Matches by XPath. When applied on an non-root element, it will not evaluate above the context + * element. <p> XPath is the domain-specific-language for navigating a node tree. It is ideal if + * the UiElement to match has a complex relationship with surrounding nodes. For simple cases, + * {@link #withParent} or {@link #withAncestor} are preferred, which can combine with other {@link + * MatchFinder}s in {@link #allOf}. For complex cases like below, XPath is superior: * * <pre> * {@code @@ -132,8 +157,8 @@ public class By { * } * </pre> * - * If we need to locate the RelativeLayout containing the album "Forever" - * instead of a song or an artist named "Forever", this XPath works: + * If we need to locate the RelativeLayout containing the album "Forever" instead of a song or an + * artist named "Forever", this XPath works: * * <pre> * {@code //*[LinearLayout/*[@text='Albums']]/RelativeLayout[*[@text='Forever']]} @@ -147,34 +172,28 @@ public class By { } /** - * Returns a finder that uses the UiElement returned by first Finder as - * context for the second Finder. - * <p> - * typically first Finder finds the ancestor, then second Finder finds the - * target UiElement, which is a descendant. - * </p> - * Note that if the first Finder matches multiple UiElements, only the first - * match is tried, which usually is not what callers expect. In this case, - * allOf(second, withAncesor(first)) may work. + * Returns a finder that uses the UiElement returned by first Finder as context for the second + * Finder. <p> typically first Finder finds the ancestor, then second Finder finds the target + * UiElement, which is a descendant. </p> Note that if the first Finder matches multiple + * UiElements, only the first match is tried, which usually is not what callers expect. In this + * case, allOf(second, withAncesor(first)) may work. */ public static ChainFinder chain(Finder first, Finder second) { return new ChainFinder(first, second); } - private static Predicate<? super UiElement>[] getPredicates(MatchFinder... finders) { - @SuppressWarnings("unchecked") - Predicate<? super UiElement>[] predicates = new Predicate[finders.length]; + private static List<Predicate<? super UiElement>> getPredicates(MatchFinder... finders) { + ArrayList<Predicate<? super UiElement>> predicates = new ArrayList<>(finders.length); for (int i = 0; i < finders.length; i++) { - predicates[i] = finders[i].predicate; + predicates.add(finders[i].predicate); } return predicates; } /** - * Evaluates given {@code finders} in short-circuit fashion in the order - * they are passed. Costly finders (for example those returned by with* - * methods that navigate the node tree) should be passed after cheap finders - * (for example the ByAttribute finders). + * Evaluates given {@code finders} in short-circuit fashion in the order they are passed. Costly + * finders (for example those returned by with* methods that navigate the node tree) should be + * passed after cheap finders (for example the ByAttribute finders). * * @return a finder that is the logical conjunction of given finders */ @@ -183,10 +202,9 @@ public class By { } /** - * Evaluates given {@code finders} in short-circuit fashion in the order - * they are passed. Costly finders (for example those returned by with* - * methods that navigate the node tree) should be passed after cheap finders - * (for example the ByAttribute finders). + * Evaluates given {@code finders} in short-circuit fashion in the order they are passed. Costly + * finders (for example those returned by with* methods that navigate the node tree) should be + * passed after cheap finders (for example the ByAttribute finders). * * @return a finder that is the logical disjunction of given finders */ @@ -195,8 +213,8 @@ public class By { } /** - * Matches a UiElement whose parent matches the given parentFinder. For - * complex cases, consider {@link #xpath}. + * Matches a UiElement whose parent matches the given parentFinder. For complex cases, consider + * {@link #xpath}. */ public static MatchFinder withParent(MatchFinder parentFinder) { checkNotNull(parentFinder); @@ -204,8 +222,8 @@ public class By { } /** - * Matches a UiElement whose ancestor matches the given ancestorFinder. For - * complex cases, consider {@link #xpath}. + * Matches a UiElement whose ancestor matches the given ancestorFinder. For complex cases, + * consider {@link #xpath}. */ public static MatchFinder withAncestor(MatchFinder ancestorFinder) { checkNotNull(ancestorFinder); @@ -213,8 +231,8 @@ public class By { } /** - * Matches a UiElement which has a visible sibling matching the given - * siblingFinder. This could be inefficient; consider {@link #xpath}. + * Matches a UiElement which has a visible sibling matching the given siblingFinder. This could be + * inefficient; consider {@link #xpath}. */ public static MatchFinder withSibling(MatchFinder siblingFinder) { checkNotNull(siblingFinder); @@ -222,8 +240,8 @@ public class By { } /** - * Matches a UiElement which has a visible child matching the given - * childFinder. This could be inefficient; consider {@link #xpath}. + * Matches a UiElement which has a visible child matching the given childFinder. This could be + * inefficient; consider {@link #xpath}. */ public static MatchFinder withChild(MatchFinder childFinder) { checkNotNull(childFinder); @@ -231,8 +249,8 @@ public class By { } /** - * Matches a UiElement whose descendant (including self) matches the given - * descendantFinder. This could be VERY inefficient; consider {@link #xpath}. + * Matches a UiElement whose descendant (including self) matches the given descendantFinder. This + * could be VERY inefficient; consider {@link #xpath}. */ public static MatchFinder withDescendant(final MatchFinder descendantFinder) { checkNotNull(descendantFinder); @@ -254,11 +272,14 @@ public class By { }); } - /** Matches a UiElement that does not match the provided {@code finder}. */ + /** + * Matches a UiElement that does not match the provided {@code finder}. + */ public static MatchFinder not(MatchFinder finder) { checkNotNull(finder); return new MatchFinder(Predicates.not(finder.predicate)); } - private By() {} + private By() { + } } diff --git a/src/io/appium/droiddriver/finders/Predicates.java b/src/io/appium/droiddriver/finders/Predicates.java index 1b9ad80..0d2d9df 100644 --- a/src/io/appium/droiddriver/finders/Predicates.java +++ b/src/io/appium/droiddriver/finders/Predicates.java @@ -18,13 +18,17 @@ package io.appium.droiddriver.finders; import android.text.TextUtils; +import java.util.Arrays; + import io.appium.droiddriver.UiElement; /** * Static utility methods pertaining to {@code Predicate} instances. */ public final class Predicates { - private Predicates() {} + + private Predicates() { + } private static final Predicate<Object> ANY = new Predicate<Object>() { @Override @@ -64,9 +68,9 @@ public final class Predicates { } /** - * Returns a predicate that evaluates to {@code true} if both arguments - * evaluate to {@code true}. The arguments are evaluated in order, and - * evaluation will be "short-circuited" as soon as a false predicate is found. + * Returns a predicate that evaluates to {@code true} if both arguments evaluate to {@code true}. + * The arguments are evaluated in order, and evaluation will be "short-circuited" as soon as a + * false predicate is found. */ @SuppressWarnings("unchecked") public static <T> Predicate<T> allOf(final Predicate<? super T> first, @@ -92,13 +96,11 @@ public final class Predicates { } /** - * Returns a predicate that evaluates to {@code true} if each of its - * components evaluates to {@code true}. The components are evaluated in - * order, and evaluation will be "short-circuited" as soon as a false - * predicate is found. + * Returns a predicate that evaluates to {@code true} if each of its components evaluates to + * {@code true}. The components are evaluated in order, and evaluation will be "short-circuited" + * as soon as a false predicate is found. */ - @SuppressWarnings("unchecked") - public static <T> Predicate<T> allOf(final Predicate<? super T>... components) { + public static <T> Predicate<T> allOf(final Iterable<Predicate<? super T>> components) { return new Predicate<T>() { @Override public boolean apply(T input) { @@ -118,13 +120,22 @@ public final class Predicates { } /** - * Returns a predicate that evaluates to {@code true} if any one of its - * components evaluates to {@code true}. The components are evaluated in - * order, and evaluation will be "short-circuited" as soon as a true predicate - * is found. + * Returns a predicate that evaluates to {@code true} if each of its components evaluates to + * {@code true}. The components are evaluated in order, and evaluation will be "short-circuited" + * as soon as a false predicate is found. */ - @SuppressWarnings("unchecked") - public static <T> Predicate<T> anyOf(final Predicate<? super T>... components) { + @SuppressWarnings("RedundantTypeArguments") // Some compilers cannot infer <T> + @SafeVarargs + public static <T> Predicate<T> allOf(final Predicate<? super T>... components) { + return Predicates.<T>allOf(Arrays.asList(components)); + } + + /** + * Returns a predicate that evaluates to {@code true} if any one of its components evaluates to + * {@code true}. The components are evaluated in order, and evaluation will be "short-circuited" + * as soon as a true predicate is found. + */ + public static <T> Predicate<T> anyOf(final Iterable<Predicate<? super T>> components) { return new Predicate<T>() { @Override public boolean apply(T input) { @@ -144,8 +155,19 @@ public final class Predicates { } /** - * Returns a predicate that evaluates to {@code true} on a {@link UiElement} - * if its {@code attribute} is {@code true}. + * Returns a predicate that evaluates to {@code true} if any one of its components evaluates to + * {@code true}. The components are evaluated in order, and evaluation will be "short-circuited" + * as soon as a true predicate is found. + */ + @SuppressWarnings("RedundantTypeArguments") // Some compilers cannot infer <T> + @SafeVarargs + public static <T> Predicate<T> anyOf(final Predicate<? super T>... components) { + return Predicates.<T>anyOf(Arrays.asList(components)); + } + + /** + * Returns a predicate that evaluates to {@code true} on a {@link UiElement} if its {@code + * attribute} is {@code true}. */ public static Predicate<UiElement> attributeTrue(final Attribute attribute) { return new Predicate<UiElement>() { @@ -163,8 +185,8 @@ public final class Predicates { } /** - * Returns a predicate that evaluates to {@code true} on a {@link UiElement} - * if its {@code attribute} is {@code false}. + * Returns a predicate that evaluates to {@code true} on a {@link UiElement} if its {@code + * attribute} is {@code false}. */ public static Predicate<UiElement> attributeFalse(final Attribute attribute) { return new Predicate<UiElement>() { @@ -182,8 +204,8 @@ public final class Predicates { } /** - * Returns a predicate that evaluates to {@code true} on a {@link UiElement} - * if its {@code attribute} equals {@code expected}. + * Returns a predicate that evaluates to {@code true} on a {@link UiElement} if its {@code + * attribute} equals {@code expected}. */ public static Predicate<UiElement> attributeEquals(final Attribute attribute, final Object expected) { @@ -202,10 +224,11 @@ public final class Predicates { } /** - * Returns a predicate that evaluates to {@code true} on a {@link UiElement} - * if its {@code attribute} matches {@code regex}. + * Returns a predicate that evaluates to {@code true} on a {@link UiElement} if its {@code + * attribute} matches {@code regex}. */ - public static Predicate<UiElement> attributeMatches(final Attribute attribute, final String regex) { + public static Predicate<UiElement> attributeMatches(final Attribute attribute, + final String regex) { return new Predicate<UiElement>() { @Override public boolean apply(UiElement element) { @@ -221,8 +244,8 @@ public final class Predicates { } /** - * Returns a predicate that evaluates to {@code true} on a {@link UiElement} - * if its {@code attribute} contains {@code substring}. + * Returns a predicate that evaluates to {@code true} on a {@link UiElement} if its {@code + * attribute} contains {@code substring}. */ public static Predicate<UiElement> attributeContains(final Attribute attribute, final String substring) { @@ -240,7 +263,8 @@ public final class Predicates { }; } - public static Predicate<UiElement> withParent(final Predicate<? super UiElement> parentPredicate) { + public static Predicate<UiElement> withParent( + final Predicate<? super UiElement> parentPredicate) { return new Predicate<UiElement>() { @Override public boolean apply(UiElement element) { @@ -277,7 +301,8 @@ public final class Predicates { }; } - public static Predicate<UiElement> withSibling(final Predicate<? super UiElement> siblingPredicate) { + public static Predicate<UiElement> withSibling( + final Predicate<? super UiElement> siblingPredicate) { return new Predicate<UiElement>() { @Override public boolean apply(UiElement element) { @@ -318,4 +343,6 @@ public final class Predicates { } }; } + + } diff --git a/src/io/appium/droiddriver/helpers/BaseDroidDriverTest.java b/src/io/appium/droiddriver/helpers/BaseDroidDriverTest.java index 0b17dc5..607da95 100644 --- a/src/io/appium/droiddriver/helpers/BaseDroidDriverTest.java +++ b/src/io/appium/droiddriver/helpers/BaseDroidDriverTest.java @@ -91,9 +91,6 @@ public abstract class BaseDroidDriverTest<T extends Activity> extends * behavior - if multiple subclasses override this method, only the first override is executed. * Other overrides are silently ignored. You can either use {@link SingleRun} in {@link #setUp}, * or override this method, which is a simpler alternative with the aforementioned catch. - * <p> - * If an InstrumentationDriver is used, this is a good place to call {@link - * io.appium.droiddriver.instrumentation.ViewElement#overrideClassName} */ protected void classSetUp() { DroidDriversInitializer.get(DroidDrivers.newDriver()).singleRun(); diff --git a/src/io/appium/droiddriver/helpers/DroidDrivers.java b/src/io/appium/droiddriver/helpers/DroidDrivers.java index 7725bf5..e55d595 100644 --- a/src/io/appium/droiddriver/helpers/DroidDrivers.java +++ b/src/io/appium/droiddriver/helpers/DroidDrivers.java @@ -21,9 +21,9 @@ import android.app.Instrumentation; import android.os.Build; import io.appium.droiddriver.DroidDriver; +import io.appium.droiddriver.duo.DuoDriver; import io.appium.droiddriver.exceptions.DroidDriverException; import io.appium.droiddriver.instrumentation.InstrumentationDriver; -import io.appium.droiddriver.uiautomation.UiAutomationDriver; import io.appium.droiddriver.util.InstrumentationUtils; /** @@ -83,7 +83,7 @@ public class DroidDrivers { // If "dd.driver" is not specified, return default. if (hasUiAutomation()) { checkUiAutomation(); - return new UiAutomationDriver(instrumentation); + return new DuoDriver(); } return new InstrumentationDriver(instrumentation); } diff --git a/src/io/appium/droiddriver/helpers/SingleRun.java b/src/io/appium/droiddriver/helpers/SingleRun.java index 5ffd21e..714c777 100644 --- a/src/io/appium/droiddriver/helpers/SingleRun.java +++ b/src/io/appium/droiddriver/helpers/SingleRun.java @@ -24,7 +24,7 @@ import java.util.concurrent.atomic.AtomicBoolean; * a class effect. */ public abstract class SingleRun { - private AtomicBoolean hasRun = new AtomicBoolean(); + private final AtomicBoolean hasRun = new AtomicBoolean(); /** * Calls {@link #run()} if it is the first time this method is called upon this instance. diff --git a/src/io/appium/droiddriver/instrumentation/InstrumentationDriver.java b/src/io/appium/droiddriver/instrumentation/InstrumentationDriver.java index d3e5dd2..03b2123 100644 --- a/src/io/appium/droiddriver/instrumentation/InstrumentationDriver.java +++ b/src/io/appium/droiddriver/instrumentation/InstrumentationDriver.java @@ -16,14 +16,11 @@ package io.appium.droiddriver.instrumentation; +import android.app.Activity; import android.app.Instrumentation; import android.os.SystemClock; import android.util.Log; import android.view.View; - -import java.util.List; -import java.util.concurrent.Callable; - import io.appium.droiddriver.actions.InputInjector; import io.appium.droiddriver.base.BaseDroidDriver; import io.appium.droiddriver.base.DroidDriverContext; @@ -31,11 +28,36 @@ import io.appium.droiddriver.exceptions.NoRunningActivityException; import io.appium.droiddriver.util.ActivityUtils; import io.appium.droiddriver.util.InstrumentationUtils; import io.appium.droiddriver.util.Logs; +import java.util.List; +import java.util.concurrent.Callable; -/** - * Implementation of DroidDriver that is driven via instrumentation. - */ +/** Implementation of DroidDriver that is driven via instrumentation. */ public class InstrumentationDriver extends BaseDroidDriver<View, ViewElement> { + private static final Callable<View> FIND_ROOT_VIEW = + new Callable<View>() { + @Override + public View call() { + InstrumentationUtils.checkMainThread(); + Activity runningActivity = ActivityUtils.getRunningActivityNoWait(); + if (runningActivity == null) { + // runningActivity changed since last call! + return null; + } + + List<View> views = RootFinder.getRootViews(); + if (views.size() > 1) { + Logs.log(Log.VERBOSE, "views.size()=" + views.size()); + for (View view : views) { + if (view.hasWindowFocus()) { + return view; + } + } + } + // Fall back to DecorView. + // TODO(kjin): Should wait until a view hasWindowFocus? + return runningActivity.getWindow().getDecorView(); + } + }; private final DroidDriverContext<View, ViewElement> context; private final InputInjector injector; private final InstrumentationUiDevice uiDevice; @@ -61,41 +83,22 @@ public class InstrumentationDriver extends BaseDroidDriver<View, ViewElement> { return new ViewElement(context, rawElement, parent); } - private static final Callable<View> FIND_ROOT_VIEW = new Callable<View>() { - @Override - public View call() { - List<View> views = RootFinder.getRootViews(); - if (views.size() > 1) { - Logs.log(Log.VERBOSE, "views.size()=" + views.size()); - for (View view : views) { - if (view.hasWindowFocus()) { - return view; - } - } - } - // Fall back to DecorView. - return ActivityUtils.getRunningActivity().getWindow().getDecorView(); - } - }; - private View findRootView() { - waitForRunningActivity(); - return InstrumentationUtils.runOnMainSyncWithTimeout(FIND_ROOT_VIEW); - } - - private void waitForRunningActivity() { long timeoutMillis = getPoller().getTimeoutMillis(); long end = SystemClock.uptimeMillis() + timeoutMillis; while (true) { - if (ActivityUtils.getRunningActivity() != null) { - return; - } long remainingMillis = end - SystemClock.uptimeMillis(); if (remainingMillis < 0) { - throw new NoRunningActivityException(String.format( - "Cannot find the running activity after %d milliseconds", timeoutMillis)); + throw new NoRunningActivityException( + String.format("Cannot find the running activity after %d milliseconds", timeoutMillis)); + } + + if (ActivityUtils.getRunningActivity(remainingMillis) != null) { + View view = InstrumentationUtils.runOnMainSyncWithTimeout(FIND_ROOT_VIEW); + if (view != null) { + return view; + } } - SystemClock.sleep(Math.min(250, remainingMillis)); } } diff --git a/src/io/appium/droiddriver/instrumentation/ViewElement.java b/src/io/appium/droiddriver/instrumentation/ViewElement.java index e706362..da7f2d0 100644 --- a/src/io/appium/droiddriver/instrumentation/ViewElement.java +++ b/src/io/appium/droiddriver/instrumentation/ViewElement.java @@ -16,39 +16,108 @@ package io.appium.droiddriver.instrumentation; +import static io.appium.droiddriver.util.Strings.charSequenceToString; + import android.content.res.Resources; import android.graphics.Rect; import android.view.View; import android.view.ViewGroup; -import android.view.accessibility.AccessibilityNodeInfo; import android.widget.Checkable; import android.widget.TextView; - +import io.appium.droiddriver.actions.InputInjector; +import io.appium.droiddriver.base.BaseUiElement; +import io.appium.droiddriver.base.DroidDriverContext; +import io.appium.droiddriver.finders.Attribute; +import io.appium.droiddriver.util.InstrumentationUtils; +import io.appium.droiddriver.util.Preconditions; import java.util.ArrayList; import java.util.Collections; import java.util.EnumMap; -import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.Callable; import java.util.concurrent.FutureTask; -import io.appium.droiddriver.actions.InputInjector; -import io.appium.droiddriver.base.BaseUiElement; -import io.appium.droiddriver.base.DroidDriverContext; -import io.appium.droiddriver.finders.Attribute; -import io.appium.droiddriver.util.InstrumentationUtils; -import io.appium.droiddriver.util.Preconditions; +/** A UiElement that is backed by a View. */ +public class ViewElement extends BaseUiElement<View, ViewElement> { + private final DroidDriverContext<View, ViewElement> context; + private final View view; + private final Map<Attribute, Object> attributes; + private final boolean visible; + private final Rect visibleBounds; + private final ViewElement parent; + private final List<ViewElement> children; -import static io.appium.droiddriver.util.Strings.charSequenceToString; + /** + * A snapshot of all attributes is taken at construction. The attributes of a {@code ViewElement} + * instance are immutable. If the underlying view is updated, a new {@code ViewElement} instance + * will be created in {@link io.appium.droiddriver.DroidDriver#refreshUiElementTree}. + */ + public ViewElement(DroidDriverContext<View, ViewElement> context, View view, ViewElement parent) { + this.context = Preconditions.checkNotNull(context); + this.view = Preconditions.checkNotNull(view); + this.parent = parent; + AttributesSnapshot attributesSnapshot = new AttributesSnapshot(view); + InstrumentationUtils.runOnMainSyncWithTimeout(attributesSnapshot); + + attributes = Collections.unmodifiableMap(attributesSnapshot.attribs); + this.visibleBounds = attributesSnapshot.visibleBounds; + this.visible = attributesSnapshot.visible; + if (attributesSnapshot.childViews == null) { + this.children = null; + } else { + List<ViewElement> children = new ArrayList<>(attributesSnapshot.childViews.size()); + for (View childView : attributesSnapshot.childViews) { + children.add(context.getElement(childView, this)); + } + this.children = Collections.unmodifiableList(children); + } + } + + @Override + public Rect getVisibleBounds() { + return visibleBounds; + } + + @Override + public boolean isVisible() { + return visible; + } + + @Override + public ViewElement getParent() { + return parent; + } + + @Override + protected List<ViewElement> getChildren() { + return children; + } + + @Override + protected Map<Attribute, Object> getAttributes() { + return attributes; + } + + @Override + public InputInjector getInjector() { + return context.getDriver().getInjector(); + } + + @Override + protected void doPerformAndWait(FutureTask<Boolean> futureTask, long timeoutMillis) { + futureTask.run(); + InstrumentationUtils.tryWaitForIdleSync(timeoutMillis); + } + + @Override + public View getRawElement() { + return view; + } -/** - * A UiElement that is backed by a View. - */ -public class ViewElement extends BaseUiElement<View, ViewElement> { private static class AttributesSnapshot implements Callable<Void> { + final Map<Attribute, Object> attribs = new EnumMap<>(Attribute.class); private final View view; - final Map<Attribute, Object> attribs = new EnumMap<Attribute, Object>(Attribute.class); boolean visible; Rect visibleBounds; List<View> childViews; @@ -107,9 +176,7 @@ public class ViewElement extends BaseUiElement<View, ViewElement> { } private String getClassName() { - String className = view.getClass().getName(); - return CLASS_NAME_OVERRIDES.containsKey(className) ? CLASS_NAME_OVERRIDES.get(className) - : className; + return view.getClass().getName(); } private String getResourceId() { @@ -168,7 +235,7 @@ public class ViewElement extends BaseUiElement<View, ViewElement> { } ViewGroup group = (ViewGroup) view; int childCount = group.getChildCount(); - childViews = new ArrayList<View>(childCount); + childViews = new ArrayList<>(childCount); for (int i = 0; i < childCount; i++) { View child = group.getChildAt(i); if (child != null) { @@ -177,104 +244,4 @@ public class ViewElement extends BaseUiElement<View, ViewElement> { } } } - - private static final Map<String, String> CLASS_NAME_OVERRIDES = new HashMap<String, String>(); - - /** - * Typically users find the class name to use in tests using SDK tool - * uiautomatorviewer. This name is returned by - * {@link AccessibilityNodeInfo#getClassName}. If the app uses custom View - * classes that do not call {@link AccessibilityNodeInfo#setClassName} with - * the actual class name, different types of drivers see different class names - * (InstrumentationDriver sees the actual class name, while UiAutomationDriver - * sees {@link AccessibilityNodeInfo#getClassName}). - * <p> - * If tests fail with InstrumentationDriver, find the actual class name by - * examining app code or by calling - * {@link io.appium.droiddriver.DroidDriver#dumpUiElementTree}, then - * call this method in setUp to override it with the class name seen in - * uiautomatorviewer. - * </p> - * A better solution is to use resource-id instead of classname, which is an - * implementation detail and subject to change. - */ - public static void overrideClassName(String actualClassName, String overridingClassName) { - CLASS_NAME_OVERRIDES.put(actualClassName, overridingClassName); - } - - private final DroidDriverContext<View, ViewElement> context; - private final View view; - private final Map<Attribute, Object> attributes; - private final boolean visible; - private final Rect visibleBounds; - private final ViewElement parent; - private final List<ViewElement> children; - - /** - * A snapshot of all attributes is taken at construction. The attributes of a - * {@code ViewElement} instance are immutable. If the underlying view is - * updated, a new {@code ViewElement} instance will be created in - * {@link io.appium.droiddriver.DroidDriver#refreshUiElementTree}. - */ - public ViewElement(DroidDriverContext<View, ViewElement> context, View view, ViewElement parent) { - this.context = Preconditions.checkNotNull(context); - this.view = Preconditions.checkNotNull(view); - this.parent = parent; - AttributesSnapshot attributesSnapshot = new AttributesSnapshot(view); - InstrumentationUtils.runOnMainSyncWithTimeout(attributesSnapshot); - - attributes = Collections.unmodifiableMap(attributesSnapshot.attribs); - this.visibleBounds = attributesSnapshot.visibleBounds; - this.visible = attributesSnapshot.visible; - if (attributesSnapshot.childViews == null) { - this.children = null; - } else { - List<ViewElement> children = new ArrayList<ViewElement>(attributesSnapshot.childViews.size()); - for (View childView : attributesSnapshot.childViews) { - children.add(context.getElement(childView, this)); - } - this.children = Collections.unmodifiableList(children); - } - } - - @Override - public Rect getVisibleBounds() { - return visibleBounds; - } - - @Override - public boolean isVisible() { - return visible; - } - - @Override - public ViewElement getParent() { - return parent; - } - - @Override - protected List<ViewElement> getChildren() { - return children; - } - - @Override - protected Map<Attribute, Object> getAttributes() { - return attributes; - } - - @Override - public InputInjector getInjector() { - return context.getDriver().getInjector(); - } - - @Override - protected void doPerformAndWait(FutureTask<Boolean> futureTask, long timeoutMillis) { - futureTask.run(); - InstrumentationUtils.tryWaitForIdleSync(timeoutMillis); - } - - @Override - public View getRawElement() { - return view; - } } diff --git a/src/io/appium/droiddriver/runner/MinSdkVersion.java b/src/io/appium/droiddriver/runner/MinSdkVersion.java index c1ea2e9..f560ad8 100644 --- a/src/io/appium/droiddriver/runner/MinSdkVersion.java +++ b/src/io/appium/droiddriver/runner/MinSdkVersion.java @@ -16,28 +16,26 @@ package io.appium.droiddriver.runner; +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.TYPE; + import java.lang.annotation.Inherited; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; -import static java.lang.annotation.ElementType.METHOD; -import static java.lang.annotation.ElementType.TYPE; - /** - * This annotation indicates that its target needs a minimum SDK version - * specified as its value. - * <p> - * As any annotations, it is useful only if it is processed by tools. - * {@link TestRunner} filters out tests with this annotation if the current - * device has a lower SDK version. + * This annotation indicates that its target needs a minimum SDK version specified as its value. + * + * <p>As any annotations, it is useful only if it is processed by tools. + * + * @deprecated Use android.support.test.filters.SdkSuppress instead. */ @Inherited @Target({TYPE, METHOD}) @Retention(RetentionPolicy.RUNTIME) +@Deprecated public @interface MinSdkVersion { - /** - * The minimum required SDK version. - */ + /** The minimum required SDK version. */ int value(); } diff --git a/src/io/appium/droiddriver/runner/TestRunner.java b/src/io/appium/droiddriver/runner/TestRunner.java deleted file mode 100644 index 71bb744..0000000 --- a/src/io/appium/droiddriver/runner/TestRunner.java +++ /dev/null @@ -1,203 +0,0 @@ -/* - * Copyright (C) 2013 DroidDriver committers - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.appium.droiddriver.runner; - -import android.app.Activity; -import android.os.Build; -import android.os.Bundle; -import android.test.AndroidTestRunner; -import android.test.InstrumentationTestRunner; -import android.test.suitebuilder.TestMethod; -import android.util.Log; - -import com.android.internal.util.Predicate; - -import junit.framework.AssertionFailedError; -import junit.framework.Test; -import junit.framework.TestListener; - -import java.lang.annotation.Annotation; -import java.util.ArrayList; -import java.util.HashSet; -import java.util.List; -import java.util.Set; - -import io.appium.droiddriver.helpers.DroidDrivers; -import io.appium.droiddriver.util.ActivityUtils; -import io.appium.droiddriver.util.ActivityUtils.Supplier; -import io.appium.droiddriver.util.InstrumentationUtils; -import io.appium.droiddriver.util.Logs; - -/** - * Adds activity watcher to InstrumentationTestRunner. - */ -public class TestRunner extends InstrumentationTestRunner { - private final Set<Activity> activities = new HashSet<Activity>(); - private final AndroidTestRunner androidTestRunner = new AndroidTestRunner(); - private volatile Activity runningActivity; - - /** - * Returns an {@link AndroidTestRunner} that is shared by this and super, such - * that we can add custom {@link TestListener}s. - */ - @Override - protected AndroidTestRunner getAndroidTestRunner() { - return androidTestRunner; - } - - /** - * {@inheritDoc} - * <p> - * Initializes {@link InstrumentationUtils}. - */ - @Override - public void onCreate(Bundle arguments) { - InstrumentationUtils.init(this, arguments); - super.onCreate(arguments); - } - - /** - * {@inheritDoc} - * <p> - * Adds a {@link TestListener} that finishes all created activities. - */ - @Override - public void onStart() { - getAndroidTestRunner().addTestListener(new TestListener() { - @Override - public void endTest(Test test) { - // Try to finish activity on best-effort basis - TestListener should - // not throw. - final Activity[] activitiesCopy; - synchronized (activities) { - if (activities.isEmpty()) { - return; - } - activitiesCopy = activities.toArray(new Activity[activities.size()]); - } - - try { - InstrumentationUtils.runOnMainSyncWithTimeout(new Runnable() { - @Override - public void run() { - for (Activity activity : activitiesCopy) { - if (!activity.isFinishing()) { - try { - Logs.log(Log.INFO, "Stopping activity: " + activity); - activity.finish(); - } catch (Throwable e) { - Logs.log(Log.ERROR, e, "Failed to stop activity"); - } - } - } - } - }); - } catch (Throwable e) { - Logs.log(Log.ERROR, e); - } - - // We've done what we can. Clear activities if any are left. - synchronized (activities) { - activities.clear(); - runningActivity = null; - } - } - - @Override - public void addError(Test arg0, Throwable arg1) {} - - @Override - public void addFailure(Test arg0, AssertionFailedError arg1) {} - - @Override - public void startTest(Test arg0) {} - }); - - ActivityUtils.setRunningActivitySupplier(new Supplier<Activity>() { - @Override - public Activity get() { - return runningActivity; - } - }); - - super.onStart(); - } - - // Overrides InstrumentationTestRunner - List<Predicate<TestMethod>> getBuilderRequirements() { - List<Predicate<TestMethod>> requirements = new ArrayList<Predicate<TestMethod>>(); - requirements.add(new Predicate<TestMethod>() { - @Override - public boolean apply(TestMethod arg0) { - MinSdkVersion minSdkVersion = getAnnotation(arg0, MinSdkVersion.class); - if (minSdkVersion != null && minSdkVersion.value() > Build.VERSION.SDK_INT) { - Logs.logfmt(Log.INFO, "filtered %s#%s: MinSdkVersion=%d", arg0.getEnclosingClassname(), - arg0.getName(), minSdkVersion.value()); - return false; - } - - UseUiAutomation useUiAutomation = getAnnotation(arg0, UseUiAutomation.class); - if (useUiAutomation != null && !DroidDrivers.hasUiAutomation()) { - Logs.logfmt(Log.INFO, - "filtered %s#%s: Has @UseUiAutomation, but ro.build.version.sdk=%d", - arg0.getEnclosingClassname(), arg0.getName(), Build.VERSION.SDK_INT); - return false; - } - return true; - } - - private <T extends Annotation> T getAnnotation(TestMethod testMethod, Class<T> clazz) { - T annotation = testMethod.getAnnotation(clazz); - if (annotation == null) { - annotation = testMethod.getEnclosingClass().getAnnotation(clazz); - } - return annotation; - } - }); - return requirements; - } - - @Override - public void callActivityOnDestroy(Activity activity) { - super.callActivityOnDestroy(activity); - synchronized (activities) { - activities.remove(activity); - } - } - - @Override - public void callActivityOnCreate(Activity activity, Bundle bundle) { - super.callActivityOnCreate(activity, bundle); - synchronized (activities) { - activities.add(activity); - } - } - - @Override - public void callActivityOnResume(Activity activity) { - super.callActivityOnResume(activity); - runningActivity = activity; - } - - @Override - public void callActivityOnPause(Activity activity) { - super.callActivityOnPause(activity); - if (activity == runningActivity) { - runningActivity = null; - } - } -} diff --git a/src/io/appium/droiddriver/runner/UseUiAutomation.java b/src/io/appium/droiddriver/runner/UseUiAutomation.java index 316ac8f..e710238 100644 --- a/src/io/appium/droiddriver/runner/UseUiAutomation.java +++ b/src/io/appium/droiddriver/runner/UseUiAutomation.java @@ -16,26 +16,25 @@ package io.appium.droiddriver.runner; +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.TYPE; + import java.lang.annotation.Inherited; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; -import static java.lang.annotation.ElementType.METHOD; -import static java.lang.annotation.ElementType.TYPE; - /** - * This annotation indicates that its target needs - * {@link android.app.UiAutomation}. It is effectively equivalent to - * {@code @MinSdkVersion(Build.VERSION_CODES.JELLY_BEAN_MR2)}, just more + * This annotation indicates that its target needs {@link android.app.UiAutomation}. It is + * effectively equivalent to {@code @MinSdkVersion(Build.VERSION_CODES.JELLY_BEAN_MR2)}, just more * explicit. - * <p> - * As any annotations, it is useful only if it is processed by tools. - * {@link TestRunner} filters out tests with this annotation if the current - * device has SDK version below 18 (JELLY_BEAN_MR2). + * + * <p>As any annotations, it is useful only if it is processed by tools. + * + * @deprecated Use android.support.test.filters.SdkSuppress instead. */ @Inherited @Target({TYPE, METHOD}) @Retention(RetentionPolicy.RUNTIME) -public @interface UseUiAutomation { -} +@Deprecated +public @interface UseUiAutomation {} diff --git a/src/io/appium/droiddriver/scroll/DynamicSentinelStrategy.java b/src/io/appium/droiddriver/scroll/DynamicSentinelStrategy.java index 051cfa7..b42b60d 100644 --- a/src/io/appium/droiddriver/scroll/DynamicSentinelStrategy.java +++ b/src/io/appium/droiddriver/scroll/DynamicSentinelStrategy.java @@ -39,7 +39,7 @@ public class DynamicSentinelStrategy extends SentinelStrategy { /** * Interface for determining whether sentinel is updated. */ - public static interface IsUpdatedStrategy { + public interface IsUpdatedStrategy { /** * Returns whether {@code newSentinel} is updated from {@code oldSentinel}. */ diff --git a/src/io/appium/droiddriver/scroll/StepBasedScroller.java b/src/io/appium/droiddriver/scroll/StepBasedScroller.java index 6dbc79e..11c42f4 100644 --- a/src/io/appium/droiddriver/scroll/StepBasedScroller.java +++ b/src/io/appium/droiddriver/scroll/StepBasedScroller.java @@ -15,6 +15,8 @@ */ package io.appium.droiddriver.scroll; +import static io.appium.droiddriver.scroll.Direction.LogicalDirection.BACKWARD; + import android.util.Log; import io.appium.droiddriver.DroidDriver; @@ -29,8 +31,6 @@ import io.appium.droiddriver.scroll.Direction.DirectionConverter; import io.appium.droiddriver.scroll.Direction.PhysicalDirection; import io.appium.droiddriver.util.Logs; -import static io.appium.droiddriver.scroll.Direction.LogicalDirection.BACKWARD; - /** * A {@link Scroller} that looks for the desired item in the currently shown * content of the scrollable container, otherwise scrolls the container one step diff --git a/src/io/appium/droiddriver/uiautomation/UiAutomationElement.java b/src/io/appium/droiddriver/uiautomation/UiAutomationElement.java index cf7449e..5b99131 100644 --- a/src/io/appium/droiddriver/uiautomation/UiAutomationElement.java +++ b/src/io/appium/droiddriver/uiautomation/UiAutomationElement.java @@ -16,6 +16,8 @@ package io.appium.droiddriver.uiautomation; +import static io.appium.droiddriver.util.Strings.charSequenceToString; + import android.annotation.TargetApi; import android.app.UiAutomation; import android.app.UiAutomation.AccessibilityEventFilter; @@ -37,8 +39,6 @@ import io.appium.droiddriver.finders.Attribute; import io.appium.droiddriver.uiautomation.UiAutomationContext.UiAutomationCallable; import io.appium.droiddriver.util.Preconditions; -import static io.appium.droiddriver.util.Strings.charSequenceToString; - /** * A UiElement that gets attributes via the Accessibility API. */ @@ -96,9 +96,9 @@ public class UiAutomationElement extends BaseUiElement<AccessibilityNodeInfo, Ui put(attribs, Attribute.BOUNDS, getBounds(node)); attributes = Collections.unmodifiableMap(attribs); - // Order matters as getVisibleBounds depends on visible + // Order matters as findVisibleBounds depends on visible visible = node.isVisibleToUser(); - visibleBounds = getVisibleBounds(node); + visibleBounds = findVisibleBounds(); List<UiAutomationElement> mutableChildren = buildChildren(node); this.children = mutableChildren == null ? null : Collections.unmodifiableList(mutableChildren); } @@ -132,19 +132,19 @@ public class UiAutomationElement extends BaseUiElement<AccessibilityNodeInfo, Ui return rect; } - private Rect getVisibleBounds(AccessibilityNodeInfo node) { + private Rect findVisibleBounds() { if (!visible) { return new Rect(); } - Rect visibleBounds = getBounds(); + Rect foundBounds = getBounds(); UiAutomationElement parent = getParent(); - Rect parentBounds; while (parent != null) { - parentBounds = parent.getBounds(); - visibleBounds.intersect(parentBounds); + if (!foundBounds.intersect(parent.getBounds())) { + return new Rect(); + } parent = parent.getParent(); } - return visibleBounds; + return foundBounds; } @Override diff --git a/src/io/appium/droiddriver/util/ActivityUtils.java b/src/io/appium/droiddriver/util/ActivityUtils.java index 1e35de8..ff06ab5 100644 --- a/src/io/appium/droiddriver/util/ActivityUtils.java +++ b/src/io/appium/droiddriver/util/ActivityUtils.java @@ -17,47 +17,90 @@ package io.appium.droiddriver.util; import android.app.Activity; +import android.os.Looper; +import android.support.test.runner.lifecycle.ActivityLifecycleMonitorRegistry; +import android.support.test.runner.lifecycle.Stage; +import android.util.Log; +import java.util.Iterator; +import java.util.concurrent.Callable; -import io.appium.droiddriver.exceptions.UnrecoverableException; -import io.appium.droiddriver.instrumentation.InstrumentationDriver; - -/** - * Static helper methods for retrieving activities. - */ +/** Static helper methods for retrieving activities. */ public class ActivityUtils { - public interface Supplier<T> { - /** - * Retrieves an instance of the appropriate type. The returned object may or - * may not be a new instance, depending on the implementation. - * - * @return an instance of the appropriate type - */ - T get(); - } + private static final Callable<Activity> GET_RUNNING_ACTIVITY = + new Callable<Activity>() { + @Override + public Activity call() { + Iterator<Activity> activityIterator = + ActivityLifecycleMonitorRegistry.getInstance() + .getActivitiesInStage(Stage.RESUMED) + .iterator(); + return activityIterator.hasNext() ? activityIterator.next() : null; + } + }; + private static Supplier<Activity> runningActivitySupplier = + new Supplier<Activity>() { + @Override + public Activity get() { + try { + // If this is called on main (UI) thread, don't call runOnMainSync + if (Looper.myLooper() == Looper.getMainLooper()) { + return GET_RUNNING_ACTIVITY.call(); + } - private static Supplier<Activity> runningActivitySupplier; + return InstrumentationUtils.runOnMainSyncWithTimeout(GET_RUNNING_ACTIVITY); + } catch (Exception e) { + Logs.log(Log.WARN, e); + return null; + } + } + }; /** - * Sets the Supplier for the running (a.k.a. resumed or foreground) activity. - * Called from {@link io.appium.droiddriver.runner.TestRunner}. If a - * custom runner is used, this method must be called appropriately, otherwise - * {@link #getRunningActivity} won't work. + * Sets the Supplier for the running (a.k.a. resumed or foreground) activity. If a custom runner + * is used, this method must be called appropriately, otherwise {@link #getRunningActivity} won't + * work. */ public static synchronized void setRunningActivitySupplier(Supplier<Activity> activitySupplier) { - runningActivitySupplier = activitySupplier; + runningActivitySupplier = Preconditions.checkNotNull(activitySupplier); + } + + /** Shorthand to {@link #getRunningActivity(long)} with {@code timeoutMillis=30_000}. */ + public static Activity getRunningActivity() { + return getRunningActivity(30_000L); } /** - * Gets the running (a.k.a. resumed or foreground) activity. - * {@link InstrumentationDriver} depends on this. + * Waits for idle on main looper, then gets the running (a.k.a. resumed or foreground) activity. * * @return the currently running activity, or null if no activity has focus. */ - public static synchronized Activity getRunningActivity() { - if (runningActivitySupplier == null) { - throw new UnrecoverableException("If you don't use DroidDriver TestRunner, you need to call" + - " ActivityUtils.setRunningActivitySupplier appropriately"); + public static Activity getRunningActivity(long timeoutMillis) { + // It's safe to check running activity only when the main looper is idle. + // If the AUT is in background, its main looper should be idle already. + // If the AUT is in foreground, its main looper should be idle eventually. + if (InstrumentationUtils.tryWaitForIdleSync(timeoutMillis)) { + return getRunningActivityNoWait(); } + return null; + } + + /** + * Gets the running (a.k.a. resumed or foreground) activity without waiting for idle on main + * looper. + * + * @return the currently running activity, or null if no activity has focus. + */ + public static synchronized Activity getRunningActivityNoWait() { return runningActivitySupplier.get(); } + + public interface Supplier<T> { + /** + * Retrieves an instance of the appropriate type. The returned object may or may not be a new + * instance, depending on the implementation. + * + * @return an instance of the appropriate type + */ + T get(); + } } diff --git a/src/io/appium/droiddriver/util/InstrumentationUtils.java b/src/io/appium/droiddriver/util/InstrumentationUtils.java index c4f280d..06ac5ab 100644 --- a/src/io/appium/droiddriver/util/InstrumentationUtils.java +++ b/src/io/appium/droiddriver/util/InstrumentationUtils.java @@ -20,38 +20,34 @@ import android.app.Instrumentation; import android.content.Context; import android.os.Bundle; import android.os.Looper; +import android.support.test.InstrumentationRegistry; import android.util.Log; - +import io.appium.droiddriver.exceptions.DroidDriverException; +import io.appium.droiddriver.exceptions.TimeoutException; import java.util.concurrent.Callable; import java.util.concurrent.Executor; import java.util.concurrent.Executors; import java.util.concurrent.FutureTask; import java.util.concurrent.TimeUnit; -import io.appium.droiddriver.exceptions.DroidDriverException; -import io.appium.droiddriver.exceptions.TimeoutException; -import io.appium.droiddriver.exceptions.UnrecoverableException; - -/** - * Static utility methods pertaining to {@link Instrumentation}. - */ +/** Static utility methods pertaining to {@link Instrumentation}. */ public class InstrumentationUtils { + private static final Runnable EMPTY_RUNNABLE = + new Runnable() { + @Override + public void run() {} + }; + private static final Executor RUN_ON_MAIN_SYNC_EXECUTOR = Executors.newSingleThreadExecutor(); private static Instrumentation instrumentation; private static Bundle options; private static long runOnMainSyncTimeoutMillis; - private static final Runnable EMPTY_RUNNABLE = new Runnable() { - @Override - public void run() { - } - }; - private static final Executor RUN_ON_MAIN_SYNC_EXECUTOR = Executors.newSingleThreadExecutor(); /** - * Initializes this class. If you use a runner that is not DroidDriver-aware, you need to call - * this method appropriately. See {@link io.appium.droiddriver.runner.TestRunner#onCreate} for - * example. + * Initializes this class. If you don't use android.support.test.runner.AndroidJUnitRunner or a + * runner that supports {link InstrumentationRegistry}, you need to call this method + * appropriately. */ - public static void init(Instrumentation instrumentation, Bundle arguments) { + public static synchronized void init(Instrumentation instrumentation, Bundle arguments) { if (InstrumentationUtils.instrumentation != null) { throw new DroidDriverException("init() can only be called once"); } @@ -59,13 +55,13 @@ public class InstrumentationUtils { options = arguments; String timeoutString = getD2Option("runOnMainSyncTimeout"); - runOnMainSyncTimeoutMillis = timeoutString == null ? 10000L : Long.parseLong(timeoutString); + runOnMainSyncTimeoutMillis = timeoutString == null ? 10_000L : Long.parseLong(timeoutString); } - private static void checkInitialized() { + private static synchronized void checkInitialized() { if (instrumentation == null) { - throw new UnrecoverableException("If you use a runner that is not DroidDriver-aware, you" + - " need to call InstrumentationUtils.init appropriately"); + // Assume android.support.test.runner.InstrumentationRegistry is valid + init(InstrumentationRegistry.getInstrumentation(), InstrumentationRegistry.getArguments()); } } @@ -79,19 +75,16 @@ public class InstrumentationUtils { } /** - * Gets the <a href= "http://developer.android.com/tools/testing/testing_otheride.html#AMOptionsSyntax" - * >am instrument options</a>. + * Gets the <a href= + * "http://developer.android.com/tools/testing/testing_otheride.html#AMOptionsSyntax" >am + * instrument options</a>. */ public static Bundle getOptions() { checkInitialized(); return options; } - /** - * Gets the string value associated with the given key. This is preferred over using {@link - * #getOptions} because the returned {@link Bundle} contains only string values - am instrument - * options do not support value types other than string. - */ + /** Gets the string value associated with the given key. */ public static String getOption(String key) { return getOptions().getString(key); } @@ -110,14 +103,15 @@ public class InstrumentationUtils { * example, the ProgressBar. */ public static boolean tryWaitForIdleSync(long timeoutMillis) { - validateNotAppThread(); + checkNotMainThread(); FutureTask<Void> emptyTask = new FutureTask<Void>(EMPTY_RUNNABLE, null); - instrumentation.waitForIdle(emptyTask); + getInstrumentation().waitForIdle(emptyTask); try { emptyTask.get(timeoutMillis, TimeUnit.MILLISECONDS); } catch (java.util.concurrent.TimeoutException e) { - Logs.log(Log.INFO, + Logs.log( + Log.INFO, "Timed out after " + timeoutMillis + " milliseconds waiting for idle on main looper"); return false; } catch (Throwable t) { @@ -127,44 +121,51 @@ public class InstrumentationUtils { } public static void runOnMainSyncWithTimeout(final Runnable runnable) { - runOnMainSyncWithTimeout(new Callable<Void>() { - @Override - public Void call() throws Exception { - runnable.run(); - return null; - } - }); + runOnMainSyncWithTimeout( + new Callable<Void>() { + @Override + public Void call() throws Exception { + runnable.run(); + return null; + } + }); } /** * Runs {@code callable} on the main thread on best-effort basis up to a time limit, which * defaults to {@code 10000L} and can be set as an am instrument option under the key {@code - * dd.runOnMainSyncTimeout}. <p>This is a safer variation of {@link Instrumentation#runOnMainSync} - * because the latter may hang. You may turn off this behavior by setting {@code "-e - * dd.runOnMainSyncTimeout 0"} on the am command line.</p>The {@code callable} may never run, for - * example, if the main Looper has exited due to uncaught exception. + * dd.runOnMainSyncTimeout}. + * + * <p>This is a safer variation of {@link Instrumentation#runOnMainSync} because the latter may + * hang. You may turn off this behavior by setting {@code "-e dd.runOnMainSyncTimeout 0"} on the + * am command line.The {@code callable} may never run, for example, if the main Looper has exited + * due to uncaught exception. */ public static <V> V runOnMainSyncWithTimeout(Callable<V> callable) { - validateNotAppThread(); + checkNotMainThread(); final RunOnMainSyncFutureTask<V> futureTask = new RunOnMainSyncFutureTask<>(callable); if (runOnMainSyncTimeoutMillis <= 0L) { // Call runOnMainSync on current thread without time limit. futureTask.runOnMainSyncNoThrow(); } else { - RUN_ON_MAIN_SYNC_EXECUTOR.execute(new Runnable() { - @Override - public void run() { - futureTask.runOnMainSyncNoThrow(); - } - }); + RUN_ON_MAIN_SYNC_EXECUTOR.execute( + new Runnable() { + @Override + public void run() { + futureTask.runOnMainSyncNoThrow(); + } + }); } try { return futureTask.get(runOnMainSyncTimeoutMillis, TimeUnit.MILLISECONDS); } catch (java.util.concurrent.TimeoutException e) { - throw new TimeoutException("Timed out after " + runOnMainSyncTimeoutMillis - + " milliseconds waiting for Instrumentation.runOnMainSync", e); + throw new TimeoutException( + "Timed out after " + + runOnMainSyncTimeoutMillis + + " milliseconds waiting for Instrumentation.runOnMainSync", + e); } catch (Throwable t) { throw DroidDriverException.propagate(t); } finally { @@ -172,6 +173,18 @@ public class InstrumentationUtils { } } + public static void checkMainThread() { + if (Looper.myLooper() != Looper.getMainLooper()) { + throw new DroidDriverException("This method must be called on the main thread"); + } + } + + public static void checkNotMainThread() { + if (Looper.myLooper() == Looper.getMainLooper()) { + throw new DroidDriverException("This method cannot be called on the main thread"); + } + } + private static class RunOnMainSyncFutureTask<V> extends FutureTask<V> { public RunOnMainSyncFutureTask(Callable<V> callable) { super(callable); @@ -185,11 +198,4 @@ public class InstrumentationUtils { } } } - - private static void validateNotAppThread() { - if (Looper.myLooper() == Looper.getMainLooper()) { - throw new DroidDriverException( - "This method can not be called from the main application thread"); - } - } } diff --git a/src/io/appium/droiddriver/validators/DefaultAccessibilityValidator.java b/src/io/appium/droiddriver/validators/DefaultAccessibilityValidator.java index 1ce3649..b78d0a5 100644 --- a/src/io/appium/droiddriver/validators/DefaultAccessibilityValidator.java +++ b/src/io/appium/droiddriver/validators/DefaultAccessibilityValidator.java @@ -41,14 +41,8 @@ public class DefaultAccessibilityValidator implements Validator { // Logic from TalkBack private static boolean isAccessibilityFocusable(UiElement element) { - if (isActionableForAccessibility(element)) { - return true; - } - - if (isTopLevelScrollItem(element) && (isSpeakingNode(element))) { - return true; - } - return false; + return isActionableForAccessibility(element) + || (isTopLevelScrollItem(element) && isSpeakingNode(element)); } private static boolean isTopLevelScrollItem(UiElement element) { |