1 package org.robolectric.simulator; 2 3 import android.app.UiAutomation; 4 import android.os.Handler; 5 import android.os.Looper; 6 import android.os.SystemClock; 7 import android.view.KeyEvent; 8 import android.view.MotionEvent; 9 import androidx.test.platform.app.InstrumentationRegistry; 10 import java.awt.event.MouseAdapter; 11 import java.awt.event.MouseEvent; 12 import java.time.Duration; 13 import java.time.Instant; 14 import javax.swing.JMenuItem; 15 import javax.swing.JPopupMenu; 16 import javax.swing.SwingUtilities; 17 import org.robolectric.shadows.ShadowUiAutomation; 18 import org.robolectric.simulator.pluginapi.MenuCustomizer; 19 import org.robolectric.util.inject.Injector; 20 21 /** A {@link MouseAdapter} that triggers Android {@link MotionEvent}s when the mouse is pressed. */ 22 public class MouseHandler extends MouseAdapter { 23 private final UiAutomation uiAutomation = 24 InstrumentationRegistry.getInstrumentation().getUiAutomation(); 25 26 private boolean isPressed; 27 private Duration androidSystemClockTimeDelta; 28 private Instant downTime; 29 30 private final Handler handler = new Handler(Looper.getMainLooper()); 31 32 private final JPopupMenu rightClickMenu; 33 MouseHandler()34 public MouseHandler() { 35 rightClickMenu = new JPopupMenu(); 36 rightClickMenu.add(getBackMenuItem()); 37 38 // Allow plugins to customize the right click menu. 39 Injector injector = new Injector.Builder(Looper.class.getClassLoader()).build(); 40 MenuCustomizer menuCustomizer = injector.getInstance(MenuCustomizer.class); 41 menuCustomizer.customizePopupMenu(rightClickMenu); 42 } 43 getBackMenuItem()44 private JMenuItem getBackMenuItem() { 45 JMenuItem sendBackMenuItem = new JMenuItem("Press back"); 46 sendBackMenuItem.addActionListener( 47 e -> { 48 handler.post( 49 () -> { 50 long eventTime = SystemClock.uptimeMillis(); 51 KeyEvent backKeyDown = 52 new KeyEvent( 53 eventTime, 54 eventTime, 55 KeyEvent.ACTION_DOWN, 56 KeyEvent.KEYCODE_BACK, 57 /* repeat= */ 0, 58 0); 59 ShadowUiAutomation.injectInputEvent(backKeyDown); 60 61 KeyEvent backKeyUp = 62 new KeyEvent( 63 eventTime, 64 eventTime, 65 KeyEvent.ACTION_UP, 66 KeyEvent.KEYCODE_BACK, 67 /* repeat= */ 0, 68 0); 69 ShadowUiAutomation.injectInputEvent(backKeyUp); 70 }); 71 }); 72 73 return sendBackMenuItem; 74 } 75 76 @Override mousePressed(MouseEvent mouseEvent)77 public void mousePressed(MouseEvent mouseEvent) { 78 if (shouldHandle(mouseEvent)) { 79 isPressed = true; 80 androidSystemClockTimeDelta = 81 Duration.ofMillis(SystemClock.uptimeMillis()).minus(Duration.ofNanos(System.nanoTime())); 82 downTime = Instant.ofEpochMilli(mouseEvent.getWhen()); 83 postMotionEvent(mouseEvent, MotionEvent.ACTION_DOWN); 84 } 85 } 86 87 @Override mouseDragged(MouseEvent mouseEvent)88 public void mouseDragged(MouseEvent mouseEvent) { 89 if (isPressed) { 90 postMotionEvent(mouseEvent, MotionEvent.ACTION_MOVE); 91 } 92 } 93 94 @Override mouseReleased(MouseEvent mouseEvent)95 public void mouseReleased(MouseEvent mouseEvent) { 96 if (shouldHandle(mouseEvent)) { 97 isPressed = false; 98 postMotionEvent(mouseEvent, MotionEvent.ACTION_UP); 99 } else if (SwingUtilities.isRightMouseButton(mouseEvent)) { 100 rightClickMenu.show(mouseEvent.getComponent(), mouseEvent.getX(), mouseEvent.getY()); 101 } 102 } 103 shouldHandle(MouseEvent mouseEvent)104 private boolean shouldHandle(MouseEvent mouseEvent) { 105 return !mouseEvent.isPopupTrigger() && SwingUtilities.isLeftMouseButton(mouseEvent); 106 } 107 postMotionEvent(MouseEvent mouseEvent, int action)108 private void postMotionEvent(MouseEvent mouseEvent, int action) { 109 MotionEvent androidEvent = obtainMotionEvent(mouseEvent, action); 110 handler.post(() -> uiAutomation.injectInputEvent(androidEvent, true)); 111 mouseEvent.consume(); 112 } 113 obtainMotionEvent(MouseEvent mouseEvent, int action)114 private MotionEvent obtainMotionEvent(MouseEvent mouseEvent, int action) { 115 return MotionEvent.obtain( 116 toAndroidTime(downTime), 117 toAndroidTime(Instant.ofEpochMilli(mouseEvent.getWhen())), 118 action, 119 mouseEvent.getX(), 120 mouseEvent.getY(), 121 /* metaState= */ 0); 122 } 123 toAndroidTime(Instant instant)124 private long toAndroidTime(Instant instant) { 125 return instant.minus(androidSystemClockTimeDelta).toEpochMilli(); 126 } 127 } 128