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.support.test.InstrumentationRegistry;
24import android.util.Log;
25import io.appium.droiddriver.exceptions.DroidDriverException;
26import io.appium.droiddriver.exceptions.TimeoutException;
27import java.util.concurrent.Callable;
28import java.util.concurrent.Executor;
29import java.util.concurrent.Executors;
30import java.util.concurrent.FutureTask;
31import java.util.concurrent.TimeUnit;
32
33/** Static utility methods pertaining to {@link Instrumentation}. */
34public class InstrumentationUtils {
35  private static final Runnable EMPTY_RUNNABLE =
36      new Runnable() {
37        @Override
38        public void run() {}
39      };
40  private static final Executor RUN_ON_MAIN_SYNC_EXECUTOR = Executors.newSingleThreadExecutor();
41  private static Instrumentation instrumentation;
42  private static Bundle options;
43  private static long runOnMainSyncTimeoutMillis;
44
45  /**
46   * Initializes this class. If you don't use android.support.test.runner.AndroidJUnitRunner or a
47   * runner that supports {link InstrumentationRegistry}, you need to call this method
48   * appropriately.
49   */
50  public static synchronized void init(Instrumentation instrumentation, Bundle arguments) {
51    if (InstrumentationUtils.instrumentation != null) {
52      throw new DroidDriverException("init() can only be called once");
53    }
54    InstrumentationUtils.instrumentation = instrumentation;
55    options = arguments;
56
57    String timeoutString = getD2Option("runOnMainSyncTimeout");
58    runOnMainSyncTimeoutMillis = timeoutString == null ? 10_000L : Long.parseLong(timeoutString);
59  }
60
61  private static synchronized void checkInitialized() {
62    if (instrumentation == null) {
63      // Assume android.support.test.runner.InstrumentationRegistry is valid
64      init(InstrumentationRegistry.getInstrumentation(), InstrumentationRegistry.getArguments());
65    }
66  }
67
68  public static Instrumentation getInstrumentation() {
69    checkInitialized();
70    return instrumentation;
71  }
72
73  public static Context getTargetContext() {
74    return getInstrumentation().getTargetContext();
75  }
76
77  /**
78   * Gets the <a href=
79   * "http://developer.android.com/tools/testing/testing_otheride.html#AMOptionsSyntax" >am
80   * instrument options</a>.
81   */
82  public static Bundle getOptions() {
83    checkInitialized();
84    return options;
85  }
86
87  /** Gets the string value associated with the given key. */
88  public static String getOption(String key) {
89    return getOptions().getString(key);
90  }
91
92  /**
93   * Calls {@link #getOption} with "dd." prefixed to {@code key}. This is for DroidDriver
94   * implementation to use a consistent pattern for its options.
95   */
96  public static String getD2Option(String key) {
97    return getOption("dd." + key);
98  }
99
100  /**
101   * Tries to wait for an idle state on the main thread on best-effort basis up to {@code
102   * timeoutMillis}. The main thread may not enter the idle state when animation is playing, for
103   * example, the ProgressBar.
104   */
105  public static boolean tryWaitForIdleSync(long timeoutMillis) {
106    checkNotMainThread();
107    FutureTask<Void> emptyTask = new FutureTask<Void>(EMPTY_RUNNABLE, null);
108    getInstrumentation().waitForIdle(emptyTask);
109
110    try {
111      emptyTask.get(timeoutMillis, TimeUnit.MILLISECONDS);
112    } catch (java.util.concurrent.TimeoutException e) {
113      Logs.log(
114          Log.INFO,
115          "Timed out after " + timeoutMillis + " milliseconds waiting for idle on main looper");
116      return false;
117    } catch (Throwable t) {
118      throw DroidDriverException.propagate(t);
119    }
120    return true;
121  }
122
123  public static void runOnMainSyncWithTimeout(final Runnable runnable) {
124    runOnMainSyncWithTimeout(
125        new Callable<Void>() {
126          @Override
127          public Void call() throws Exception {
128            runnable.run();
129            return null;
130          }
131        });
132  }
133
134  /**
135   * Runs {@code callable} on the main thread on best-effort basis up to a time limit, which
136   * defaults to {@code 10000L} and can be set as an am instrument option under the key {@code
137   * dd.runOnMainSyncTimeout}.
138   *
139   * <p>This is a safer variation of {@link Instrumentation#runOnMainSync} because the latter may
140   * hang. You may turn off this behavior by setting {@code "-e dd.runOnMainSyncTimeout 0"} on the
141   * am command line.The {@code callable} may never run, for example, if the main Looper has exited
142   * due to uncaught exception.
143   */
144  public static <V> V runOnMainSyncWithTimeout(Callable<V> callable) {
145    checkNotMainThread();
146    final RunOnMainSyncFutureTask<V> futureTask = new RunOnMainSyncFutureTask<>(callable);
147
148    if (runOnMainSyncTimeoutMillis <= 0L) {
149      // Call runOnMainSync on current thread without time limit.
150      futureTask.runOnMainSyncNoThrow();
151    } else {
152      RUN_ON_MAIN_SYNC_EXECUTOR.execute(
153          new Runnable() {
154            @Override
155            public void run() {
156              futureTask.runOnMainSyncNoThrow();
157            }
158          });
159    }
160
161    try {
162      return futureTask.get(runOnMainSyncTimeoutMillis, TimeUnit.MILLISECONDS);
163    } catch (java.util.concurrent.TimeoutException e) {
164      throw new TimeoutException(
165          "Timed out after "
166              + runOnMainSyncTimeoutMillis
167              + " milliseconds waiting for Instrumentation.runOnMainSync",
168          e);
169    } catch (Throwable t) {
170      throw DroidDriverException.propagate(t);
171    } finally {
172      futureTask.cancel(false);
173    }
174  }
175
176  public static void checkMainThread() {
177    if (Looper.myLooper() != Looper.getMainLooper()) {
178      throw new DroidDriverException("This method must be called on the main thread");
179    }
180  }
181
182  public static void checkNotMainThread() {
183    if (Looper.myLooper() == Looper.getMainLooper()) {
184      throw new DroidDriverException("This method cannot be called on the main thread");
185    }
186  }
187
188  private static class RunOnMainSyncFutureTask<V> extends FutureTask<V> {
189    public RunOnMainSyncFutureTask(Callable<V> callable) {
190      super(callable);
191    }
192
193    public void runOnMainSyncNoThrow() {
194      try {
195        getInstrumentation().runOnMainSync(this);
196      } catch (Throwable e) {
197        setException(e);
198      }
199    }
200  }
201}
202