Android: Improve external controller/add auto-hide touch option

This commit is contained in:
Connor McLaughlin 2020-10-14 14:42:08 +10:00
parent 3b6b7007b3
commit 2a824751e7
3 changed files with 216 additions and 69 deletions

View File

@ -8,10 +8,12 @@ import androidx.appcompat.app.ActionBar;
import androidx.appcompat.app.AlertDialog; import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity; import androidx.appcompat.app.AppCompatActivity;
import android.content.Context;
import android.content.DialogInterface; import android.content.DialogInterface;
import android.content.Intent; import android.content.Intent;
import android.content.SharedPreferences; import android.content.SharedPreferences;
import android.content.res.Configuration; import android.content.res.Configuration;
import android.hardware.input.InputManager;
import android.os.Bundle; import android.os.Bundle;
import android.os.Handler; import android.os.Handler;
import android.util.AndroidException; import android.util.AndroidException;
@ -187,6 +189,7 @@ public class EmulationActivity extends AppCompatActivity implements SurfaceHolde
// Hook up controller input. // Hook up controller input.
updateControllers(); updateControllers();
registerInputDeviceListener();
} }
@Override @Override
@ -209,6 +212,8 @@ public class EmulationActivity extends AppCompatActivity implements SurfaceHolde
mWasDestroyed = true; mWasDestroyed = true;
AndroidHostInterface.getInstance().stopEmulationThread(); AndroidHostInterface.getInstance().stopEmulationThread();
} }
unregisterInputDeviceListener();
} }
@Override @Override
@ -397,14 +402,15 @@ public class EmulationActivity extends AppCompatActivity implements SurfaceHolde
public void updateControllers() { public void updateControllers() {
final String controllerType = getStringSetting("Controller1/Type", "DigitalController"); final String controllerType = getStringSetting("Controller1/Type", "DigitalController");
final String viewType = getStringSetting("Controller1/TouchscreenControllerView", "digital"); final String viewType = getStringSetting("Controller1/TouchscreenControllerView", "digital");
final boolean autoHideTouchscreenController = getBooleanSetting("Controller1/AutoHideTouchscreenController", false);
final FrameLayout activityLayout = findViewById(R.id.frameLayout); final FrameLayout activityLayout = findViewById(R.id.frameLayout);
Log.i("EmulationActivity", "Controller type: " + controllerType); Log.i("EmulationActivity", "Controller type: " + controllerType);
Log.i("EmulationActivity", "View type: " + viewType); 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) { if (mTouchscreenController != null) {
activityLayout.removeView(mTouchscreenController); activityLayout.removeView(mTouchscreenController);
mTouchscreenController = null; mTouchscreenController = null;
@ -418,4 +424,44 @@ public class EmulationActivity extends AppCompatActivity implements SurfaceHolde
mTouchscreenController.init(0, controllerType, viewType); 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;
}
} }

View File

