diff options
Diffstat (limited to 'src/io/appium/droiddriver/util/InstrumentationUtils.java')
-rw-r--r-- | src/io/appium/droiddriver/util/InstrumentationUtils.java | 189 |
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"); + } + } +} |