SDL: Refactor Android input handling and add pen support

From e2323c136744ce5fe60800fb359257b268bde4a5 Mon Sep 17 00:00:00 2001
From: hwsmm <[EMAIL REDACTED]>
Date: Tue, 12 Nov 2024 23:04:40 +0900
Subject: [PATCH] Refactor Android input handling and add pen support

---
 .../main/java/org/libsdl/app/SDLActivity.java |   6 +-
 .../org/libsdl/app/SDLControllerManager.java  | 183 ++++++-----------
 .../main/java/org/libsdl/app/SDLSurface.java  | 193 ++++++++----------
 src/video/android/SDL_androidpen.c            |  22 +-
 4 files changed, 163 insertions(+), 241 deletions(-)

diff --git a/android-project/app/src/main/java/org/libsdl/app/SDLActivity.java b/android-project/app/src/main/java/org/libsdl/app/SDLActivity.java
index a1edff7f407de..6067fa74dcafc 100644
--- a/android-project/app/src/main/java/org/libsdl/app/SDLActivity.java
+++ b/android-project/app/src/main/java/org/libsdl/app/SDLActivity.java
@@ -223,7 +223,7 @@ public enum NativeState {
     protected static SDLClipboardHandler mClipboardHandler;
     protected static Hashtable<Integer, PointerIcon> mCursors;
     protected static int mLastCursorID;
-    protected static SDLGenericMotionListener_API12 mMotionListener;
+    protected static SDLGenericMotionListener_API14 mMotionListener;
     protected static HIDDeviceManager mHIDDeviceManager;
 
     // This is what SDL runs in. It invokes SDL_main(), eventually
@@ -232,14 +232,14 @@ public enum NativeState {
     protected static boolean mActivityCreated = false;
     private static SDLFileDialogState mFileDialogState = null;
 
-    protected static SDLGenericMotionListener_API12 getMotionListener() {
+    protected static SDLGenericMotionListener_API14 getMotionListener() {
         if (mMotionListener == null) {
             if (Build.VERSION.SDK_INT >= 26 /* Android 8.0 (O) */) {
                 mMotionListener = new SDLGenericMotionListener_API26();
             } else if (Build.VERSION.SDK_INT >= 24 /* Android 7.0 (N) */) {
                 mMotionListener = new SDLGenericMotionListener_API24();
             } else {
-                mMotionListener = new SDLGenericMotionListener_API12();
+                mMotionListener = new SDLGenericMotionListener_API14();
             }
         }
 
diff --git a/android-project/app/src/main/java/org/libsdl/app/SDLControllerManager.java b/android-project/app/src/main/java/org/libsdl/app/SDLControllerManager.java
index b7faee89972e3..dc6fb9d12a6ce 100644
--- a/android-project/app/src/main/java/org/libsdl/app/SDLControllerManager.java
+++ b/android-project/app/src/main/java/org/libsdl/app/SDLControllerManager.java
@@ -662,44 +662,61 @@ protected SDLHaptic getHaptic(int device_id) {
     }
 }
 
-class SDLGenericMotionListener_API12 implements View.OnGenericMotionListener {
+class SDLGenericMotionListener_API14 implements View.OnGenericMotionListener {
     // Generic Motion (mouse hover, joystick...) events go here
     @Override
     public boolean onGenericMotion(View v, MotionEvent event) {
+        if (event.getSource() == InputDevice.SOURCE_JOYSTICK)
+            return SDLControllerManager.handleJoystickMotionEvent(event);
+
         float x, y;
-        int action;
+        int action = event.getActionMasked();
+        int pointerCount = event.getPointerCount();
+        boolean consumed = false;
 
-        switch ( event.getSource() ) {
-            case InputDevice.SOURCE_JOYSTICK:
-                return SDLControllerManager.handleJoystickMotionEvent(event);
+        for (int i = 0; i < pointerCount; i++) {
+            int toolType = event.getToolType(i);
 
-            case InputDevice.SOURCE_MOUSE:
-                action = event.getActionMasked();
+            if (toolType == MotionEvent.TOOL_TYPE_MOUSE) {
                 switch (action) {
                     case MotionEvent.ACTION_SCROLL:
-                        x = event.getAxisValue(MotionEvent.AXIS_HSCROLL, 0);
-                        y = event.getAxisValue(MotionEvent.AXIS_VSCROLL, 0);
+                        x = event.getAxisValue(MotionEvent.AXIS_HSCROLL, i);
+                        y = event.getAxisValue(MotionEvent.AXIS_VSCROLL, i);
                         SDLActivity.onNativeMouse(0, action, x, y, false);
-                        return true;
+                        consumed = true;
+                        break;
 
                     case MotionEvent.ACTION_HOVER_MOVE:
-                        x = event.getX(0);
-                        y = event.getY(0);
+                        x = getEventX(event, i);
+                        y = getEventY(event, i);
 
-                        SDLActivity.onNativeMouse(0, action, x, y, false);
-                        return true;
+                        SDLActivity.onNativeMouse(0, action, x, y, checkRelativeEvent(event));
+                        consumed = true;
+                        break;
 
                     default:
                         break;
                 }
-                break;
+            } else if (toolType == MotionEvent.TOOL_TYPE_STYLUS || toolType == MotionEvent.TOOL_TYPE_ERASER) {
+                switch (action) {
+                    case MotionEvent.ACTION_HOVER_ENTER:
+                    case MotionEvent.ACTION_HOVER_MOVE:
+                    case MotionEvent.ACTION_HOVER_EXIT:
+                        x = event.getX(i);
+                        y = event.getY(i);
+                        float p = event.getPressure(i);
+
+                        // BUTTON_STYLUS_PRIMARY is 2^5, so shift by 4
+                        int buttons = event.getButtonState() >> 4;
 
-            default:
-                break;
+                        SDLActivity.onNativePen(event.getPointerId(i), buttons, action, x, y, p);
+                        consumed = true;
+                        break;
+                }
+            }
         }
 
-        // Event was not managed
-        return false;
+        return consumed;
     }
 
     public boolean supportsRelativeMouse() {
@@ -714,46 +731,29 @@ public boolean setRelativeMouseEnabled(boolean enabled) {
         return false;
     }
 
-    public void reclaimRelativeMouseModeIfNeeded()
-    {
+    public void reclaimRelativeMouseModeIfNeeded() {
+
+    }
 
+    public boolean checkRelativeEvent(MotionEvent event) {
+        return inRelativeMode();
     }
 
-    public float getEventX(MotionEvent event) {
-        return event.getX(0);
+    public float getEventX(MotionEvent event, int pointerIndex) {
+        return event.getX(pointerIndex);
     }
 
-    public float getEventY(MotionEvent event) {
-        return event.getY(0);
+    public float getEventY(MotionEvent event, int pointerIndex) {
+        return event.getY(pointerIndex);
     }
 
 }
 
-class SDLGenericMotionListener_API24 extends SDLGenericMotionListener_API12 {
+class SDLGenericMotionListener_API24 extends SDLGenericMotionListener_API14 {
     // Generic Motion (mouse hover, joystick...) events go here
 
     private boolean mRelativeModeEnabled;
 
-    @Override
-    public boolean onGenericMotion(View v, MotionEvent event) {
-
-        // Handle relative mouse mode
-        if (mRelativeModeEnabled) {
-            if (event.getSource() == InputDevice.SOURCE_MOUSE) {
-                int action = event.getActionMasked();
-                if (action == MotionEvent.ACTION_HOVER_MOVE) {
-                    float x = event.getAxisValue(MotionEvent.AXIS_RELATIVE_X);
-                    float y = event.getAxisValue(MotionEvent.AXIS_RELATIVE_Y);
-                    SDLActivity.onNativeMouse(0, action, x, y, true);
-                    return true;
-                }
-            }
-        }
-
-        // Event was not managed, call SDLGenericMotionListener_API12 method
-        return super.onGenericMotion(v, event);
-    }
-
     @Override
     public boolean supportsRelativeMouse() {
         return true;
@@ -771,20 +771,20 @@ public boolean setRelativeMouseEnabled(boolean enabled) {
     }
 
     @Override
-    public float getEventX(MotionEvent event) {
-        if (mRelativeModeEnabled) {
-            return event.getAxisValue(MotionEvent.AXIS_RELATIVE_X);
+    public float getEventX(MotionEvent event, int pointerIndex) {
+        if (mRelativeModeEnabled && event.getToolType(pointerIndex) == MotionEvent.TOOL_TYPE_MOUSE) {
+            return event.getAxisValue(MotionEvent.AXIS_RELATIVE_X, pointerIndex);
         } else {
-            return event.getX(0);
+            return event.getX(pointerIndex);
         }
     }
 
     @Override
-    public float getEventY(MotionEvent event) {
-        if (mRelativeModeEnabled) {
-            return event.getAxisValue(MotionEvent.AXIS_RELATIVE_Y);
+    public float getEventY(MotionEvent event, int pointerIndex) {
+        if (mRelativeModeEnabled && event.getToolType(pointerIndex) == MotionEvent.TOOL_TYPE_MOUSE) {
+            return event.getAxisValue(MotionEvent.AXIS_RELATIVE_Y, pointerIndex);
         } else {
-            return event.getY(0);
+            return event.getY(pointerIndex);
         }
     }
 }
@@ -793,65 +793,6 @@ class SDLGenericMotionListener_API26 extends SDLGenericMotionListener_API24 {
     // Generic Motion (mouse hover, joystick...) events go here
     private boolean mRelativeModeEnabled;
 
-    @Override
-    public boolean onGenericMotion(View v, MotionEvent event) {
-        float x, y;
-        int action;
-
-        switch ( event.getSource() ) {
-            case InputDevice.SOURCE_JOYSTICK:
-                return SDLControllerManager.handleJoystickMotionEvent(event);
-
-            case InputDevice.SOURCE_MOUSE:
-            // DeX desktop mouse cursor is a separate non-standard input type.
-            case InputDevice.SOURCE_MOUSE | InputDevice.SOURCE_TOUCHSCREEN:
-                action = event.getActionMasked();
-                switch (action) {
-                    case MotionEvent.ACTION_SCROLL:
-                        x = event.getAxisValue(MotionEvent.AXIS_HSCROLL, 0);
-                        y = event.getAxisValue(MotionEvent.AXIS_VSCROLL, 0);
-                        SDLActivity.onNativeMouse(0, action, x, y, false);
-                        return true;
-
-                    case MotionEvent.ACTION_HOVER_MOVE:
-                        x = event.getX(0);
-                        y = event.getY(0);
-                        SDLActivity.onNativeMouse(0, action, x, y, false);
-                        return true;
-
-                    default:
-                        break;
-                }
-                break;
-
-            case InputDevice.SOURCE_MOUSE_RELATIVE:
-                action = event.getActionMasked();
-                switch (action) {
-                    case MotionEvent.ACTION_SCROLL:
-                        x = event.getAxisValue(MotionEvent.AXIS_HSCROLL, 0);
-                        y = event.getAxisValue(MotionEvent.AXIS_VSCROLL, 0);
-                        SDLActivity.onNativeMouse(0, action, x, y, false);
-                        return true;
-
-                    case MotionEvent.ACTION_HOVER_MOVE:
-                        x = event.getX(0);
-                        y = event.getY(0);
-                        SDLActivity.onNativeMouse(0, action, x, y, true);
-                        return true;
-
-                    default:
-                        break;
-                }
-                break;
-
-            default:
-                break;
-        }
-
-        // Event was not managed
-        return false;
-    }
-
     @Override
     public boolean supportsRelativeMouse() {
         return (!SDLActivity.isDeXMode() || Build.VERSION.SDK_INT >= 27 /* Android 8.1 (O_MR1) */);
@@ -878,22 +819,26 @@ public boolean setRelativeMouseEnabled(boolean enabled) {
     }
 
     @Override
-    public void reclaimRelativeMouseModeIfNeeded()
-    {
+    public void reclaimRelativeMouseModeIfNeeded() {
         if (mRelativeModeEnabled && !SDLActivity.isDeXMode()) {
             SDLActivity.getContentView().requestPointerCapture();
         }
     }
 
     @Override
-    public float getEventX(MotionEvent event) {
+    public boolean checkRelativeEvent(MotionEvent event) {
+        return event.getSource() == InputDevice.SOURCE_MOUSE_RELATIVE;
+    }
+
+    @Override
+    public float getEventX(MotionEvent event, int pointerIndex) {
         // Relative mouse in capture mode will only have relative for X/Y
-        return event.getX(0);
+        return event.getX(pointerIndex);
     }
 
     @Override
-    public float getEventY(MotionEvent event) {
+    public float getEventY(MotionEvent event, int pointerIndex) {
         // Relative mouse in capture mode will only have relative for X/Y
-        return event.getY(0);
+        return event.getY(pointerIndex);
     }
 }
diff --git a/android-project/app/src/main/java/org/libsdl/app/SDLSurface.java b/android-project/app/src/main/java/org/libsdl/app/SDLSurface.java
index d896f6ce4a663..6b5f4346622db 100644
--- a/android-project/app/src/main/java/org/libsdl/app/SDLSurface.java
+++ b/android-project/app/src/main/java/org/libsdl/app/SDLSurface.java
@@ -239,93 +239,65 @@ public boolean onTouch(View v, MotionEvent event) {
         int touchDevId = event.getDeviceId();
         final int pointerCount = event.getPointerCount();
         int action = event.getActionMasked();
-        int pointerFingerId;
-        int i = -1;
+        int pointerId;
+        int i = 0;
         float x,y,p;
 
-        // 12290 = Samsung DeX mode desktop mouse
-        // 12290 = 0x3002 = 0x2002 | 0x1002 = SOURCE_MOUSE | SOURCE_TOUCHSCREEN
-        // 0x2   = SOURCE_CLASS_POINTER
-        if (event.getSource() == InputDevice.SOURCE_MOUSE || event.getSource() == (InputDevice.SOURCE_MOUSE | InputDevice.SOURCE_TOUCHSCREEN)) {
-            int mouseButton = 1;
-            try {
-                Object object = event.getClass().getMethod("getButtonState").invoke(event);
-                if (object != null) {
-                    mouseButton = (Integer) object;
+        if (action == MotionEvent.ACTION_POINTER_UP || action == MotionEvent.ACTION_POINTER_DOWN)
+            i = event.getActionIndex();
+
+        do {
+            int toolType = event.getToolType(i);
+
+            if (toolType == MotionEvent.TOOL_TYPE_MOUSE) {
+                int buttonState = event.getButtonState();
+                boolean relative = false;
+
+                // We need to check if we're in relative mouse mode and get the axis offset rather than the x/y values
+                // if we are. We'll leverage our existing mouse motion listener
+                SDLGenericMotionListener_API14 motionListener = SDLActivity.getMotionListener();
+                x = motionListener.getEventX(event, i);
+                y = motionListener.getEventY(event, i);
+                relative = motionListener.inRelativeMode();
+
+                SDLActivity.onNativeMouse(buttonState, action, x, y, relative);
+            } else if (toolType == MotionEvent.TOOL_TYPE_STYLUS || toolType == MotionEvent.TOOL_TYPE_ERASER) {
+                pointerId = event.getPointerId(i);
+                x = event.getX(i);
+                y = event.getY(i);
+                p = event.getPressure(i);
+                if (p > 1.0f) {
+                    // may be larger than 1.0f on some devices
+                    // see the documentation of getPressure(i)
+                    p = 1.0f;
                 }
-            } catch(Exception ignored) {
-            }
-
-            // We need to check if we're in relative mouse mode and get the axis offset rather than the x/y values
-            // if we are.  We'll leverage our existing mouse motion listener
-            SDLGenericMotionListener_API12 motionListener = SDLActivity.getMotionListener();
-            x = motionListener.getEventX(event);
-            y = motionListener.getEventY(event);
-
-            SDLActivity.onNativeMouse(mouseButton, action, x, y, motionListener.inRelativeMode());
-        } else {
-            switch(action) {
-                case MotionEvent.ACTION_MOVE:
-                    for (i = 0; i < pointerCount; i++) {
-                        pointerFingerId = event.getPointerId(i);
-                        x = getNormalizedX(event.getX(i));
-                        y = getNormalizedY(event.getY(i));
-                        p = event.getPressure(i);
-                        if (p > 1.0f) {
-                            // may be larger than 1.0f on some devices
-                            // see the documentation of getPressure(i)
-                            p = 1.0f;
-                        }
-                        SDLActivity.onNativeTouch(touchDevId, pointerFingerId, action, x, y, p);
-                    }
-                    break;
 
-                case MotionEvent.ACTION_UP:
-                case MotionEvent.ACTION_DOWN:
-                    // Primary pointer up/down, the index is always zero
-                    i = 0;
-                    /* fallthrough */
-                case MotionEvent.ACTION_POINTER_UP:
-                case MotionEvent.ACTION_POINTER_DOWN:
-                    // Non primary pointer up/down
-                    if (i == -1) {
-                        i = event.getActionIndex();
-                    }
-
-                    pointerFingerId = event.getPointerId(i);
-                    x = getNormalizedX(event.getX(i));
-                    y = getNormalizedY(event.getY(i));
-                    p = event.getPressure(i);
-                    if (p > 1.0f) {
-                        // may be larger than 1.0f on some devices
-                        // see the documentation of getPressure(i)
-                        p = 1.0f;
-                    }
-                    SDLActivity.onNativeTouch(touchDevId, pointerFingerId, action, x, y, p);
-                    break;
-
-                case MotionEvent.ACTION_CANCEL:
-                    for (i = 0; i < pointerCount; i++) {
-                        pointerFingerId = event.getPointerId(i);
-                        x = getNormalizedX(event.getX(i));
-                        y = getNormalizedY(event.getY(i));
-                        p = event.getPressure(i);
-                        if (p > 1.0f) {
-                            // may be larger than 1.0f on some devices
-                            // see the documentation of getPressure(i)
-                            p = 1.0f;
-                        }
-                        SDLActivity.onNativeTouch(touchDevId, pointerFingerId, MotionEvent.ACTION_UP, x, y, p);
-                    }
-                    break;
+                // BUTTON_STYLUS_PRIMARY is 2^5, so shift by 4, and apply SDL_PEN_INPUT_DOWN/SDL_PEN_INPUT_ERASER_TIP
+                int buttonState = (event.getButtonState() >> 4) | (1 << (toolType == MotionEvent.TOOL_TYPE_STYLUS ? 0 : 30));
+
+                SDLActivity.onNativePen(pointerId, buttonState, action, x, y, p);
+            } else if (toolType == MotionEvent.TOOL_TYPE_FINGER) {
+                pointerId = event.getPointerId(i);
+                x = getNormalizedX(event.getX(i));
+                y = getNormalizedY(event.getY(i));
+                p = event.getPressure(i);
+                if (p > 1.0f) {
+                    // may be larger than 1.0f on some devices
+                    // see the documentation of getPressure(i)
+                    p = 1.0f;
+                }
 
-                default:
-                    break;
+                SDLActivity.onNativeTouch(touchDevId, pointerId,
+                        action == MotionEvent.ACTION_CANCEL ? MotionEvent.ACTION_UP : action, x, y, p);
             }
-        }
+
+            // Non-primary up/down
+            if (action == MotionEvent.ACTION_POINTER_UP || action == MotionEvent.ACTION_POINTER_DOWN)
+                break;
+        } while (++i < pointerCount);
 
         return true;
-   }
+    }
 
     // Sensor events
     public void enableSensor(int sensortype, boolean enabled) {
@@ -395,39 +367,42 @@ public void onSensorChanged(SensorEvent event) {
     public boolean onCapturedPointerEvent(MotionEvent event)
     {
         int action = event.getActionMasked();
+        int pointerCount = event.getPointerCount();
 
-        float x, y;
-        switch (action) {
-            case MotionEvent.ACTION_SCROLL:
-                x = event.getAxisValue(MotionEvent.AXIS_HSCROLL, 0);
-                y = event.getAxisValue(MotionEvent.AXIS_VSCROLL, 0);
-                SDLActivity.onNativeMouse(0, action, x, y, false);
-                return true;
-
-            case MotionEvent.ACTION_HOVER_MOVE:
-            case MotionEvent.ACTION_MOVE:
-                x = event.getX(0);
-                y = event.getY(0);
-                SDLActivity.onNativeMouse(0, action, x, y, true);
-                return true;
-
-            case MotionEvent.ACTION_BUTTON_PRESS:
-            case MotionEvent.ACTION_BUTTON_RELEASE:
-
-                // Change our action value to what SDL's code expects.
-                if (action == MotionEvent.ACTION_BUTTON_PRESS) {
-                    action = MotionEvent.ACTION_DOWN;
-                } else { /* MotionEvent.ACTION_BUTTON_RELEASE */
-                    action = MotionEvent.ACTION_UP;
-                }
+        for (int i = 0; i < pointerCount; i++) {
+            float x, y;
+            switch (action) {
+                case MotionEvent.ACTION_SCROLL:
+                    x = event.getAxisValue(MotionEvent.AXIS_HSCROLL, i);
+                    y = event.getAxisValue(MotionEvent.AXIS_VSCROLL, i);
+                    SDLActivity.onNativeMouse(0, action, x, y, false);
+                    return true;
+
+                case MotionEvent.ACTION_HOVER_MOVE:
+                case MotionEvent.ACTION_MOVE:
+                    x = event.getX(i);
+                    y = event.getY(i);
+                    SDLActivity.onNativeMouse(0, action, x, y, true);
+                    return true;
+
+                case MotionEvent.ACTION_BUTTON_PRESS:
+                case MotionEvent.ACTION_BUTTON_RELEASE:
+
+                    // Change our action value to what SDL's code expects.
+                    if (action == MotionEvent.ACTION_BUTTON_PRESS) {
+                        action = MotionEvent.ACTION_DOWN;
+                    } else { /* MotionEvent.ACTION_BUTTON_RELEASE */
+                        action = MotionEvent.ACTION_UP;
+                    }
 
-                x = event.getX(0);
-                y = event.getY(0);
-                int button = event.getButtonState();
+                    x = event.getX(i);
+                    y = event.getY(i);
+                    int button = event.getButtonState();
 
-                SDLActivity.onNativeMouse(button, action, x, y, true);
-                return true;
-        }
+                    SDLActivity.onNativeMouse(button, action, x, y, true);
+                    return true;
+            }
+        }      
 
         return false;
     }
diff --git a/src/video/android/SDL_androidpen.c b/src/video/android/SDL_androidpen.c
index 65730bd75dce3..a691b2e146ba3 100644
--- a/src/video/android/SDL_androidpen.c
+++ b/src/video/android/SDL_androidpen.c
@@ -26,11 +26,12 @@
 #include "../../events/SDL_pen_c.h"
 #include "../../core/android/SDL_android.h"
 
-#define ACTION_DOWN 0
-#define ACTION_UP   1
-#define ACTION_POINTER_DOWN   5
-#define ACTION_POINTER_UP     6
-#define ACTION_HOVER_EXIT     10
+#define ACTION_DOWN   0
+#define ACTION_UP     1
+#define ACTION_CANCEL 3
+#define ACTION_POINTER_DOWN 5
+#define ACTION_POINTER_UP   6
+#define ACTION_HOVER_EXIT   10
 
 void Android_OnPen(SDL_Window *window, int pen_id_in, int button, int action, float x, float y, float p)
 {
@@ -56,11 +57,6 @@ void Android_OnPen(SDL_Window *window, int pen_id_in, int button, int action, fl
         }
     }
 
-    if (action == ACTION_HOVER_EXIT) {
-        SDL_RemovePenDevice(0, pen);
-        return;
-    }
-
     SDL_SendPenMotion(0, pen, window, x, y);
     SDL_SendPenAxis(0, pen, window, SDL_PEN_AXIS_PRESSURE, p);
     // TODO: add more axis
@@ -77,7 +73,13 @@ void Android_OnPen(SDL_Window *window, int pen_id_in, int button, int action, fl
     }
 
     // button contains DOWN/ERASER_TIP on DOWN/UP regardless of pressed state, use action to distinguish
+    // we don't compare tip flags above because MotionEvent.getButtonState doesn't return stylus tip/eraser state.
     switch (action) {
+    case ACTION_CANCEL:
+    case ACTION_HOVER_EXIT:
+        SDL_RemovePenDevice(0, pen);
+        break;
+
     case ACTION_DOWN:
     case ACTION_POINTER_DOWN:
         SDL_SendPenTouch(0, pen, window, (button & SDL_PEN_INPUT_ERASER_TIP) != 0, true);