• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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
injectMotionEvent(MotionEvent event)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
injectKeyEvent(KeyEvent event)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
injectString(String str)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")
getKeyCharacterMap()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
loopMainThreadUntilIdle()140   public void loopMainThreadUntilIdle() {
141     shadowOf(Looper.getMainLooper()).idle();
142   }
143 
144   @Override
loopMainThreadForAtLeast(long millisDelay)145   public void loopMainThreadForAtLeast(long millisDelay) {
146     shadowOf(Looper.getMainLooper()).idle(millisDelay, TimeUnit.MILLISECONDS);
147   }
148 
getViewRoots()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 
getViewRootsContainer()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