@ -1,6 +1,7 @@
package com.github.stenzek.duckstation; package com.github.stenzek.duckstation;
import android.content.Context; import android.content.Context;
import android.hardware.input.InputManager;
import android.util.ArrayMap; import android.util.ArrayMap;
import android.util.AttributeSet; import android.util.AttributeSet;
import android.util.Log; import android.util.Log;
@ -10,6 +11,9 @@ import android.view.KeyEvent;
import android.view.MotionEvent; import android.view.MotionEvent;
import android.view.SurfaceView; import android.view.SurfaceView;
import java.util.ArrayList;
import java.util.List;
public class EmulationSurfaceView extends SurfaceView { public class EmulationSurfaceView extends SurfaceView {
public EmulationSurfaceView(Context context) { public EmulationSurfaceView(Context context) {
super(context); super(context);
@ -33,7 +37,7 @@ public class EmulationSurfaceView extends SurfaceView {
@Override @Override
public boolean onKeyDown(int keyCode, KeyEvent event) { public boolean onKeyDown(int keyCode, KeyEvent event) {
if (isDPadOrButtonEvent(event) && event.getRepeatCount() == 0 && if (isDPadOrButtonEvent(event) && event.getRepeatCount() == 0 &&
handleControllerKey(keyCode, true)) { handleControllerKey(event.getDeviceId(), keyCode, true)) {
return true; return true;
} }
@ -43,7 +47,7 @@ public class EmulationSurfaceView extends SurfaceView {
@Override @Override
public boolean onKeyUp(int keyCode, KeyEvent event) { public boolean onKeyUp(int keyCode, KeyEvent event) {
if (isDPadOrButtonEvent(event) && event.getRepeatCount() == 0 && if (isDPadOrButtonEvent(event) && event.getRepeatCount() == 0 &&
handleControllerKey(keyCode, false)) { handleControllerKey(event.getDeviceId(), keyCode, false)) {
return true; return true;
} }
@ -56,58 +60,110 @@ public class EmulationSurfaceView extends SurfaceView {
if ((source & InputDevice.SOURCE_JOYSTICK) == 0) if ((source & InputDevice.SOURCE_JOYSTICK) == 0)
return super.onGenericMotionEvent(event); return super.onGenericMotionEvent(event);
final InputDevice device = event.getDevice(); final int deviceId = event.getDeviceId();
for (int axis : AXISES) { for (AxisMapping mapping : mControllerAxisMapping) {
Integer mapping = mControllerAxisMapping.containsKey(axis) ? mControllerAxisMapping.get(axis) : null; if (mapping.deviceId != deviceId)
Pair<Integer, Integer> buttonMapping = mControllerAxisButtonMapping.containsKey(axis) ? mControllerAxisButtonMapping.get(axis) : null;
if (mapping == null && buttonMapping == null)
continue; continue;
final float axisValue = event.getAxisValue(axis); final float axisValue = event.getAxisValue(mapping.deviceAxisOrButton);
float emuValue; float emuValue;
final InputDevice.MotionRange range = device.getMotionRange(axis, source); if (mapping.deviceMotionRange != null) {
if (range != null) { final float transformedValue = (axisValue - mapping.deviceMotionRange.getMin()) / mapping.deviceMotionRange.getRange();
final float transformedValue = (axisValue - range.getMin()) / range.getRange();
emuValue = (transformedValue * 2.0f) - 1.0f; emuValue = (transformedValue * 2.0f) - 1.0f;
} else { } else {
emuValue = axisValue; emuValue = axisValue;
} }
Log.d("EmulationSurfaceView", String.format("axis %d value %f emuvalue %f", axis, axisValue, emuValue)); Log.d("EmulationSurfaceView", String.format("axis %d value %f emuvalue %f", mapping.deviceAxisOrButton, axisValue, emuValue));
if (mapping != null) {
AndroidHostInterface.getInstance().setControllerAxisState(0, mapping, emuValue); if (mapping.axisMapping >= 0) {
} else { AndroidHostInterface.getInstance().setControllerAxisState(0, mapping.axisMapping, emuValue);
}
final float DEAD_ZONE = 0.25f; final float DEAD_ZONE = 0.25f;
AndroidHostInterface.getInstance().setControllerButtonState(0, buttonMapping.first, (emuValue <= -DEAD_ZONE)); if (mapping.negativeButton >= 0) {
AndroidHostInterface.getInstance().setControllerButtonState(0, buttonMapping.second, (emuValue >= DEAD_ZONE)); AndroidHostInterface.getInstance().setControllerButtonState(0, mapping.negativeButton, (emuValue <= -DEAD_ZONE));
Log.d("EmulationSurfaceView", String.format("using emuValue %f for buttons %d %d", emuValue, buttonMapping.first, buttonMapping.second)); }
if (mapping.positiveButton >= 0) {
AndroidHostInterface.getInstance().setControllerButtonState(0, mapping.positiveButton, (emuValue >= DEAD_ZONE));
} }
} }
return true; return true;
} }
private ArrayMap<Integer, Integer> mControllerKeyMapping; private class ButtonMapping {
private ArrayMap<Integer, Integer> mControllerAxisMapping; public ButtonMapping(int deviceId, int deviceButton, int controllerIndex, int button) {
private ArrayMap<Integer, Pair<Integer, Integer>> mControllerAxisButtonMapping; this.deviceId = deviceId;
static final int[] AXISES = new int[]{MotionEvent.AXIS_X, MotionEvent.AXIS_Y, MotionEvent.AXIS_Z, this.deviceAxisOrButton = deviceButton;
MotionEvent.AXIS_RZ, MotionEvent.AXIS_LTRIGGER, MotionEvent.AXIS_RTRIGGER, this.controllerIndex = controllerIndex;
MotionEvent.AXIS_HAT_X, MotionEvent.AXIS_HAT_Y}; 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); int mapping = AndroidHostInterface.getControllerButtonCode(controllerType, buttonName);
Log.i("EmulationSurfaceView", String.format("Map %d to %d (%s)", keyCode, mapping, Log.i("EmulationSurfaceView", String.format("Map %d to %d (%s)", keyCode, mapping,
buttonName)); buttonName));
if (mapping >= 0) if (mapping >= 0) {
mControllerKeyMapping.put(keyCode, mapping); 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) { if (axisName != null) {
int mapping = AndroidHostInterface.getControllerAxisCode(controllerType, axisName); int mapping = AndroidHostInterface.getControllerAxisCode(controllerType, axisName);
Log.i("EmulationSurfaceView", String.format("Map axis %d to %d (%s)", axis, mapping, axisName)); Log.i("EmulationSurfaceView", String.format("Map axis %d to %d (%s)", axis, mapping, axisName));
if (mapping >= 0) { if (mapping >= 0) {
mControllerAxisMapping.put(axis, mapping); mControllerAxisMapping.add(new AxisMapping(deviceId, axis, range, controllerIndex, mapping));
return; 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, Log.i("EmulationSurfaceView", String.format("Map axis %d to %d %d (button %s %s)", axis, negativeMapping, positiveMapping,
negativeButtonName, positiveButtonName)); negativeButtonName, positiveButtonName));
if (negativeMapping >= 0 && positiveMapping >= 0) { 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) { private static boolean isJoystickDevice(int deviceId) {
mControllerKeyMapping = new ArrayMap<>(); if (deviceId < 0)
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))
return false; return false;
final int mapping = mControllerKeyMapping.get(keyCode); final InputDevice dev = InputDevice.getDevice(deviceId);
AndroidHostInterface.getInstance().setControllerButtonState(0, mapping, pressed); if (dev == null)
Log.d("EmulationSurfaceView", String.format("handleControllerKey %d -> %d %d", keyCode, mapping, pressed ? 1 : 0)); return false;
final int sources = dev.getSources();
if ((sources & InputDevice.SOURCE_CLASS_JOYSTICK) != 0)
return true; 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;
} }
} }

View File

@ -266,6 +266,12 @@
app:defaultValue="digital" app:defaultValue="digital"
app:useSimpleSummaryProvider="true" app:useSimpleSummaryProvider="true"
app:iconSpaceReserved="false" /> 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>
<PreferenceCategory app:title="Memory Cards" app:iconSpaceReserved="false"> <PreferenceCategory app:title="Memory Cards" app:iconSpaceReserved="false">