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.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    */
     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 
     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 
     72   public static Instrumentation getInstrumentation() {
     73     checkInitialized();
     74     return instrumentation;
     75   }
     76 
     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    */
     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    */
     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    */
    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    */
    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 
    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    */
    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> {
    176     public RunOnMainSyncFutureTask(Callable<V> callable) {
    177       super(callable);
    178     }
    179 
    180     public void runOnMainSyncNoThrow() {
    181       try {
    182         getInstrumentation().runOnMainSync(this);
    183       } catch (Throwable e) {
    184         setException(e);
    185       }
    186     }
    187   }
    188 
    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