From 2a824751e74316c1afa9f2a7cc08943a890e82a8 Mon Sep 17 00:00:00 2001
From: Connor McLaughlin <stenzek@gmail.com>
Date: Wed, 14 Oct 2020 14:42:08 +1000
Subject: [PATCH] Android: Improve external controller/add auto-hide touch
 option

---
 .../duckstation/EmulationActivity.java        |  50 +++-
 .../duckstation/EmulationSurfaceView.java     | 229 +++++++++++++-----
 .../app/src/main/res/xml/root_preferences.xml |   6 +
 3 files changed, 216 insertions(+), 69 deletions(-)

diff --git a/android/app/src/main/java/com/github/stenzek/duckstation/EmulationActivity.java b/android/app/src/main/java/com/github/stenzek/duckstation/EmulationActivity.java
index f19503a95..a0130b1aa 100644
--- a/android/app/src/main/java/com/github/stenzek/duckstation/EmulationActivity.java
+++ b/android/app/src/main/java/com/github/stenzek/duckstation/EmulationActivity.java
@@ -8,10 +8,12 @@ import androidx.appcompat.app.ActionBar;
 import androidx.appcompat.app.AlertDialog;
 import androidx.appcompat.app.AppCompatActivity;
 
+import android.content.Context;
 import android.content.DialogInterface;
 import android.content.Intent;
 import android.content.SharedPreferences;
 import android.content.res.Configuration;
+import android.hardware.input.InputManager;
 import android.os.Bundle;
 import android.os.Handler;
 import android.util.AndroidException;
@@ -187,6 +189,7 @@ public class EmulationActivity extends AppCompatActivity implements SurfaceHolde
 
         // Hook up controller input.
         updateControllers();
+        registerInputDeviceListener();
     }
 
     @Override
@@ -209,6 +212,8 @@ public class EmulationActivity extends AppCompatActivity implements SurfaceHolde
             mWasDestroyed = true;
             AndroidHostInterface.getInstance().stopEmulationThread();
         }
+
+        unregisterInputDeviceListener();
     }
 
     @Override
