aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorKevin Jin <kjin@google.com>2015-03-05 15:40:13 -0800
committerKevin Jin <kjin@google.com>2015-03-05 18:00:44 -0800
commit367267b01bcc1ec5965cfc7c26149ccd405c11cf (patch)
tree9d638e644dc9079f06f8c8794cea09dbf404023b
parentf2c5634785d05fbdaff81a8b5f781910dd7a2894 (diff)
downloaddroiddriver-367267b01bcc1ec5965cfc7c26149ccd405c11cf.tar.gz
Add InstrumentationUtils
Move runOnMainSyncWithTimeout to InstrumentationUtils Add By.resourceId(int) since InstrumentationUtils should always be available Change-Id: I8e4683030583b88c3e2b777d0b80b5a2f310e279
-rw-r--r--droiddriver-android_support_test/src/io/appium/droiddriver/android_support_test/D2AndroidJUnitRunner.java68
-rw-r--r--manualtest/src/io/appium/droiddriver/manualtest/ManualTest.java9
-rw-r--r--src/io/appium/droiddriver/base/DroidDriverContext.java83
-rw-r--r--src/io/appium/droiddriver/finders/By.java14
-rw-r--r--src/io/appium/droiddriver/helpers/BaseDroidDriverTest.java26
-rw-r--r--src/io/appium/droiddriver/helpers/DroidDrivers.java88
-rw-r--r--src/io/appium/droiddriver/helpers/DroidDriversInitializer.java49
-rw-r--r--src/io/appium/droiddriver/instrumentation/InstrumentationDriver.java40
-rw-r--r--src/io/appium/droiddriver/instrumentation/InstrumentationUiDevice.java62
-rw-r--r--src/io/appium/droiddriver/instrumentation/ViewElement.java82
-rw-r--r--src/io/appium/droiddriver/runner/TestRunner.java59
-rw-r--r--src/io/appium/droiddriver/util/InstrumentationUtils.java189
12 files changed, 396 insertions, 373 deletions
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
index 7924380..4d916bd 100644
--- 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
@@ -26,13 +26,9 @@ import android.util.Log;
import java.util.Iterator;
import java.util.concurrent.Callable;
-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.helpers.DroidDrivers;
import io.appium.droiddriver.util.ActivityUtils;
+import io.appium.droiddriver.util.InstrumentationUtils;
import io.appium.droiddriver.util.Logs;
/**
@@ -50,12 +46,11 @@ public class D2AndroidJUnitRunner extends AndroidJUnitRunner {
};
/**
- * {@inheritDoc} <p> Sets the values for the convenience methods {@link
- * DroidDrivers#getInstrumentation()} and {@link DroidDrivers#getOptions()}.
+ * {@inheritDoc} <p> Initializes {@link InstrumentationUtils}.
*/
@Override
public void onCreate(Bundle arguments) {
- DroidDrivers.initInstrumentation(this, arguments);
+ InstrumentationUtils.init(this, arguments);
super.onCreate(arguments);
}
@@ -74,67 +69,14 @@ public class D2AndroidJUnitRunner extends AndroidJUnitRunner {
return GET_RUNNING_ACTIVITY.call();
}
- return runOnMainSyncWithTimeout(GET_RUNNING_ACTIVITY);
+ return InstrumentationUtils.runOnMainSyncWithTimeout(GET_RUNNING_ACTIVITY);
} catch (Exception e) {
Logs.log(Log.WARN, e);
return null;
}
}
});
- super.onStart();
- }
-
- /**
- * 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 <a href= "http://developer.android.com/tools/testing/testing_otheride.html#AMOptionsSyntax">
- * am instrument option</a> under the key {@code dd.runOnMainSyncTimeout}. <p>This is a safer
- * variation of {@link #runOnMainSync} because the latter may hang. But it is heavy because a new
- * thread is created for each call unless the am command line specifies {@code
- * dd.runOnMainSyncTimeout <=0} such as "-e dd.runOnMainSyncTimeout 0".</p>The {@code callable}
- * may never run, for example, in case that the main Looper has exited due to uncaught exception.
- */
- // TODO: move this to DroidDrivers
- // TODO: call runOnMainSync on a single worker thread?
- private <V> V runOnMainSyncWithTimeout(Callable<V> callable) {
- final RunOnMainSyncFutureTask<V> futureTask = new RunOnMainSyncFutureTask<>(callable);
-
- String timeoutMillisString = DroidDrivers.getOptions().getString("dd.runOnMainSyncTimeout");
- long timeoutMillis = timeoutMillisString == null? 10000L : Long.parseLong(timeoutMillisString);
- if (timeoutMillis <= 0L) {
- // Call runOnMainSync on current thread without time limit.
- futureTask.runOnMainSyncNoThrow();
- } else {
- new Thread() {
- @Override
- public void run() {
- futureTask.runOnMainSyncNoThrow();
- }
- }.start();
- }
- try {
- return futureTask.get(timeoutMillis, TimeUnit.MILLISECONDS);
- } catch (java.util.concurrent.TimeoutException e) {
- throw new TimeoutException("Timed out after " + timeoutMillis
- + " milliseconds waiting for Instrumentation.runOnMainSync", e);
- } catch (Throwable e) {
- throw new DroidDriverException(e);
- } finally {
- futureTask.cancel(false);
- }
- }
-
- private class RunOnMainSyncFutureTask<V> extends FutureTask<V> {
- public RunOnMainSyncFutureTask(Callable<V> callable) {
- super(callable);
- }
-
- public void runOnMainSyncNoThrow() {
- try {
- runOnMainSync(this);
- } catch (Throwable e) {
- setException(e);
- }
- }
+ super.onStart();
}
}
diff --git a/manualtest/src/io/appium/droiddriver/manualtest/ManualTest.java b/manualtest/src/io/appium/droiddriver/manualtest/ManualTest.java
index 6e95272..83966f7 100644
--- a/manualtest/src/io/appium/droiddriver/manualtest/ManualTest.java
+++ b/manualtest/src/io/appium/droiddriver/manualtest/ManualTest.java
@@ -5,6 +5,9 @@ import android.app.Activity;
import io.appium.droiddriver.finders.By;
import io.appium.droiddriver.finders.Finder;
import io.appium.droiddriver.helpers.BaseDroidDriverTest;
+import io.appium.droiddriver.helpers.DroidDrivers;
+import io.appium.droiddriver.helpers.DroidDriversInitializer;
+import io.appium.droiddriver.uiautomation.UiAutomationDriver;
/**
* This is for manually testing DroidDriver. It is not meant for continuous
@@ -24,6 +27,12 @@ public class ManualTest extends BaseDroidDriverTest<Activity> {
super(Activity.class);
}
+ // This does not instrument a certain AUT, so InstrumentationDriver won't work
+ protected void classSetUp() {
+ DroidDrivers.checkUiAutomation();
+ DroidDriversInitializer.get(new UiAutomationDriver(getInstrumentation())).singleRun();
+ }
+
public void testSetTextForPassword() {
Finder password_edit = By.resourceId("com.google.android.gsf.login:id/password_edit");
String oldPassword = "A fake password that is not empty and needs to be cleared by setText";
diff --git a/src/io/appium/droiddriver/base/DroidDriverContext.java b/src/io/appium/droiddriver/base/DroidDriverContext.java
index 89e2022..064d472 100644
--- a/src/io/appium/droiddriver/base/DroidDriverContext.java
+++ b/src/io/appium/droiddriver/base/DroidDriverContext.java
@@ -17,20 +17,11 @@
package io.appium.droiddriver.base;
import android.app.Instrumentation;
-import android.os.Looper;
-import android.util.Log;
-import java.util.Locale;
import java.util.Map;
import java.util.WeakHashMap;
-import java.util.concurrent.ExecutionException;
-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.finders.ByXPath;
-import io.appium.droiddriver.util.Logs;
/**
* Internal helper for DroidDriver implementation.
@@ -72,78 +63,4 @@ public class DroidDriverContext<R, E extends BaseUiElement<R, E>> {
map.clear();
ByXPath.clearData();
}
-
- /**
- * Tries to wait for an idle state on the main thread on best-effort basis up
- * to {@code timeoutMillis}. The main thread may not enter the idle state when
- * animation is playing, for example, the ProgressBar.
- */
- public boolean tryWaitForIdleSync(long timeoutMillis) {
- validateNotAppThread();
- FutureTask<?> futureTask = new FutureTask<Void>(new Runnable() {
- @Override
- public void run() {}
- }, null);
- instrumentation.waitForIdle(futureTask);
-
- try {
- futureTask.get(timeoutMillis, TimeUnit.MILLISECONDS);
- } catch (InterruptedException e) {
- throw new DroidDriverException(e);
- } catch (ExecutionException e) {
- throw new DroidDriverException(e);
- } catch (java.util.concurrent.TimeoutException e) {
- Logs.log(Log.DEBUG, String.format(Locale.US,
- "Timed out after %d milliseconds waiting for idle on main looper", timeoutMillis));
- return false;
- }
- return true;
- }
-
- /**
- * Tries to run {@code runnable} on the main thread on best-effort basis up to
- * {@code timeoutMillis}. The {@code runnable} may never run, for example, in
- * case that the main Looper has exited due to uncaught exception.
- */
- public boolean tryRunOnMainSync(Runnable runnable, long timeoutMillis) {
- validateNotAppThread();
- final FutureTask<?> futureTask = new FutureTask<Void>(runnable, null);
- new Thread(new Runnable() {
- @Override
- public void run() {
- instrumentation.runOnMainSync(futureTask);
- }
- }).start();
-
- try {
- futureTask.get(timeoutMillis, TimeUnit.MILLISECONDS);
- } catch (InterruptedException e) {
- throw new DroidDriverException(e);
- } catch (ExecutionException e) {
- throw new DroidDriverException(e);
- } catch (java.util.concurrent.TimeoutException e) {
- Logs.log(Log.WARN, getRunOnMainSyncTimeoutMessage(timeoutMillis));
- return false;
- }
- return true;
- }
-
- public void runOnMainSync(Runnable runnable) {
- long timeoutMillis = getDriver().getPoller().getTimeoutMillis();
- if (!tryRunOnMainSync(runnable, timeoutMillis)) {
- throw new TimeoutException(getRunOnMainSyncTimeoutMessage(timeoutMillis));
- }
- }
-
- private String getRunOnMainSyncTimeoutMessage(long timeoutMillis) {
- return String.format(Locale.US,
- "Timed out after %d milliseconds waiting for Instrumentation.runOnMainSync", timeoutMillis);
- }
-
- private 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/finders/By.java b/src/io/appium/droiddriver/finders/By.java
index 874cd29..f8ac924 100644
--- a/src/io/appium/droiddriver/finders/By.java
+++ b/src/io/appium/droiddriver/finders/By.java
@@ -16,8 +16,11 @@
package io.appium.droiddriver.finders;
+import android.content.Context;
+
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;
@@ -44,7 +47,16 @@ public class By {
return new MatchFinder(Predicates.attributeFalse(attribute));
}
- /** Matches a UiElement by resource id. */
+ /** 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.
+ */
public static MatchFinder resourceId(String resourceId) {
return new MatchFinder(Predicates.attributeEquals(Attribute.RESOURCE_ID, resourceId));
}
diff --git a/src/io/appium/droiddriver/helpers/BaseDroidDriverTest.java b/src/io/appium/droiddriver/helpers/BaseDroidDriverTest.java
index 7b29bc0..0b17dc5 100644
--- a/src/io/appium/droiddriver/helpers/BaseDroidDriverTest.java
+++ b/src/io/appium/droiddriver/helpers/BaseDroidDriverTest.java
@@ -17,7 +17,6 @@
package io.appium.droiddriver.helpers;
import android.app.Activity;
-import android.app.Instrumentation;
import android.content.Context;
import android.os.Debug;
import android.test.FlakyTest;
@@ -41,29 +40,6 @@ import io.appium.droiddriver.util.Logs;
*/
public abstract class BaseDroidDriverTest<T extends Activity> extends
D2ActivityInstrumentationTestCase2<T> {
- /**
- * Calls {@link DroidDrivers#init} once and only once.
- */
- public static class DroidDriversInitializer extends SingleRun {
- private static DroidDriversInitializer instance;
- protected final Instrumentation instrumentation;
-
- protected DroidDriversInitializer(Instrumentation instrumentation) {
- this.instrumentation = instrumentation;
- }
-
- @Override
- protected void run() {
- DroidDrivers.init(DroidDrivers.newDriver(instrumentation));
- }
-
- public static synchronized DroidDriversInitializer get(Instrumentation instrumentation) {
- if (instance == null) {
- instance = new DroidDriversInitializer(instrumentation);
- }
- return instance;
- }
- }
private static boolean classSetUpDone = false;
// In case of device-wide fatal errors, e.g. OOME, the remaining tests will
@@ -120,7 +96,7 @@ public abstract class BaseDroidDriverTest<T extends Activity> extends
* io.appium.droiddriver.instrumentation.ViewElement#overrideClassName}
*/
protected void classSetUp() {
- DroidDriversInitializer.get(getInstrumentation()).singleRun();
+ DroidDriversInitializer.get(DroidDrivers.newDriver()).singleRun();
}
protected boolean reportSkippedAsFailed() {
diff --git a/src/io/appium/droiddriver/helpers/DroidDrivers.java b/src/io/appium/droiddriver/helpers/DroidDrivers.java
index 60c3740..5cddb4f 100644
--- a/src/io/appium/droiddriver/helpers/DroidDrivers.java
+++ b/src/io/appium/droiddriver/helpers/DroidDrivers.java
@@ -19,7 +19,6 @@ package io.appium.droiddriver.helpers;
import android.annotation.TargetApi;
import android.app.Instrumentation;
import android.os.Build;
-import android.os.Bundle;
import java.lang.reflect.InvocationTargetException;
@@ -27,83 +26,53 @@ import io.appium.droiddriver.DroidDriver;
import io.appium.droiddriver.exceptions.DroidDriverException;
import io.appium.droiddriver.instrumentation.InstrumentationDriver;
import io.appium.droiddriver.uiautomation.UiAutomationDriver;
+import io.appium.droiddriver.util.InstrumentationUtils;
/**
- * Static utility methods pertaining to {@link DroidDriver} instances.
+ * Static utility methods using a singleton {@link DroidDriver} instance. This class is NOT
+ * required, but it is handy and using a singleton driver can avoid memory leak when you have many
+ * instances around (for example, one in every test - JUnit framework keeps the test instances in
+ * memory after running them).
*/
public class DroidDrivers {
private static DroidDriver driver;
- private static Instrumentation instrumentation;
- private static Bundle options;
/**
- * Gets the singleton driver. Throws if {@link #init} has not been called.
+ * Gets the singleton driver. Throws if {@link #setSingleton} has not been called.
*/
public static DroidDriver get() {
- if (DroidDrivers.driver == null) {
- throw new DroidDriverException("init() has not been called");
+ if (driver == null) {
+ throw new DroidDriverException("setSingleton() has not been called");
}
- return DroidDrivers.driver;
+ return driver;
}
/**
- * Initializes the singleton driver. The singleton driver is NOT required, but
- * it is handy and using a singleton driver can avoid memory leak if you have
- * many instances around (for example, one in every test -- JUnit framework
- * keeps the test instances in memory after running them).
+ * Sets the singleton driver.
*/
- public static void init(DroidDriver driver) {
+ public static void setSingleton(DroidDriver driver) {
if (DroidDrivers.driver != null) {
- throw new DroidDriverException("init() can only be called once");
+ throw new DroidDriverException("setSingleton() can only be called once");
}
DroidDrivers.driver = driver;
}
/**
- * Initializes for the convenience methods {@link #getInstrumentation()} and
- * {@link #getOptions()}. Called by
- * {@link io.appium.droiddriver.runner.TestRunner}. If a custom
- * runner is used, this method must be called appropriately, otherwise the two
- * convenience methods won't work.
- */
- public static void initInstrumentation(Instrumentation instrumentation, Bundle arguments) {
- if (DroidDrivers.instrumentation != null) {
- throw new DroidDriverException("DroidDrivers.initInstrumentation() can only be called once");
- }
- DroidDrivers.instrumentation = instrumentation;
- DroidDrivers.options = arguments;
- }
-
- public static Instrumentation getInstrumentation() {
- return instrumentation;
- }
-
- /**
- * Gets the <a href=
- * "http://developer.android.com/tools/testing/testing_otheride.html#AMOptionsSyntax"
- * >am instrument options</a>.
- */
- public static Bundle getOptions() {
- return options;
- }
-
- /**
- * Returns whether the running target (device or emulator) has
- * {@link android.app.UiAutomation} API, which is introduced in SDK API 18
- * (JELLY_BEAN_MR2).
+ * Returns whether the running target (device or emulator) has {@link android.app.UiAutomation}
+ * API, which is introduced in SDK API 18 (JELLY_BEAN_MR2).
*/
public static boolean hasUiAutomation() {
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2;
}
/**
- * Returns a new DroidDriver instance. If am instrument options have "driver",
- * treat it as the fully-qualified-class-name and create a new instance of it
- * with {@code instrumentation} as the argument; otherwise a new
- * platform-dependent default DroidDriver instance.
+ * Returns a new DroidDriver instance. If am instrument options have "driver", treat it as the
+ * fully-qualified-class-name and create a new instance of it with {@code instrumentation} as the
+ * argument; otherwise a new platform-dependent default DroidDriver instance.
*/
- public static DroidDriver newDriver(Instrumentation instrumentation) {
- String driverClass = options == null ? null : options.getString("driver");
+ public static DroidDriver newDriver() {
+ Instrumentation instrumentation = InstrumentationUtils.getInstrumentation();
+ String driverClass = InstrumentationUtils.getD2Option("driver");
if (driverClass != null) {
try {
return (DroidDriver) Class.forName(driverClass).getConstructor(Instrumentation.class)
@@ -123,29 +92,24 @@ public class DroidDrivers {
}
}
- // If "driver" is not specified, return default.
+ // If "dd.driver" is not specified, return default.
if (hasUiAutomation()) {
- return newUiAutomationDriver(instrumentation);
+ checkUiAutomation();
+ return new UiAutomationDriver(instrumentation);
}
- return newInstrumentationDriver(instrumentation);
- }
-
- /** Returns a new InstrumentationDriver */
- public static InstrumentationDriver newInstrumentationDriver(Instrumentation instrumentation) {
return new InstrumentationDriver(instrumentation);
}
- /** Returns a new UiAutomationDriver */
+ /** Checks if UiAutomation API is available */
@TargetApi(18)
- public static UiAutomationDriver newUiAutomationDriver(Instrumentation instrumentation) {
+ public static void checkUiAutomation() {
if (!hasUiAutomation()) {
throw new DroidDriverException("UiAutomation is not available below API 18. "
+ "See http://developer.android.com/reference/android/app/UiAutomation.html");
}
- if (instrumentation.getUiAutomation() == null) {
+ if (InstrumentationUtils.getInstrumentation().getUiAutomation() == null) {
throw new DroidDriverException(
"uiAutomation==null: did you forget to set '-w' flag for 'am instrument'?");
}
- return new UiAutomationDriver(instrumentation);
}
}
diff --git a/src/io/appium/droiddriver/helpers/DroidDriversInitializer.java b/src/io/appium/droiddriver/helpers/DroidDriversInitializer.java
new file mode 100644
index 0000000..727c97b
--- /dev/null
+++ b/src/io/appium/droiddriver/helpers/DroidDriversInitializer.java
@@ -0,0 +1,49 @@
+/*
+ * 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.helpers;
+
+import io.appium.droiddriver.DroidDriver;
+import io.appium.droiddriver.exceptions.UnrecoverableException;
+
+/**
+ * Calls {@link DroidDrivers#setSingleton} once and only once.
+ */
+public class DroidDriversInitializer extends SingleRun {
+ private static DroidDriversInitializer instance;
+ private final DroidDriver driver;
+
+ private DroidDriversInitializer(DroidDriver driver) {
+ this.driver = driver;
+ }
+
+ @Override
+ protected void run() {
+ DroidDrivers.setSingleton(driver);
+ }
+
+ public static synchronized DroidDriversInitializer get(DroidDriver driver) {
+ if (instance == null) {
+ instance = new DroidDriversInitializer(driver);
+ }
+
+ if (instance.driver != driver) {
+ throw new UnrecoverableException("The singleton DroidDriversInitializer has already been" +
+ " created with a different driver");
+ }
+ return instance;
+ }
+}
diff --git a/src/io/appium/droiddriver/instrumentation/InstrumentationDriver.java b/src/io/appium/droiddriver/instrumentation/InstrumentationDriver.java
index fa3fb8e..d3e5dd2 100644
--- a/src/io/appium/droiddriver/instrumentation/InstrumentationDriver.java
+++ b/src/io/appium/droiddriver/instrumentation/InstrumentationDriver.java
@@ -22,13 +22,14 @@ 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;
-import io.appium.droiddriver.exceptions.DroidDriverException;
import io.appium.droiddriver.exceptions.NoRunningActivityException;
import io.appium.droiddriver.util.ActivityUtils;
+import io.appium.droiddriver.util.InstrumentationUtils;
import io.appium.droiddriver.util.Logs;
/**
@@ -60,39 +61,26 @@ public class InstrumentationDriver extends BaseDroidDriver<View, ViewElement> {
return new ViewElement(context, rawElement, parent);
}
- private static class FindRootViewRunnable implements Runnable {
- View rootView;
- Throwable exception;
-
+ private static final Callable<View> FIND_ROOT_VIEW = new Callable<View>() {
@Override
- public void run() {
- try {
- List<View> views = RootFinder.getRootViews();
- if (views.size() > 1) {
- Logs.log(Log.VERBOSE, "views.size()=" + views.size());
- for (View view : views) {
- if (view.hasWindowFocus()) {
- rootView = view;
- return;
- }
+ 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.
- rootView = ActivityUtils.getRunningActivity().getWindow().getDecorView();
- } catch (Throwable e) {
- exception = e;
}
+ // Fall back to DecorView.
+ return ActivityUtils.getRunningActivity().getWindow().getDecorView();
}
- }
+ };
private View findRootView() {
waitForRunningActivity();
- FindRootViewRunnable findRootViewRunnable = new FindRootViewRunnable();
- context.runOnMainSync(findRootViewRunnable);
- if (findRootViewRunnable.exception != null) {
- throw new DroidDriverException(findRootViewRunnable.exception);
- }
- return findRootViewRunnable.rootView;
+ return InstrumentationUtils.runOnMainSyncWithTimeout(FIND_ROOT_VIEW);
}
private void waitForRunningActivity() {
diff --git a/src/io/appium/droiddriver/instrumentation/InstrumentationUiDevice.java b/src/io/appium/droiddriver/instrumentation/InstrumentationUiDevice.java
index 3e3b35c..f485f89 100644
--- a/src/io/appium/droiddriver/instrumentation/InstrumentationUiDevice.java
+++ b/src/io/appium/droiddriver/instrumentation/InstrumentationUiDevice.java
@@ -24,8 +24,11 @@ import android.graphics.RectF;
import android.util.Log;
import android.view.View;
+import java.util.concurrent.Callable;
+
import io.appium.droiddriver.base.BaseUiDevice;
import io.appium.droiddriver.base.DroidDriverContext;
+import io.appium.droiddriver.util.InstrumentationUtils;
import io.appium.droiddriver.util.Logs;
class InstrumentationUiDevice extends BaseUiDevice {
@@ -37,10 +40,13 @@ class InstrumentationUiDevice extends BaseUiDevice {
@Override
protected Bitmap takeScreenshot() {
- ScreenshotRunnable screenshotRunnable =
- new ScreenshotRunnable(context.getDriver().getRootElement().getRawElement());
- context.runOnMainSync(screenshotRunnable);
- return screenshotRunnable.screenshot;
+ try {
+ return InstrumentationUtils.runOnMainSyncWithTimeout(new GetScreenshot(
+ context.getDriver().getRootElement().getRawElement()));
+ } catch (Throwable e) {
+ Logs.log(Log.ERROR, e);
+ return null;
+ }
}
@Override
@@ -48,38 +54,36 @@ class InstrumentationUiDevice extends BaseUiDevice {
return context;
}
- private static class ScreenshotRunnable implements Runnable {
+ private static class GetScreenshot implements Callable<Bitmap> {
private final View rootView;
- Bitmap screenshot;
- private ScreenshotRunnable(View rootView) {
+ private GetScreenshot(View rootView) {
this.rootView = rootView;
}
@Override
- public void run() {
- try {
- rootView.destroyDrawingCache();
- rootView.buildDrawingCache(false);
- Bitmap drawingCache = rootView.getDrawingCache();
- int[] xy = new int[2];
- rootView.getLocationOnScreen(xy);
- if (xy[0] == 0 && xy[1] == 0) {
- screenshot = Bitmap.createBitmap(drawingCache);
- } else {
- Canvas canvas = new Canvas();
- Rect rect = new Rect(0, 0, drawingCache.getWidth(), drawingCache.getHeight());
- rect.offset(xy[0], xy[1]);
- screenshot =
- Bitmap.createBitmap(rect.width() + xy[0], rect.height() + xy[1], Config.ARGB_8888);
- canvas.setBitmap(screenshot);
- canvas.drawBitmap(drawingCache, null, new RectF(rect), null);
- canvas.setBitmap(null);
- }
- rootView.destroyDrawingCache();
- } catch (Throwable e) {
- Logs.log(Log.ERROR, e);
+ public Bitmap call() {
+ Bitmap screenshot;
+ rootView.destroyDrawingCache();
+ rootView.buildDrawingCache(false);
+ Bitmap drawingCache = rootView.getDrawingCache();
+
+ int[] xy = new int[2];
+ rootView.getLocationOnScreen(xy);
+ if (xy[0] == 0 && xy[1] == 0) {
+ screenshot = Bitmap.createBitmap(drawingCache);
+ } else {
+ Canvas canvas = new Canvas();
+ Rect rect = new Rect(0, 0, drawingCache.getWidth(), drawingCache.getHeight());
+ rect.offset(xy[0], xy[1]);
+ screenshot =
+ Bitmap.createBitmap(rect.width() + xy[0], rect.height() + xy[1], Config.ARGB_8888);
+ canvas.setBitmap(screenshot);
+ canvas.drawBitmap(drawingCache, null, new RectF(rect), null);
+ canvas.setBitmap(null);
}
+ rootView.destroyDrawingCache();
+ return screenshot;
}
}
}
diff --git a/src/io/appium/droiddriver/instrumentation/ViewElement.java b/src/io/appium/droiddriver/instrumentation/ViewElement.java
index a92dee4..e706362 100644
--- a/src/io/appium/droiddriver/instrumentation/ViewElement.java
+++ b/src/io/appium/droiddriver/instrumentation/ViewElement.java
@@ -30,13 +30,14 @@ 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.exceptions.DroidDriverException;
import io.appium.droiddriver.finders.Attribute;
+import io.appium.droiddriver.util.InstrumentationUtils;
import io.appium.droiddriver.util.Preconditions;
import static io.appium.droiddriver.util.Strings.charSequenceToString;
@@ -45,55 +46,51 @@ import static io.appium.droiddriver.util.Strings.charSequenceToString;
* A UiElement that is backed by a View.
*/
public class ViewElement extends BaseUiElement<View, ViewElement> {
- private static class SnapshotViewAttributesRunnable implements Runnable {
+ private static class AttributesSnapshot implements Callable<Void> {
private final View view;
final Map<Attribute, Object> attribs = new EnumMap<Attribute, Object>(Attribute.class);
boolean visible;
Rect visibleBounds;
List<View> childViews;
- Throwable exception;
- private SnapshotViewAttributesRunnable(View view) {
+ private AttributesSnapshot(View view) {
this.view = view;
}
@Override
- public void run() {
- try {
- put(Attribute.PACKAGE, view.getContext().getPackageName());
- put(Attribute.CLASS, getClassName());
- put(Attribute.TEXT, getText());
- put(Attribute.CONTENT_DESC, charSequenceToString(view.getContentDescription()));
- put(Attribute.RESOURCE_ID, getResourceId());
- put(Attribute.CHECKABLE, view instanceof Checkable);
- put(Attribute.CHECKED, isChecked());
- put(Attribute.CLICKABLE, view.isClickable());
- put(Attribute.ENABLED, view.isEnabled());
- put(Attribute.FOCUSABLE, view.isFocusable());
- put(Attribute.FOCUSED, view.isFocused());
- put(Attribute.LONG_CLICKABLE, view.isLongClickable());
- put(Attribute.PASSWORD, isPassword());
- put(Attribute.SCROLLABLE, isScrollable());
- if (view instanceof TextView) {
- TextView textView = (TextView) view;
- if (textView.hasSelection()) {
- attribs.put(Attribute.SELECTION_START, textView.getSelectionStart());
- attribs.put(Attribute.SELECTION_END, textView.getSelectionEnd());
- }
+ public Void call() {
+ put(Attribute.PACKAGE, view.getContext().getPackageName());
+ put(Attribute.CLASS, getClassName());
+ put(Attribute.TEXT, getText());
+ put(Attribute.CONTENT_DESC, charSequenceToString(view.getContentDescription()));
+ put(Attribute.RESOURCE_ID, getResourceId());
+ put(Attribute.CHECKABLE, view instanceof Checkable);
+ put(Attribute.CHECKED, isChecked());
+ put(Attribute.CLICKABLE, view.isClickable());
+ put(Attribute.ENABLED, view.isEnabled());
+ put(Attribute.FOCUSABLE, view.isFocusable());
+ put(Attribute.FOCUSED, view.isFocused());
+ put(Attribute.LONG_CLICKABLE, view.isLongClickable());
+ put(Attribute.PASSWORD, isPassword());
+ put(Attribute.SCROLLABLE, isScrollable());
+ if (view instanceof TextView) {
+ TextView textView = (TextView) view;
+ if (textView.hasSelection()) {
+ attribs.put(Attribute.SELECTION_START, textView.getSelectionStart());
+ attribs.put(Attribute.SELECTION_END, textView.getSelectionEnd());
}
- put(Attribute.SELECTED, view.isSelected());
- put(Attribute.BOUNDS, getBounds());
-
- // Order matters as setVisible() depends on setVisibleBounds().
- this.visibleBounds = getVisibleBounds();
- // isShown() checks the visibility flag of this view and ancestors; it
- // needs to have the VISIBLE flag as well as non-empty bounds to be
- // visible.
- this.visible = view.isShown() && !visibleBounds.isEmpty();
- setChildViews();
- } catch (Throwable e) {
- exception = e;
}
+ put(Attribute.SELECTED, view.isSelected());
+ put(Attribute.BOUNDS, getBounds());
+
+ // Order matters as setVisible() depends on setVisibleBounds().
+ this.visibleBounds = getVisibleBounds();
+ // isShown() checks the visibility flag of this view and ancestors; it
+ // needs to have the VISIBLE flag as well as non-empty bounds to be
+ // visible.
+ this.visible = view.isShown() && !visibleBounds.isEmpty();
+ setChildViews();
+ return null;
}
private void put(Attribute key, Object value) {
@@ -223,11 +220,8 @@ public class ViewElement extends BaseUiElement<View, ViewElement> {
this.context = Preconditions.checkNotNull(context);
this.view = Preconditions.checkNotNull(view);
this.parent = parent;
- SnapshotViewAttributesRunnable attributesSnapshot = new SnapshotViewAttributesRunnable(view);
- context.runOnMainSync(attributesSnapshot);
- if (attributesSnapshot.exception != null) {
- throw new DroidDriverException(attributesSnapshot.exception);
- }
+ AttributesSnapshot attributesSnapshot = new AttributesSnapshot(view);
+ InstrumentationUtils.runOnMainSyncWithTimeout(attributesSnapshot);
attributes = Collections.unmodifiableMap(attributesSnapshot.attribs);
this.visibleBounds = attributesSnapshot.visibleBounds;
@@ -276,7 +270,7 @@ public class ViewElement extends BaseUiElement<View, ViewElement> {
@Override
protected void doPerformAndWait(FutureTask<Boolean> futureTask, long timeoutMillis) {
futureTask.run();
- context.tryWaitForIdleSync(timeoutMillis);
+ InstrumentationUtils.tryWaitForIdleSync(timeoutMillis);
}
@Override
diff --git a/src/io/appium/droiddriver/runner/TestRunner.java b/src/io/appium/droiddriver/runner/TestRunner.java
index be92b44..71bb744 100644
--- a/src/io/appium/droiddriver/runner/TestRunner.java
+++ b/src/io/appium/droiddriver/runner/TestRunner.java
@@ -35,12 +35,11 @@ import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
-import java.util.concurrent.FutureTask;
-import java.util.concurrent.TimeUnit;
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;
/**
@@ -63,12 +62,11 @@ public class TestRunner extends InstrumentationTestRunner {
/**
* {@inheritDoc}
* <p>
- * Sets the values for the convenience methods {@link DroidDrivers#getInstrumentation()} and
- * {@link DroidDrivers#getOptions()}.
+ * Initializes {@link InstrumentationUtils}.
*/
@Override
public void onCreate(Bundle arguments) {
- DroidDrivers.initInstrumentation(this, arguments);
+ InstrumentationUtils.init(this, arguments);
super.onCreate(arguments);
}
@@ -92,21 +90,25 @@ public class TestRunner extends InstrumentationTestRunner {
activitiesCopy = activities.toArray(new Activity[activities.size()]);
}
- runOnMainSyncWithTimeLimit(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");
+ 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) {
@@ -198,27 +200,4 @@ public class TestRunner extends InstrumentationTestRunner {
runningActivity = null;
}
}
-
- private boolean runOnMainSyncWithTimeLimit(Runnable runnable) {
- // Do we need it configurable? Now only used in endTest.
- long timeoutMillis = 10000L;
- final FutureTask<?> futureTask = new FutureTask<Void>(runnable, null);
- new Thread(new Runnable() {
- @Override
- public void run() {
- runOnMainSync(futureTask);
- }
- }).start();
-
- try {
- futureTask.get(timeoutMillis, TimeUnit.MILLISECONDS);
- return true;
- } catch (Throwable e) {
- Logs.log(Log.WARN, e, String.format(
- "Timed out after %d milliseconds waiting for Instrumentation.runOnMainSync",
- timeoutMillis));
- futureTask.cancel(false);
- return false;
- }
- }
}
diff --git a/src/io/appium/droiddriver/util/InstrumentationUtils.java b/src/io/appium/droiddriver/util/InstrumentationUtils.java
new file mode 100644
index 0000000..a019008
--- /dev/null
+++ b/src/io/appium/droiddriver/util/InstrumentationUtils.java
@@ -0,0 +1,189 @@
+/*
+ * 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.util;
+
+import android.app.Instrumentation;
+import android.os.Bundle;
+import android.os.Looper;
+import android.util.Log;
+
+import java.util.concurrent.Callable;
+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}.
+ */
+public class InstrumentationUtils {
+ private static Instrumentation instrumentation;
+ private static Bundle options;
+ private static long runOnMainSyncTimeoutMillis;
+ private static final Runnable EMPTY_RUNNABLE = new Runnable() {
+ @Override
+ public void run() {
+ }
+ };
+
+ /**
+ * 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.
+ */
+ public static void init(Instrumentation instrumentation, Bundle arguments) {
+ if (InstrumentationUtils.instrumentation != null) {
+ throw new DroidDriverException("init() can only be called once");
+ }
+ InstrumentationUtils.instrumentation = instrumentation;
+ options = arguments;
+
+ String timeoutString = getD2Option("runOnMainSyncTimeout");
+ runOnMainSyncTimeoutMillis = timeoutString == null ? 10000L : Long.parseLong(timeoutString);
+ }
+
+ private static 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");
+ }
+ }
+
+ public static Instrumentation getInstrumentation() {
+ checkInitialized();
+ return instrumentation;
+ }
+
+ /**
+ * 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.
+ */
+ public static String getOption(String key) {
+ return getOptions().getString(key);
+ }
+
+ /**
+ * Calls {@link #getOption} with "dd." prefixed to {@code key}. This is for DroidDriver
+ * implementation to use a consistent pattern for its options.
+ */
+ public static String getD2Option(String key) {
+ return getOption("dd." + key);
+ }
+
+ /**
+ * Tries to wait for an idle state on the main thread on best-effort basis up to {@code
+ * timeoutMillis}. The main thread may not enter the idle state when animation is playing, for
+ * example, the ProgressBar.
+ */
+ public static boolean tryWaitForIdleSync(long timeoutMillis) {
+ validateNotAppThread();
+ FutureTask<Void> emptyTask = new FutureTask<Void>(EMPTY_RUNNABLE, null);
+ instrumentation.waitForIdle(emptyTask);
+
+ try {
+ emptyTask.get(timeoutMillis, TimeUnit.MILLISECONDS);
+ } catch (java.util.concurrent.TimeoutException e) {
+ Logs.log(Log.INFO,
+ "Timed out after " + timeoutMillis + " milliseconds waiting for idle on main looper");
+ return false;
+ } catch (Throwable e) {
+ throw new DroidDriverException(e);
+ }
+ return true;
+ }
+
+ public static void runOnMainSyncWithTimeout(final Runnable runnable) {
+ 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. But it is heavy because a new thread is created for each call. 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, in case that the main Looper has
+ * exited due to uncaught exception.
+ */
+ // TODO: call runOnMainSync on a single worker thread?
+ public static <V> V runOnMainSyncWithTimeout(Callable<V> callable) {
+ validateNotAppThread();
+ final RunOnMainSyncFutureTask<V> futureTask = new RunOnMainSyncFutureTask<>(callable);
+
+ if (runOnMainSyncTimeoutMillis <= 0L) {
+ // Call runOnMainSync on current thread without time limit.
+ futureTask.runOnMainSyncNoThrow();
+ } else {
+ new Thread() {
+ @Override
+ public void run() {
+ futureTask.runOnMainSyncNoThrow();
+ }
+ }.start();
+ }
+
+ 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);
+ } catch (Throwable e) {
+ throw new DroidDriverException(e);
+ } finally {
+ futureTask.cancel(false);
+ }
+ }
+
+ private static class RunOnMainSyncFutureTask<V> extends FutureTask<V> {
+ public RunOnMainSyncFutureTask(Callable<V> callable) {
+ super(callable);
+ }
+
+ public void runOnMainSyncNoThrow() {
+ try {
+ getInstrumentation().runOnMainSync(this);
+ } catch (Throwable e) {
+ setException(e);
+ }
+ }
+ }
+
+ private static void validateNotAppThread() {
+ if (Looper.myLooper() == Looper.getMainLooper()) {
+ throw new DroidDriverException(
+ "This method can not be called from the main application thread");
+ }
+ }
+}