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