1/*
2 * Copyright (C) 2015 DroidDriver committers
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package io.appium.droiddriver.util;
18
19import android.app.Instrumentation;
20import android.content.Context;
21import android.os.Bundle;
22import android.os.Looper;
23import android.util.Log;
24
25import java.util.concurrent.Callable;
26import java.util.concurrent.Executor;
27import java.util.concurrent.Executors;
28import java.util.concurrent.FutureTask;
29import java.util.concurrent.TimeUnit;
30
31import io.appium.droiddriver.exceptions.DroidDriverException;
32import io.appium.droiddriver.exceptions.TimeoutException;
33import io.appium.droiddriver.exceptions.UnrecoverableException;
34
35/**
36 * Static utility methods pertaining to {@link Instrumentation}.
37 */
38public class InstrumentationUtils {
39  private static Instrumentation instrumentation;
40  private static Bundle options;
41  private static long runOnMainSyncTimeoutMillis;
42  private static final Runnable EMPTY_RUNNABLE = new Runnable() {
43    @Override
44    public void run() {
45    }
46  };
47  private static final Executor RUN_ON_MAIN_SYNC_EXECUTOR = Executors.newSingleThreadExecutor();
48
49  /**
50   * Initializes this class. If you use a runner that is not DroidDriver-aware, you need to call
51   * this method appropriately. See {@link io.appium.droiddriver.runner.TestRunner#onCreate} for
52   * example.
53   */
54  public static void init(Instrumentation instrumentation, Bundle arguments) {
55    if (InstrumentationUtils.instrumentation != null) {
56      throw new DroidDriverException("init() can only be called once");
57    }
58    InstrumentationUtils.instrumentation = instrumentation;
59    options = arguments;
60
61    String timeoutString = getD2Option("runOnMainSyncTimeout");
62    runOnMainSyncTimeoutMillis = timeoutString == null ? 10000L : Long.parseLong(timeoutString);
63  }
64
65  private static void checkInitialized() {
66    if (instrumentation == null) {
67      throw new UnrecoverableException("If you use a runner that is not DroidDriver-aware, you" +
68          " need to call InstrumentationUtils.init appropriately");
69    }
70  }
71
72  public static Instrumentation getInstrumentation() {
73    checkInitialized();
74    return instrumentation;
75  }
76
77  public static Context getTargetContext() {
78    return getInstrumentation().getTargetContext();
79  }
80
81  /**
82   * Gets the <a href= "http://developer.android.com/tools/testing/testing_otheride.html#AMOptionsSyntax"
83   * >am instrument options</a>.
84   */
85  public static Bundle getOptions() {
86    checkInitialized();
87    return options;
88  }
89
90  /**
91   * Gets the string value associated with the given key. This is preferred over using {@link
92   * #getOptions} because the returned {@link Bundle} contains only string values - am instrument
93   * options do not support value types other than string.
94   */
95  public static String getOption(String key) {
96    return getOptions().getString(key);
97  }
98
99  /**
100   * Calls {@link #getOption} with "dd." prefixed to {@code key}. This is for DroidDriver
101   * implementation to use a consistent pattern for its options.
102   */
103  public static String getD2Option(String key) {
104    return getOption("dd." + key);
105  }
106
107  /**
108   * Tries to wait for an idle state on the main thread on best-effort basis up to {@code
109   * timeoutMillis}. The main thread may not enter the idle state when animation is playing, for
110   * example, the ProgressBar.
111   */
112  public static boolean tryWaitForIdleSync(long timeoutMillis) {
113    validateNotAppThread();
114    FutureTask<Void> emptyTask = new FutureTask<Void>(EMPTY_RUNNABLE, null);
115    instrumentation.waitForIdle(emptyTask);
116
117    try {
118      emptyTask.get(timeoutMillis, TimeUnit.MILLISECONDS);
119    } catch (java.util.concurrent.TimeoutException e) {
120      Logs.log(Log.INFO,
121          "Timed out after " + timeoutMillis + " milliseconds waiting for idle on main looper");
122      return false;
123    } catch (Throwable t) {
124      throw DroidDriverException.propagate(t);
125    }
126    return true;
127  }
128
129  public static void runOnMainSyncWithTimeout(final Runnable runnable) {
130    runOnMainSyncWithTimeout(new Callable<Void>() {
131      @Override
132      public Void call() throws Exception {
133        runnable.run();
134        return null;
135      }
136    });
137  }
138
139  /**
140   * Runs {@code callable} on the main thread on best-effort basis up to a time limit, which
141   * defaults to {@code 10000L} and can be set as an am instrument option under the key {@code
142   * dd.runOnMainSyncTimeout}. <p>This is a safer variation of {@link Instrumentation#runOnMainSync}
143   * because the latter may hang. You may turn off this behavior by setting {@code "-e
144   * dd.runOnMainSyncTimeout 0"} on the am command line.</p>The {@code callable} may never run, for
145   * example, if the main Looper has exited due to uncaught exception.
146   */
147  public static <V> V runOnMainSyncWithTimeout(Callable<V> callable) {
148    validateNotAppThread();
149    final RunOnMainSyncFutureTask<V> futureTask = new RunOnMainSyncFutureTask<>(callable);
150
151    if (runOnMainSyncTimeoutMillis <= 0L) {
152      // Call runOnMainSync on current thread without time limit.
153      futureTask.runOnMainSyncNoThrow();
154    } else {
155      RUN_ON_MAIN_SYNC_EXECUTOR.execute(new Runnable() {
156        @Override
157        public void run() {
158          futureTask.runOnMainSyncNoThrow();
159        }
160      });
161    }
162
163    try {
164      return futureTask.get(runOnMainSyncTimeoutMillis, TimeUnit.MILLISECONDS);
165    } catch (java.util.concurrent.TimeoutException e) {
166      throw new TimeoutException("Timed out after " + runOnMainSyncTimeoutMillis
167          + " milliseconds waiting for Instrumentation.runOnMainSync", e);
168    } catch (Throwable t) {
169      throw DroidDriverException.propagate(t);
170    } finally {
171      futureTask.cancel(false);
172    }
173  }
174
175  private static class RunOnMainSyncFutureTask<V> extends FutureTask<V> {
176    public RunOnMainSyncFutureTask(Callable<V> callable) {
177      super(callable);
178    }
179
180    public void runOnMainSyncNoThrow() {
181      try {
182        getInstrumentation().runOnMainSync(this);
183      } catch (Throwable e) {
184        setException(e);
185      }
186    }
187  }
188
189  private static void validateNotAppThread() {
190    if (Looper.myLooper() == Looper.getMainLooper()) {
191      throw new DroidDriverException(
192          "This method can not be called from the main application thread");
193    }
194  }
195}
196