aboutsummaryrefslogtreecommitdiff
path: root/src/io/appium/droiddriver/util/InstrumentationUtils.java
diff options
context:
space:
mode:
Diffstat (limited to 'src/io/appium/droiddriver/util/InstrumentationUtils.java')
-rw-r--r--src/io/appium/droiddriver/util/InstrumentationUtils.java189
1 files changed, 189 insertions, 0 deletions
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");
+ }
+ }
+}