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