• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
17 package io.appium.droiddriver.util;
18 
19 import android.app.Instrumentation;
20 import android.content.Context;
21 import android.os.Bundle;
22 import android.os.Looper;
23 import android.support.test.InstrumentationRegistry;
24 import android.util.Log;
25 import io.appium.droiddriver.exceptions.DroidDriverException;
26 import io.appium.droiddriver.exceptions.TimeoutException;
27 import java.util.concurrent.Callable;
28 import java.util.concurrent.Executor;
29 import java.util.concurrent.Executors;
30 import java.util.concurrent.FutureTask;
31 import java.util.concurrent.TimeUnit;
32 
33 /** Static utility methods pertaining to {@link Instrumentation}. */
34 public 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    */
init(Instrumentation instrumentation, Bundle arguments)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 
checkInitialized()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 
getInstrumentation()68   public static Instrumentation getInstrumentation() {
69     checkInitialized();
70     return instrumentation;
71   }
72 
getTargetContext()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    */
getOptions()82   public static Bundle getOptions() {
83     checkInitialized();
84     return options;
85   }
86 
87   /** Gets the string value associated with the given key. */
getOption(String 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    */
getD2Option(String key)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    */
tryWaitForIdleSync(long timeoutMillis)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 
runOnMainSyncWithTimeout(final Runnable runnable)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    */
runOnMainSyncWithTimeout(Callable<V> callable)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 
checkMainThread()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 
checkNotMainThread()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> {
RunOnMainSyncFutureTask(Callable<V> callable)189     public RunOnMainSyncFutureTask(Callable<V> callable) {
190       super(callable);
191     }
192 
runOnMainSyncNoThrow()193     public void runOnMainSyncNoThrow() {
194       try {
195         getInstrumentation().runOnMainSync(this);
196       } catch (Throwable e) {
197         setException(e);
198       }
199     }
200   }
201 }
202