Home | History | Annotate | Download | only in internal
      1 package org.robolectric.android.internal;
      2 
      3 import static com.google.common.base.Preconditions.checkNotNull;
      4 import static com.google.common.base.Preconditions.checkState;
      5 import static com.google.common.collect.Iterables.getOnlyElement;
      6 import static org.robolectric.Shadows.shadowOf;
      7 
      8 import android.annotation.SuppressLint;
      9 import android.os.Build;
     10 import android.os.Build.VERSION_CODES;
     11 import android.os.Looper;
     12 import android.os.SystemClock;
     13 import android.util.Log;
     14 import android.view.KeyCharacterMap;
     15 import android.view.KeyEvent;
     16 import android.view.MotionEvent;
     17 import android.view.ViewRootImpl;
     18 import android.view.WindowManagerGlobal;
     19 import android.view.WindowManagerImpl;
     20 import androidx.test.platform.ui.InjectEventSecurityException;
     21 import androidx.test.platform.ui.UiController;
     22 import com.google.common.annotations.VisibleForTesting;
     23 import java.util.Arrays;
     24 import java.util.List;
     25 import java.util.concurrent.TimeUnit;
     26 import org.robolectric.RuntimeEnvironment;
     27 import org.robolectric.util.ReflectionHelpers;
     28 
     29 /** A {@link UiController} that runs on a local JVM with Robolectric. */
     30 public class LocalUiController implements UiController {
     31 
     32   private static final String TAG = "LocalUiController";
     33 
     34   @Override
     35   public boolean injectMotionEvent(MotionEvent event) throws InjectEventSecurityException {
     36     checkNotNull(event);
     37     checkState(Looper.myLooper() == Looper.getMainLooper(), "Expecting to be on main thread!");
     38     loopMainThreadUntilIdle();
     39 
     40     // TODO: temporarily restrict to one view root for now
     41     getOnlyElement(getViewRoots()).getView().dispatchTouchEvent(event);
     42 
     43     loopMainThreadUntilIdle();
     44 
     45     return true;
     46   }
     47 
     48   @Override
     49   public boolean injectKeyEvent(KeyEvent event) throws InjectEventSecurityException {
     50     checkNotNull(event);
     51     checkState(Looper.myLooper() == Looper.getMainLooper(), "Expecting to be on main thread!");
     52 
     53     loopMainThreadUntilIdle();
     54     // TODO: temporarily restrict to one view root for now
     55     getOnlyElement(getViewRoots()).getView().dispatchKeyEvent(event);
     56 
     57     loopMainThreadUntilIdle();
     58     return true;
     59   }
     60 
     61   // TODO(b/80130000): implementation copied from espresso's UIControllerImpl. Refactor code into common location
     62   @Override
     63   public boolean injectString(String str) throws InjectEventSecurityException {
     64     checkNotNull(str);
     65     checkState(Looper.myLooper() == Looper.getMainLooper(), "Expecting to be on main thread!");
     66 
     67     // No-op if string is empty.
     68     if (str.isEmpty()) {
     69       Log.w(TAG, "Supplied string is empty resulting in no-op (nothing is typed).");
     70       return true;
     71     }
     72 
     73     boolean eventInjected = false;
     74     KeyCharacterMap keyCharacterMap = getKeyCharacterMap();
     75 
     76     // TODO(b/80130875): Investigate why not use (as suggested in javadoc of
     77     // keyCharacterMap.getEvents):
     78     // http://developer.android.com/reference/android/view/KeyEvent.html#KeyEvent(long,
     79     // java.lang.String, int, int)
     80     KeyEvent[] events = keyCharacterMap.getEvents(str.toCharArray());
     81     if (events == null) {
     82       throw new RuntimeException(
     83           String.format(
     84               "Failed to get key events for string %s (i.e. current IME does not understand how to"
     85                   + " translate the string into key events). As a workaround, you can use"
     86                   + " replaceText action to set the text directly in the EditText field.",
     87               str));
     88     }
     89 
     90     Log.d(TAG, String.format("Injecting string: \"%s\"", str));
     91 
     92     for (KeyEvent event : events) {
     93       checkNotNull(
     94           event,
     95           String.format(
     96               "Failed to get event for character (%c) with key code (%s)",
     97               event.getKeyCode(), event.getUnicodeChar()));
     98 
     99       eventInjected = false;
    100       for (int attempts = 0; !eventInjected && attempts < 4; attempts++) {
    101         // We have to change the time of an event before injecting it because
    102         // all KeyEvents returned by KeyCharacterMap.getEvents() have the same
    103         // time stamp and the system rejects too old events. Hence, it is
    104         // possible for an event to become stale before it is injected if it
    105         // takes too long to inject the preceding ones.
    106         event = KeyEvent.changeTimeRepeat(event, SystemClock.uptimeMillis(), 0);
    107         eventInjected = injectKeyEvent(event);
    108       }
    109 
    110       if (!eventInjected) {
    111         Log.e(
    112             TAG,
    113             String.format(
    114                 "Failed to inject event for character (%c) with key code (%s)",
    115                 event.getUnicodeChar(), event.getKeyCode()));
    116         break;
    117       }
    118     }
    119 
    120     return eventInjected;
    121   }
    122 
    123   @SuppressLint("InlinedApi")
    124   @VisibleForTesting
    125   @SuppressWarnings("deprecation")
    126   static KeyCharacterMap getKeyCharacterMap() {
    127     KeyCharacterMap keyCharacterMap = null;
    128 
    129     // KeyCharacterMap.VIRTUAL_KEYBOARD is present from API11.
    130     // For earlier APIs we use KeyCharacterMap.BUILT_IN_KEYBOARD
    131     if (Build.VERSION.SDK_INT < 11) {
    132       keyCharacterMap = KeyCharacterMap.load(KeyCharacterMap.BUILT_IN_KEYBOARD);
    133     } else {
    134       keyCharacterMap = KeyCharacterMap.load(KeyCharacterMap.VIRTUAL_KEYBOARD);
    135     }
    136     return keyCharacterMap;
    137   }
    138 
    139   @Override
    140   public void loopMainThreadUntilIdle() {
    141     shadowOf(Looper.getMainLooper()).idle();
    142   }
    143 
    144   @Override
    145   public void loopMainThreadForAtLeast(long millisDelay) {
    146     shadowOf(Looper.getMainLooper()).idle(millisDelay, TimeUnit.MILLISECONDS);
    147   }
    148 
    149   private static List<ViewRootImpl> getViewRoots() {
    150     Object windowManager = getViewRootsContainer();
    151     Object viewRootsObj = ReflectionHelpers.getField(windowManager, "mRoots");
    152     Class<?> viewRootsClass = viewRootsObj.getClass();
    153     if (ViewRootImpl[].class.isAssignableFrom(viewRootsClass)) {
    154       return Arrays.asList((ViewRootImpl[]) viewRootsObj);
    155     } else if (List.class.isAssignableFrom(viewRootsClass)) {
    156       return (List<ViewRootImpl>) viewRootsObj;
    157     } else {
    158       throw new IllegalStateException(
    159           "WindowManager.mRoots is an unknown type " + viewRootsClass.getName());
    160     }
    161   }
    162 
    163   private static Object getViewRootsContainer() {
    164     if (RuntimeEnvironment.getApiLevel() <= VERSION_CODES.JELLY_BEAN) {
    165       return ReflectionHelpers.callStaticMethod(WindowManagerImpl.class, "getDefault");
    166     } else {
    167       return WindowManagerGlobal.getInstance();
    168     }
    169   }
    170 }
    171