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.util.Log;
24 
25 import java.util.concurrent.Callable;
26 import java.util.concurrent.Executor;
27 import java.util.concurrent.Executors;
28 import java.util.concurrent.FutureTask;
29 import java.util.concurrent.TimeUnit;
30 
31 import io.appium.droiddriver.exceptions.DroidDriverException;
32 import io.appium.droiddriver.exceptions.TimeoutException;
33 import io.appium.droiddriver.exceptions.UnrecoverableException;
34 
35 /**
36  * Static utility methods pertaining to {@link Instrumentation}.
37  */
38 public 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    */
init(Instrumentation instrumentation, Bundle arguments)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 
checkInitialized()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 
getInstrumentation()72   public static Instrumentation getInstrumentation() {
73     checkInitialized();
74     return instrumentation;
75   }
76 
getTargetContext()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    */
getOptions()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    */
getOption(String key)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    */
getD2Option(String key)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    */
tryWaitForIdleSync(long timeoutMillis)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 
runOnMainSyncWithTimeout(final Runnable runnable)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    */
runOnMainSyncWithTimeout(Callable<V> callable)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> {
RunOnMainSyncFutureTask(Callable<V> callable)176     public RunOnMainSyncFutureTask(Callable<V> callable) {
177       super(callable);
178     }
179 
runOnMainSyncNoThrow()180     public void runOnMainSyncNoThrow() {
181       try {
182         getInstrumentation().runOnMainSync(this);
183       } catch (Throwable e) {
184         setException(e);
185       }
186     }
187   }
188 
validateNotAppThread()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