blob: c4f280d7cf3d2262ad875fdd7f275cabf256acd8 [file] [log] [blame]
/*
* 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.content.Context;
import android.os.Bundle;
import android.os.Looper;
import android.util.Log;
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}.
*/
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() {
}
};
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.
*/
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;
}
public static Context getTargetContext() {
return getInstrumentation().getTargetContext();
}
/**
* 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 t) {
throw DroidDriverException.propagate(t);
}
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. 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.
*/
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 {
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);
} catch (Throwable t) {
throw DroidDriverException.propagate(t);
} 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");
}
}
}