Home | History | Annotate | Download | only in util
      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    */
     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