@@ -397,14 +402,15 @@ public class EmulationActivity extends AppCompatActivity implements SurfaceHolde
     public void updateControllers() {
         final String controllerType = getStringSetting("Controller1/Type", "DigitalController");
         final String viewType = getStringSetting("Controller1/TouchscreenControllerView", "digital");
+        final boolean autoHideTouchscreenController = getBooleanSetting("Controller1/AutoHideTouchscreenController", false);
         final FrameLayout activityLayout = findViewById(R.id.frameLayout);
 
         Log.i("EmulationActivity", "Controller type: " + controllerType);
         Log.i("EmulationActivity", "View type: " + viewType);
 
-        mContentView.initControllerKeyMapping(controllerType);
+        final boolean hasAnyControllers = mContentView.initControllerMapping(controllerType);
 
-        if (controllerType == "none" || viewType == "none") {
+        if (controllerType == "none" || viewType == "none" || (hasAnyControllers && autoHideTouchscreenController)) {
             if (mTouchscreenController != null) {
                 activityLayout.removeView(mTouchscreenController);
                 mTouchscreenController = null;
@@ -418,4 +424,44 @@ public class EmulationActivity extends AppCompatActivity implements SurfaceHolde
             mTouchscreenController.init(0, controllerType, viewType);
         }
     }
+
+    private InputManager.InputDeviceListener mInputDeviceListener;
+    private void registerInputDeviceListener() {
+        if (mInputDeviceListener != null)
+            return;
+
+        mInputDeviceListener = new InputManager.InputDeviceListener() {
+            @Override
+            public void onInputDeviceAdded(int i) {
+                Log.i("EmulationActivity", String.format("InputDeviceAdded %d", i));
+                updateControllers();
+            }
+
+            @Override
+            public void onInputDeviceRemoved(int i) {
+                Log.i("EmulationActivity", String.format("InputDeviceRemoved %d", i));
+                updateControllers();
+            }
+
+            @Override
+            public void onInputDeviceChanged(int i) {
+                Log.i("EmulationActivity", String.format("InputDeviceChanged %d", i));
+                updateControllers();
+            }
+        };
+
+        InputManager inputManager = ((InputManager)getSystemService(Context.INPUT_SERVICE));
+        if (inputManager != null)
+            inputManager.registerInputDeviceListener(mInputDeviceListener, null);
+    }
+    private void unregisterInputDeviceListener() {
+        if (mInputDeviceListener == null)
+            return;
+
+        InputManager inputManager = ((InputManager)getSystemService(Context.INPUT_SERVICE));
+        if (inputManager != null)
+            inputManager.unregisterInputDeviceListener(mInputDeviceListener);
+        
+        mInputDeviceListener = null;
+    }
 }
diff --git a/android/app/src/main/java/com/github/stenzek/duckstation/EmulationSurfaceView.java b/android/app/src/main/java/com/github/stenzek/duckstation/EmulationSurfaceView.java
index 7a3c55aac..0d6654ab7 100644
--- a/android/app/src/main/java/com/github/stenzek/duckstation/EmulationSurfaceView.java
+++ b/android/app/src/main/java/com/github/stenzek/duckstation/EmulationSurfaceView.java
@@ -1,6 +1,7 @@
 package com.github.stenzek.duckstation;
 
 import android.content.Context;
+import android.hardware.input.InputManager;
 import android.util.ArrayMap;
 import android.util.AttributeSet;
 import android.util.Log;
@@ -10,6 +11,9 @@ import android.view.KeyEvent;
 import android.view.MotionEvent;
 import android.view.SurfaceView;
 
+import java.util.ArrayList;
+import java.util.List;
+
 public class EmulationSurfaceView extends SurfaceView {
     public EmulationSurfaceView(Context context) {
         super(context);
@@ -33,7 +37,7 @@ public class EmulationSurfaceView extends SurfaceView {
     @Override
     public boolean onKeyDown(int keyCode, KeyEvent event) {
         if (isDPadOrButtonEvent(event) && event.getRepeatCount() == 0 &&
-                handleControllerKey(keyCode, true)) {
+                handleControllerKey(event.getDeviceId(), keyCode, true)) {
             return true;
         }
 
@@ -43,7 +47,7 @@ public class EmulationSurfaceView extends SurfaceView {
     @Override
     public boolean onKeyUp(int keyCode, KeyEvent event) {
         if (isDPadOrButtonEvent(event) && event.getRepeatCount() == 0 &&
-                handleControllerKey(keyCode, false)) {
+                handleControllerKey(event.getDeviceId(), keyCode, false)) {
             return true;
         }
 
@@ -56,58 +60,110 @@ public class EmulationSurfaceView extends SurfaceView {
         if ((source & InputDevice.SOURCE_JOYSTICK) == 0)
             return super.onGenericMotionEvent(event);
 
-        final InputDevice device = event.getDevice();
-        for (int axis : AXISES) {
-            Integer mapping = mControllerAxisMapping.containsKey(axis) ? mControllerAxisMapping.get(axis) : null;
-            Pair<Integer, Integer> buttonMapping = mControllerAxisButtonMapping.containsKey(axis) ? mControllerAxisButtonMapping.get(axis) : null;
-            if (mapping == null && buttonMapping == null)
+        final int deviceId = event.getDeviceId();
+        for (AxisMapping mapping : mControllerAxisMapping) {
+            if (mapping.deviceId != deviceId)
                 continue;
 
-            final float axisValue = event.getAxisValue(axis);
+            final float axisValue = event.getAxisValue(mapping.deviceAxisOrButton);
             float emuValue;
 
-            final InputDevice.MotionRange range = device.getMotionRange(axis, source);
-            if (range != null) {
-                final float transformedValue = (axisValue - range.getMin()) / range.getRange();
+            if (mapping.deviceMotionRange != null) {
+                final float transformedValue = (axisValue - mapping.deviceMotionRange.getMin()) / mapping.deviceMotionRange.getRange();
                 emuValue = (transformedValue * 2.0f) - 1.0f;
             } else {
                 emuValue = axisValue;
             }
-            Log.d("EmulationSurfaceView", String.format("axis %d value %f emuvalue %f", axis, axisValue, emuValue));
-            if (mapping != null) {
-                AndroidHostInterface.getInstance().setControllerAxisState(0, mapping, emuValue);
-            } else {
-                final float DEAD_ZONE = 0.25f;
-                AndroidHostInterface.getInstance().setControllerButtonState(0, buttonMapping.first, (emuValue <= -DEAD_ZONE));
-                AndroidHostInterface.getInstance().setControllerButtonState(0, buttonMapping.second, (emuValue >= DEAD_ZONE));
-                Log.d("EmulationSurfaceView", String.format("using emuValue %f for buttons %d %d", emuValue, buttonMapping.first, buttonMapping.second));
+            Log.d("EmulationSurfaceView", String.format("axis %d value %f emuvalue %f", mapping.deviceAxisOrButton, axisValue, emuValue));
+
+            if (mapping.axisMapping >= 0) {
+                AndroidHostInterface.getInstance().setControllerAxisState(0, mapping.axisMapping, emuValue);
+            }
+
+            final float DEAD_ZONE = 0.25f;
+            if (mapping.negativeButton >= 0) {
+                AndroidHostInterface.getInstance().setControllerButtonState(0, mapping.negativeButton, (emuValue <= -DEAD_ZONE));
+            }
+            if (mapping.positiveButton >= 0) {
+                AndroidHostInterface.getInstance().setControllerButtonState(0, mapping.positiveButton, (emuValue >= DEAD_ZONE));
             }
         }
 
         return true;
     }
 
-    private ArrayMap<Integer, Integer> mControllerKeyMapping;
-    private ArrayMap<Integer, Integer> mControllerAxisMapping;
-    private ArrayMap<Integer, Pair<Integer, Integer>> mControllerAxisButtonMapping;
-    static final int[] AXISES = new int[]{MotionEvent.AXIS_X, MotionEvent.AXIS_Y, MotionEvent.AXIS_Z,
-            MotionEvent.AXIS_RZ, MotionEvent.AXIS_LTRIGGER, MotionEvent.AXIS_RTRIGGER,
-            MotionEvent.AXIS_HAT_X, MotionEvent.AXIS_HAT_Y};
+    private class ButtonMapping {
+        public ButtonMapping(int deviceId, int deviceButton, int controllerIndex, int button) {
+            this.deviceId = deviceId;
+            this.deviceAxisOrButton = deviceButton;
+            this.controllerIndex = controllerIndex;
+            this.buttonMapping = button;
+        }
 
-    private void addControllerKeyMapping(int keyCode, String controllerType, String buttonName) {
+        public int deviceId;
+        public int deviceAxisOrButton;
+        public int controllerIndex;
+        public int buttonMapping;
+    }
+
+    private class AxisMapping {
+        public AxisMapping(int deviceId, int deviceAxis, InputDevice.MotionRange motionRange, int controllerIndex, int axis) {
+            this.deviceId = deviceId;
+            this.deviceAxisOrButton = deviceAxis;
+            this.deviceMotionRange = motionRange;
+            this.controllerIndex = controllerIndex;
+            this.axisMapping = axis;
+            this.positiveButton = -1;
+            this.negativeButton = -1;
+        }
+
+        public AxisMapping(int deviceId, int deviceAxis, InputDevice.MotionRange motionRange, int controllerIndex, int positiveButton, int negativeButton) {
+            this.deviceId = deviceId;
+            this.deviceAxisOrButton = deviceAxis;
+            this.deviceMotionRange = motionRange;
+            this.controllerIndex = controllerIndex;
+            this.axisMapping = -1;
+            this.positiveButton = positiveButton;
+            this.negativeButton = negativeButton;
+        }
+
+        public int deviceId;
+        public int deviceAxisOrButton;
+        public InputDevice.MotionRange deviceMotionRange;
+        public int controllerIndex;
+        public int axisMapping;
+        public int positiveButton;
+        public int negativeButton;
+    }
+
+    private ArrayList<ButtonMapping> mControllerKeyMapping;
+    private ArrayList<AxisMapping> mControllerAxisMapping;
+
+    private void addControllerKeyMapping(int deviceId, int keyCode, int controllerIndex, String controllerType, String buttonName) {
         int mapping = AndroidHostInterface.getControllerButtonCode(controllerType, buttonName);
         Log.i("EmulationSurfaceView", String.format("Map %d to %d (%s)", keyCode, mapping,
                 buttonName));
-        if (mapping >= 0)
-            mControllerKeyMapping.put(keyCode, mapping);
+        if (mapping >= 0) {
+            mControllerKeyMapping.add(new ButtonMapping(deviceId, keyCode, controllerIndex, mapping));
+        }
     }
 
-    private void addControllerAxisMapping(int axis, String controllerType, String axisName, String negativeButtonName, String positiveButtonName) {
+    private void addControllerAxisMapping(int deviceId, List<InputDevice.MotionRange> motionRanges, int axis, int controllerIndex, String controllerType, String axisName, String negativeButtonName, String positiveButtonName) {
+        InputDevice.MotionRange range = null;
+        for (InputDevice.MotionRange curRange : motionRanges) {
+            if (curRange.getAxis() == axis) {
+                range = curRange;
+                break;
+            }
+        }
+        if (range == null)
+            return;
+
         if (axisName != null) {
             int mapping = AndroidHostInterface.getControllerAxisCode(controllerType, axisName);
             Log.i("EmulationSurfaceView", String.format("Map axis %d to %d (%s)", axis, mapping, axisName));
             if (mapping >= 0) {
-                mControllerAxisMapping.put(axis, mapping);
+                mControllerAxisMapping.add(new AxisMapping(deviceId, axis, range, controllerIndex, mapping));
                 return;
             }
         }
@@ -118,48 +174,87 @@ public class EmulationSurfaceView extends SurfaceView {
             Log.i("EmulationSurfaceView", String.format("Map axis %d to %d %d (button %s %s)", axis, negativeMapping, positiveMapping,
                     negativeButtonName, positiveButtonName));
             if (negativeMapping >= 0 && positiveMapping >= 0) {
-                mControllerAxisButtonMapping.put(axis, new Pair<Integer, Integer>(negativeMapping, positiveMapping));
+                mControllerAxisMapping.add(new AxisMapping(deviceId, axis, range, controllerIndex, positiveMapping, negativeMapping));
             }
         }
     }
 
-    public void initControllerKeyMapping(String controllerType) {
-        mControllerKeyMapping = new ArrayMap<>();
-        mControllerAxisMapping = new ArrayMap<>();
-        mControllerAxisButtonMapping = new ArrayMap<>();
-
-        // TODO: Don't hardcode...
-        addControllerKeyMapping(KeyEvent.KEYCODE_DPAD_UP, controllerType, "Up");
-        addControllerKeyMapping(KeyEvent.KEYCODE_DPAD_RIGHT, controllerType, "Right");
-        addControllerKeyMapping(KeyEvent.KEYCODE_DPAD_DOWN, controllerType, "Down");
-        addControllerKeyMapping(KeyEvent.KEYCODE_DPAD_LEFT, controllerType, "Left");
-        addControllerKeyMapping(KeyEvent.KEYCODE_BUTTON_L1, controllerType, "L1");
-        addControllerKeyMapping(KeyEvent.KEYCODE_BUTTON_L2, controllerType, "L2");
-        addControllerKeyMapping(KeyEvent.KEYCODE_BUTTON_SELECT, controllerType, "Select");
-        addControllerKeyMapping(KeyEvent.KEYCODE_BUTTON_START, controllerType, "Start");
-        addControllerKeyMapping(KeyEvent.KEYCODE_BUTTON_Y, controllerType, "Triangle");
-        addControllerKeyMapping(KeyEvent.KEYCODE_BUTTON_B, controllerType, "Circle");
-        addControllerKeyMapping(KeyEvent.KEYCODE_BUTTON_A, controllerType, "Cross");
-        addControllerKeyMapping(KeyEvent.KEYCODE_BUTTON_X, controllerType, "Square");
-        addControllerKeyMapping(KeyEvent.KEYCODE_BUTTON_R1, controllerType, "R1");
-        addControllerKeyMapping(KeyEvent.KEYCODE_BUTTON_R2, controllerType, "R2");
-        addControllerAxisMapping(MotionEvent.AXIS_X, controllerType, "LeftX", null, null);
-        addControllerAxisMapping(MotionEvent.AXIS_Y, controllerType, "LeftY", null, null);
-        addControllerAxisMapping(MotionEvent.AXIS_Z, controllerType, "RightX", null, null);
-        addControllerAxisMapping(MotionEvent.AXIS_RZ, controllerType, "RightY", null, null);
-        addControllerAxisMapping(MotionEvent.AXIS_LTRIGGER, controllerType, "L2", "L2", "L2");
-        addControllerAxisMapping(MotionEvent.AXIS_RTRIGGER, controllerType, "R2", "R2", "R2");
-        addControllerAxisMapping(MotionEvent.AXIS_HAT_X, controllerType, null, "Left", "Right");
-        addControllerAxisMapping(MotionEvent.AXIS_HAT_Y, controllerType, null, "Up", "Down");
-    }
-
-    private boolean handleControllerKey(int keyCode, boolean pressed) {
-        if (!mControllerKeyMapping.containsKey(keyCode))
+    private static boolean isJoystickDevice(int deviceId) {
+        if (deviceId < 0)
             return false;
 
-        final int mapping = mControllerKeyMapping.get(keyCode);
-        AndroidHostInterface.getInstance().setControllerButtonState(0, mapping, pressed);
-        Log.d("EmulationSurfaceView", String.format("handleControllerKey %d -> %d %d", keyCode, mapping, pressed ? 1 : 0));
-        return true;
+        final InputDevice dev = InputDevice.getDevice(deviceId);
+        if (dev == null)
+            return false;
+
+        final int sources = dev.getSources();
+        if ((sources & InputDevice.SOURCE_CLASS_JOYSTICK) != 0)
+            return true;
+
+        if ((sources & InputDevice.SOURCE_GAMEPAD) == InputDevice.SOURCE_GAMEPAD)
+            return true;
+
+        if ((sources & InputDevice.SOURCE_DPAD) == InputDevice.SOURCE_DPAD)
+            return true;
+
+        return false;
+    }
+
+    public boolean initControllerMapping(String controllerType) {
+        mControllerKeyMapping = new ArrayList<>();
+        mControllerAxisMapping = new ArrayList<>();
+
+        final int[] deviceIds = InputDevice.getDeviceIds();
+        for (int deviceId : deviceIds) {
+            if (!isJoystickDevice(deviceId))
+                continue;
+
+            InputDevice device = InputDevice.getDevice(deviceId);
+            List<InputDevice.MotionRange> motionRanges = device.getMotionRanges();
+            int controllerIndex = 0;
+
+            addControllerKeyMapping(deviceId, KeyEvent.KEYCODE_DPAD_UP, controllerIndex, controllerType, "Up");
+            addControllerKeyMapping(deviceId, KeyEvent.KEYCODE_DPAD_RIGHT, controllerIndex, controllerType, "Right");
+            addControllerKeyMapping(deviceId, KeyEvent.KEYCODE_DPAD_DOWN, controllerIndex, controllerType, "Down");
+            addControllerKeyMapping(deviceId, KeyEvent.KEYCODE_DPAD_LEFT, controllerIndex, controllerType, "Left");
+            addControllerKeyMapping(deviceId, KeyEvent.KEYCODE_BUTTON_L1, controllerIndex, controllerType, "L1");
+            addControllerKeyMapping(deviceId, KeyEvent.KEYCODE_BUTTON_L2, controllerIndex, controllerType, "L2");
+            addControllerKeyMapping(deviceId, KeyEvent.KEYCODE_BUTTON_SELECT, controllerIndex, controllerType, "Select");
+            addControllerKeyMapping(deviceId, KeyEvent.KEYCODE_BUTTON_START, controllerIndex, controllerType, "Start");
+            addControllerKeyMapping(deviceId, KeyEvent.KEYCODE_BUTTON_Y, controllerIndex, controllerType, "Triangle");
+            addControllerKeyMapping(deviceId, KeyEvent.KEYCODE_BUTTON_B, controllerIndex, controllerType, "Circle");
+            addControllerKeyMapping(deviceId, KeyEvent.KEYCODE_BUTTON_A, controllerIndex, controllerType, "Cross");
+            addControllerKeyMapping(deviceId, KeyEvent.KEYCODE_BUTTON_X, controllerIndex, controllerType, "Square");
+            addControllerKeyMapping(deviceId, KeyEvent.KEYCODE_BUTTON_R1, controllerIndex, controllerType, "R1");
+            addControllerKeyMapping(deviceId, KeyEvent.KEYCODE_BUTTON_R2, controllerIndex, controllerType, "R2");
+            if (motionRanges != null) {
+                addControllerAxisMapping(deviceId, motionRanges, MotionEvent.AXIS_X, controllerIndex, controllerType, "LeftX", null, null);
+                addControllerAxisMapping(deviceId, motionRanges, MotionEvent.AXIS_Y, controllerIndex, controllerType, "LeftY", null, null);
+                addControllerAxisMapping(deviceId, motionRanges, MotionEvent.AXIS_RX, controllerIndex, controllerType, "RightX", null, null);
+                addControllerAxisMapping(deviceId, motionRanges, MotionEvent.AXIS_RY, controllerIndex, controllerType, "RightY", null, null);
+                addControllerAxisMapping(deviceId, motionRanges, MotionEvent.AXIS_Z, controllerIndex, controllerType, "RightX", null, null);
+                addControllerAxisMapping(deviceId, motionRanges, MotionEvent.AXIS_RZ, controllerIndex, controllerType, "RightY", null, null);
+                addControllerAxisMapping(deviceId, motionRanges, MotionEvent.AXIS_LTRIGGER, controllerIndex, controllerType, "L2", "L2", "L2");
+                addControllerAxisMapping(deviceId, motionRanges, MotionEvent.AXIS_RTRIGGER, controllerIndex, controllerType, "R2", "R2", "R2");
+                addControllerAxisMapping(deviceId, motionRanges, MotionEvent.AXIS_HAT_X, controllerIndex, controllerType, null, "Left", "Right");
+                addControllerAxisMapping(deviceId, motionRanges, MotionEvent.AXIS_HAT_Y, controllerIndex, controllerType, null, "Up", "Down");
+            }
+        }
+
+        return !mControllerKeyMapping.isEmpty() || !mControllerKeyMapping.isEmpty();
+    }
+
+    private boolean handleControllerKey(int deviceId, int keyCode, boolean pressed) {
+        boolean result = false;
+        for (ButtonMapping mapping : mControllerKeyMapping) {
+            if (mapping.deviceId != deviceId || mapping.deviceAxisOrButton != keyCode)
+                continue;
+
+            AndroidHostInterface.getInstance().setControllerButtonState(0, mapping.buttonMapping, pressed);
+            Log.d("EmulationSurfaceView", String.format("handleControllerKey %d -> %d %d", keyCode, mapping.buttonMapping, pressed ? 1 : 0));
+            result = true;
+        }
+
+        return result;
     }
 }
diff --git a/android/app/src/main/res/xml/root_preferences.xml b/android/app/src/main/res/xml/root_preferences.xml
index 0433c2a62..699e488d2 100644
--- a/android/app/src/main/res/xml/root_preferences.xml
+++ b/android/app/src/main/res/xml/root_preferences.xml
@@ -266,6 +266,12 @@
             app:defaultValue="digital"
             app:useSimpleSummaryProvider="true"
             app:iconSpaceReserved="false" />
+        <SwitchPreferenceCompat
+            app:key="Controller1/AutoHideTouchscreenController"
+            app:title="Auto-Hide Touchscreen Controller"
+            app:defaultValue="false"
+            app:summary="Hides the touchscreen controller when an external controller is detected."
+            app:iconSpaceReserved="false"/>
     </PreferenceCategory>
 
     <PreferenceCategory app:title="Memory Cards" app:iconSpaceReserved="false">