mirror of
https://github.com/WinampDesktop/winamp.git
synced 2025-06-19 19:35:41 -04:00
Android: Rewrite input binding to be more flexible
Supports vibration, non-gamepad controllers, etc. You will need to rebind your controllers.
This commit is contained in:
@ -82,10 +82,14 @@ public class AndroidHostInterface {
|
||||
|
||||
public static native String[] getControllerAxisNames(String controllerType);
|
||||
|
||||
public static native int getControllerVibrationMotorCount(String controllerType);
|
||||
|
||||
public native void handleControllerButtonEvent(int controllerIndex, int buttonIndex, boolean pressed);
|
||||
|
||||
public native void handleControllerAxisEvent(int controllerIndex, int axisIndex, float value);
|
||||
|
||||
public native boolean hasControllerButtonBinding(int controllerIndex, int buttonIndex);
|
||||
|
||||
public native void toggleControllerAnalogMode();
|
||||
|
||||
public native String[] getInputProfileNames();
|
||||
@ -115,6 +119,7 @@ public class AndroidHostInterface {
|
||||
public native void saveResumeState(boolean waitForCompletion);
|
||||
|
||||
public native void applySettings();
|
||||
public native void updateInputMap();
|
||||
|
||||
public native void setDisplayAlignment(int alignment);
|
||||
|
||||
|
@ -1,13 +1,11 @@
|
||||
package com.github.stenzek.duckstation;
|
||||
|
||||
import android.app.AlertDialog;
|
||||
import android.app.Dialog;
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
import android.content.SharedPreferences;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.util.ArraySet;
|
||||
import android.util.Log;
|
||||
import android.view.InputDevice;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.MotionEvent;
|
||||
@ -15,16 +13,20 @@ import android.view.MotionEvent;
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
|
||||
public class ControllerBindingDialog extends AlertDialog {
|
||||
private boolean mIsAxis;
|
||||
private String mSettingKey;
|
||||
final static float DETECT_THRESHOLD = 0.25f;
|
||||
private final ControllerBindingPreference.Type mType;
|
||||
private final String mSettingKey;
|
||||
private String mCurrentBinding;
|
||||
private int mUpdatedAxisCode = -1;
|
||||
private final HashMap<Integer, float[]> mStartingAxisValues = new HashMap<>();
|
||||
|
||||
public ControllerBindingDialog(Context context, String buttonName, String settingKey, String currentBinding, boolean isAxis) {
|
||||
public ControllerBindingDialog(Context context, String buttonName, String settingKey, String currentBinding, ControllerBindingPreference.Type type) {
|
||||
super(context);
|
||||
|
||||
mIsAxis = isAxis;
|
||||
mType = type;
|
||||
mSettingKey = settingKey;
|
||||
mCurrentBinding = currentBinding;
|
||||
if (mCurrentBinding == null)
|
||||
@ -42,10 +44,7 @@ public class ControllerBindingDialog extends AlertDialog {
|
||||
setOnKeyListener(new DialogInterface.OnKeyListener() {
|
||||
@Override
|
||||
public boolean onKey(DialogInterface dialog, int keyCode, KeyEvent event) {
|
||||
if (onKeyDown(keyCode, event))
|
||||
return true;
|
||||
|
||||
return false;
|
||||
return onKeyDown(keyCode, event);
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -73,57 +72,53 @@ public class ControllerBindingDialog extends AlertDialog {
|
||||
|
||||
@Override
|
||||
public boolean onKeyDown(int keyCode, KeyEvent event) {
|
||||
if (mIsAxis || !EmulationSurfaceView.isDPadOrButtonEvent(event))
|
||||
if (!EmulationSurfaceView.isBindableDevice(event.getDevice()) || !EmulationSurfaceView.isBindableKeyCode(event.getKeyCode())) {
|
||||
return super.onKeyUp(keyCode, event);
|
||||
}
|
||||
|
||||
if (mType == ControllerBindingPreference.Type.BUTTON)
|
||||
mCurrentBinding = String.format("%s/Button%d", event.getDevice().getDescriptor(), event.getKeyCode());
|
||||
else if (mType == ControllerBindingPreference.Type.VIBRATION)
|
||||
mCurrentBinding = event.getDevice().getDescriptor();
|
||||
else
|
||||
return super.onKeyUp(keyCode, event);
|
||||
|
||||
int buttonIndex = EmulationSurfaceView.getButtonIndexForKeyCode(keyCode);
|
||||
if (buttonIndex < 0)
|
||||
return super.onKeyUp(keyCode, event);
|
||||
|
||||
// TODO: Multiple controllers
|
||||
final int controllerIndex = 0;
|
||||
mCurrentBinding = String.format("Controller%d/Button%d", controllerIndex, buttonIndex);
|
||||
updateMessage();
|
||||
updateBinding();
|
||||
dismiss();
|
||||
return true;
|
||||
}
|
||||
|
||||
private int mUpdatedAxisCode = -1;
|
||||
|
||||
private void setAxisCode(int axisCode, boolean positive) {
|
||||
final int axisIndex = EmulationSurfaceView.getAxisIndexForAxisCode(axisCode);
|
||||
if (mUpdatedAxisCode >= 0 || axisIndex < 0)
|
||||
private void setAxisCode(InputDevice device, int axisCode, boolean positive) {
|
||||
if (mUpdatedAxisCode >= 0)
|
||||
return;
|
||||
|
||||
mUpdatedAxisCode = axisCode;
|
||||
|
||||
final int controllerIndex = 0;
|
||||
if (mIsAxis)
|
||||
mCurrentBinding = String.format("Controller%d/Axis%d", controllerIndex, axisIndex);
|
||||
if (mType == ControllerBindingPreference.Type.AXIS)
|
||||
mCurrentBinding = String.format("%s/Axis%d", device.getDescriptor(), axisCode);
|
||||
else
|
||||
mCurrentBinding = String.format("Controller%d/%cAxis%d", controllerIndex, (positive) ? '+' : '-', axisIndex);
|
||||
mCurrentBinding = String.format("%s/%cAxis%d", device.getDescriptor(), (positive) ? '+' : '-', axisCode);
|
||||
|
||||
updateBinding();
|
||||
updateMessage();
|
||||
dismiss();
|
||||
}
|
||||
|
||||
final static float DETECT_THRESHOLD = 0.25f;
|
||||
|
||||
private HashMap<Integer, float[]> mStartingAxisValues = new HashMap<>();
|
||||
|
||||
private boolean doAxisDetection(MotionEvent event) {
|
||||
if ((event.getSource() & (InputDevice.SOURCE_JOYSTICK | InputDevice.SOURCE_GAMEPAD | InputDevice.SOURCE_DPAD)) == 0)
|
||||
if (!EmulationSurfaceView.isBindableDevice(event.getDevice()))
|
||||
return false;
|
||||
|
||||
final int[] axisCodes = EmulationSurfaceView.getKnownAxisCodes();
|
||||
final int deviceId = event.getDeviceId();
|
||||
final List<InputDevice.MotionRange> motionEventList = event.getDevice().getMotionRanges();
|
||||
if (motionEventList == null || motionEventList.isEmpty())
|
||||
return false;
|
||||
|
||||
final int deviceId = event.getDeviceId();
|
||||
if (!mStartingAxisValues.containsKey(deviceId)) {
|
||||
final float[] axisValues = new float[axisCodes.length];
|
||||
for (int axisIndex = 0; axisIndex < axisCodes.length; axisIndex++) {
|
||||
final int axisCode = axisCodes[axisIndex];
|
||||
final float[] axisValues = new float[motionEventList.size()];
|
||||
for (int axisIndex = 0; axisIndex < motionEventList.size(); axisIndex++) {
|
||||
final int axisCode = motionEventList.get(axisIndex).getAxis();
|
||||
|
||||
// these are binary, so start at zero
|
||||
if (axisCode == MotionEvent.AXIS_HAT_X || axisCode == MotionEvent.AXIS_HAT_Y)
|
||||
@ -133,13 +128,15 @@ public class ControllerBindingDialog extends AlertDialog {
|
||||
}
|
||||
|
||||
mStartingAxisValues.put(deviceId, axisValues);
|
||||
return false;
|
||||
}
|
||||
|
||||
final float[] axisValues = mStartingAxisValues.get(deviceId);
|
||||
for (int axisIndex = 0; axisIndex < axisCodes.length; axisIndex++) {
|
||||
final float newValue = event.getAxisValue(axisCodes[axisIndex]);
|
||||
for (int axisIndex = 0; axisIndex < motionEventList.size(); axisIndex++) {
|
||||
final int axisCode = motionEventList.get(axisIndex).getAxis();
|
||||
final float newValue = event.getAxisValue(axisCode);
|
||||
if (Math.abs(newValue - axisValues[axisIndex]) >= DETECT_THRESHOLD) {
|
||||
setAxisCode(axisCodes[axisIndex], newValue >= 0.0f);
|
||||
setAxisCode(event.getDevice(), axisCode, newValue >= 0.0f);
|
||||
break;
|
||||
}
|
||||
}
|
||||
@ -149,6 +146,10 @@ public class ControllerBindingDialog extends AlertDialog {
|
||||
|
||||
@Override
|
||||
public boolean onGenericMotionEvent(@NonNull MotionEvent event) {
|
||||
if (mType != ControllerBindingPreference.Type.AXIS && mType != ControllerBindingPreference.Type.BUTTON) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (doAxisDetection(event))
|
||||
return true;
|
||||
|
||||
|
@ -14,16 +14,25 @@ import androidx.preference.PreferenceViewHolder;
|
||||
import java.util.Set;
|
||||
|
||||
public class ControllerBindingPreference extends Preference {
|
||||
private enum Type {
|
||||
public enum Type {
|
||||
BUTTON,
|
||||
AXIS,
|
||||
VIBRATION
|
||||
}
|
||||
|
||||
private enum VisualType {
|
||||
BUTTON,
|
||||
AXIS,
|
||||
VIBRATION,
|
||||
HOTKEY
|
||||
}
|
||||
|
||||
private String mBindingName;
|
||||
private String mDisplayName;
|
||||
private String mValue;
|
||||
private TextView mValueView;
|
||||
private Type mType = Type.BUTTON;
|
||||
private VisualType mVisualType = VisualType.BUTTON;
|
||||
|
||||
private static int getIconForButton(String buttonName) {
|
||||
if (buttonName.equals("Up")) {
|
||||
@ -64,7 +73,16 @@ public class ControllerBindingPreference extends Preference {
|
||||
}
|
||||
|
||||
private static int getIconForHotkey(String hotkeyDisplayName) {
|
||||
return R.drawable.ic_baseline_category_24;
|
||||
switch (hotkeyDisplayName) {
|
||||
case "FastForward":
|
||||
case "ToggleFastForward":
|
||||
case "Turbo":
|
||||
case "ToggleTurbo":
|
||||
return R.drawable.ic_controller_fast_forward;
|
||||
|
||||
default:
|
||||
return R.drawable.ic_baseline_category_24;
|
||||
}
|
||||
}
|
||||
|
||||
public ControllerBindingPreference(Context context, AttributeSet attrs) {
|
||||
@ -94,7 +112,7 @@ public class ControllerBindingPreference extends Preference {
|
||||
mValueView = ((TextView) holder.findViewById(R.id.controller_binding_value));
|
||||
|
||||
int drawableId = R.drawable.ic_baseline_radio_button_checked_24;
|
||||
switch (mType) {
|
||||
switch (mVisualType) {
|
||||
case BUTTON:
|
||||
drawableId = getIconForButton(mBindingName);
|
||||
break;
|
||||
@ -104,37 +122,55 @@ public class ControllerBindingPreference extends Preference {
|
||||
case HOTKEY:
|
||||
drawableId = getIconForHotkey(mBindingName);
|
||||
break;
|
||||
case VIBRATION:
|
||||
drawableId = R.drawable.ic_baseline_vibration_24;
|
||||
break;
|
||||
}
|
||||
|
||||
iconView.setImageDrawable(ContextCompat.getDrawable(getContext(), drawableId));
|
||||
nameView.setText(mBindingName);
|
||||
nameView.setText(mDisplayName);
|
||||
updateValue();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onClick() {
|
||||
ControllerBindingDialog dialog = new ControllerBindingDialog(getContext(), mBindingName, getKey(), mValue, (mType == Type.AXIS));
|
||||
ControllerBindingDialog dialog = new ControllerBindingDialog(getContext(), mBindingName, getKey(), mValue, mType);
|
||||
dialog.setOnDismissListener((dismissedDialog) -> updateValue());
|
||||
dialog.show();
|
||||
}
|
||||
|
||||
public void initButton(int controllerIndex, String buttonName) {
|
||||
mBindingName = buttonName;
|
||||
mDisplayName = buttonName;
|
||||
mType = Type.BUTTON;
|
||||
mVisualType = VisualType.BUTTON;
|
||||
setKey(String.format("Controller%d/Button%s", controllerIndex, buttonName));
|
||||
updateValue();
|
||||
}
|
||||
|
||||
public void initAxis(int controllerIndex, String axisName) {
|
||||
mBindingName = axisName;
|
||||
mDisplayName = axisName;
|
||||
mType = Type.AXIS;
|
||||
mVisualType = VisualType.AXIS;
|
||||
setKey(String.format("Controller%d/Axis%s", controllerIndex, axisName));
|
||||
updateValue();
|
||||
}
|
||||
|
||||
public void initVibration(int controllerIndex) {
|
||||
mBindingName = "Rumble";
|
||||
mDisplayName = getContext().getString(R.string.controller_binding_device_for_vibration);
|
||||
mType = Type.VIBRATION;
|
||||
mVisualType = VisualType.VIBRATION;
|
||||
setKey(String.format("Controller%d/Rumble", controllerIndex));
|
||||
updateValue();
|
||||
}
|
||||
|
||||
public void initHotkey(HotkeyInfo hotkeyInfo) {
|
||||
mBindingName = hotkeyInfo.getDisplayName();
|
||||
mType = Type.HOTKEY;
|
||||
mBindingName = hotkeyInfo.getName();
|
||||
mDisplayName = hotkeyInfo.getDisplayName();
|
||||
mType = Type.BUTTON;
|
||||
mVisualType = VisualType.HOTKEY;
|
||||
setKey(hotkeyInfo.getBindingConfigKey());
|
||||
updateValue();
|
||||
}
|
||||
|
@ -172,9 +172,10 @@ public class ControllerMappingActivity extends AppCompatActivity {
|
||||
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
|
||||
final SharedPreferences sp = getPreferenceManager().getSharedPreferences();
|
||||
final String defaultControllerType = controllerIndex == 0 ? "DigitalController" : "None";
|
||||
String controllerType = sp.getString(String.format("Controller%d/Type", controllerIndex), defaultControllerType);
|
||||
String[] controllerButtons = AndroidHostInterface.getControllerButtonNames(controllerType);
|
||||
String[] axisButtons = AndroidHostInterface.getControllerAxisNames(controllerType);
|
||||
final String controllerType = sp.getString(String.format("Controller%d/Type", controllerIndex), defaultControllerType);
|
||||
final String[] controllerButtons = AndroidHostInterface.getControllerButtonNames(controllerType);
|
||||
final String[] axisButtons = AndroidHostInterface.getControllerAxisNames(controllerType);
|
||||
final int vibrationMotors = AndroidHostInterface.getControllerVibrationMotorCount(controllerType);
|
||||
|
||||
final PreferenceScreen ps = getPreferenceManager().createPreferenceScreen(getContext());
|
||||
if (controllerButtons != null) {
|
||||
@ -193,6 +194,12 @@ public class ControllerMappingActivity extends AppCompatActivity {
|
||||
activity.mPreferences.add(cbp);
|
||||
}
|
||||
}
|
||||
if (vibrationMotors > 0) {
|
||||
final ControllerBindingPreference cbp = new ControllerBindingPreference(getContext(), null);
|
||||
cbp.initVibration(controllerIndex);
|
||||
ps.addPreference(cbp);
|
||||
activity.mPreferences.add(cbp);
|
||||
}
|
||||
|
||||
setPreferenceScreen(ps);
|
||||
}
|
||||
|
@ -1,7 +1,6 @@
|
||||
package com.github.stenzek.duckstation;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.pm.ActivityInfo;
|
||||
@ -17,17 +16,14 @@ import android.view.KeyEvent;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.SurfaceHolder;
|
||||
import android.view.View;
|
||||
import android.view.Window;
|
||||
import android.view.WindowManager;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.ListView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
import androidx.preference.PreferenceManager;
|
||||
|
||||
/**
|
||||
@ -157,6 +153,19 @@ public class EmulationActivity extends AppCompatActivity implements SurfaceHolde
|
||||
});
|
||||
}
|
||||
|
||||
public String[] getInputDeviceNames() {
|
||||
return (mContentView != null) ? mContentView.getInputDeviceNames() : null;
|
||||
}
|
||||
|
||||
public boolean hasInputDeviceVibration(int controllerIndex) {
|
||||
return (mContentView != null) ? mContentView.hasInputDeviceVibration(controllerIndex) : null;
|
||||
}
|
||||
|
||||
public void setInputDeviceVibration(int controllerIndex, float smallMotor, float largeMotor) {
|
||||
if (mContentView != null)
|
||||
mContentView.setInputDeviceVibration(controllerIndex, smallMotor, largeMotor);
|
||||
}
|
||||
|
||||
private void doApplySettings() {
|
||||
AndroidHostInterface.getInstance().applySettings();
|
||||
updateRequestedOrientation();
|
||||
@ -731,8 +740,10 @@ public class EmulationActivity extends AppCompatActivity implements SurfaceHolde
|
||||
Log.i("EmulationActivity", "Controller type: " + controllerType);
|
||||
Log.i("EmulationActivity", "View type: " + viewType);
|
||||
|
||||
final boolean hasAnyControllers = mContentView.initControllerMapping(controllerType);
|
||||
mContentView.updateInputDevices();
|
||||
AndroidHostInterface.getInstance().updateInputMap();
|
||||
|
||||
final boolean hasAnyControllers = mContentView.hasAnyGamePads();
|
||||
if (controllerType.equals("none") || viewType.equals("none") || (hasAnyControllers && autoHideTouchscreenController)) {
|
||||
if (mTouchscreenController != null) {
|
||||
activityLayout.removeView(mTouchscreenController);
|
||||
|
@ -2,6 +2,7 @@ package com.github.stenzek.duckstation;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.os.Vibrator;
|
||||
import android.util.AttributeSet;
|
||||
import android.util.Log;
|
||||
import android.view.InputDevice;
|
||||
@ -28,14 +29,43 @@ public class EmulationSurfaceView extends SurfaceView {
|
||||
super(context, attrs, defStyle);
|
||||
}
|
||||
|
||||
public static boolean isDPadOrButtonEvent(KeyEvent event) {
|
||||
final int source = event.getSource();
|
||||
return (source & InputDevice.SOURCE_GAMEPAD) == InputDevice.SOURCE_GAMEPAD ||
|
||||
(source & InputDevice.SOURCE_DPAD) == InputDevice.SOURCE_DPAD ||
|
||||
(source & InputDevice.SOURCE_JOYSTICK) == InputDevice.SOURCE_JOYSTICK;
|
||||
public static boolean isBindableDevice(InputDevice inputDevice) {
|
||||
if (inputDevice == null)
|
||||
return false;
|
||||
|
||||
final int sources = inputDevice.getSources();
|
||||
|
||||
// Prevent binding pointer devices such as a mouse.
|
||||
if ((sources & InputDevice.SOURCE_CLASS_POINTER) == InputDevice.SOURCE_CLASS_POINTER)
|
||||
return false;
|
||||
|
||||
return ((sources & InputDevice.SOURCE_CLASS_JOYSTICK) == InputDevice.SOURCE_CLASS_JOYSTICK) ||
|
||||
((sources & InputDevice.SOURCE_CLASS_BUTTON) == InputDevice.SOURCE_CLASS_BUTTON);
|
||||
}
|
||||
|
||||
private boolean isExternalKeyCode(int keyCode) {
|
||||
public static boolean isGamepadDevice(InputDevice inputDevice) {
|
||||
final int sources = inputDevice.getSources();
|
||||
return ((sources & InputDevice.SOURCE_GAMEPAD) == InputDevice.SOURCE_GAMEPAD);
|
||||
}
|
||||
|
||||
public static boolean isBindableKeyCode(int keyCode) {
|
||||
switch (keyCode) {
|
||||
case KeyEvent.KEYCODE_BACK:
|
||||
case KeyEvent.KEYCODE_HOME:
|
||||
case KeyEvent.KEYCODE_MENU:
|
||||
case KeyEvent.KEYCODE_POWER:
|
||||
case KeyEvent.KEYCODE_CAMERA:
|
||||
case KeyEvent.KEYCODE_CALL:
|
||||
case KeyEvent.KEYCODE_ENDCALL:
|
||||
case KeyEvent.KEYCODE_VOICE_ASSIST:
|
||||
return false;
|
||||
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean isExternalKeyCode(int keyCode) {
|
||||
switch (keyCode) {
|
||||
case KeyEvent.KEYCODE_BACK:
|
||||
case KeyEvent.KEYCODE_HOME:
|
||||
@ -55,157 +85,146 @@ public class EmulationSurfaceView extends SurfaceView {
|
||||
}
|
||||
}
|
||||
|
||||
private static final int[] buttonKeyCodes = new int[]{
|
||||
KeyEvent.KEYCODE_BUTTON_A, // 0/Cross
|
||||
KeyEvent.KEYCODE_BUTTON_B, // 1/Circle
|
||||
KeyEvent.KEYCODE_BUTTON_X, // 2/Square
|
||||
KeyEvent.KEYCODE_BUTTON_Y, // 3/Triangle
|
||||
KeyEvent.KEYCODE_BUTTON_SELECT, // 4/Select
|
||||
KeyEvent.KEYCODE_BUTTON_MODE, // 5/Analog
|
||||
KeyEvent.KEYCODE_BUTTON_START, // 6/Start
|
||||
KeyEvent.KEYCODE_BUTTON_THUMBL, // 7/L3
|
||||
KeyEvent.KEYCODE_BUTTON_THUMBR, // 8/R3
|
||||
KeyEvent.KEYCODE_BUTTON_L1, // 9/L1
|
||||
KeyEvent.KEYCODE_BUTTON_R1, // 10/R1
|
||||
KeyEvent.KEYCODE_DPAD_UP, // 11/Up
|
||||
KeyEvent.KEYCODE_DPAD_DOWN, // 12/Down
|
||||
KeyEvent.KEYCODE_DPAD_LEFT, // 13/Left
|
||||
KeyEvent.KEYCODE_DPAD_RIGHT, // 14/Right
|
||||
KeyEvent.KEYCODE_BUTTON_L2, // 15
|
||||
KeyEvent.KEYCODE_BUTTON_R2, // 16
|
||||
KeyEvent.KEYCODE_BUTTON_C, // 17
|
||||
KeyEvent.KEYCODE_BUTTON_Z, // 18
|
||||
KeyEvent.KEYCODE_VOLUME_DOWN, // 19
|
||||
KeyEvent.KEYCODE_VOLUME_UP, // 20
|
||||
KeyEvent.KEYCODE_MENU, // 21
|
||||
KeyEvent.KEYCODE_CAMERA, // 22
|
||||
};
|
||||
private static final int[] axisCodes = new int[]{
|
||||
MotionEvent.AXIS_X, // 0/LeftX
|
||||
MotionEvent.AXIS_Y, // 1/LeftY
|
||||
MotionEvent.AXIS_Z, // 2/RightX
|
||||
MotionEvent.AXIS_RZ, // 3/RightY
|
||||
MotionEvent.AXIS_LTRIGGER, // 4/L2
|
||||
MotionEvent.AXIS_RTRIGGER, // 5/R2
|
||||
MotionEvent.AXIS_RX, // 6
|
||||
MotionEvent.AXIS_RY, // 7
|
||||
MotionEvent.AXIS_HAT_X, // 8
|
||||
MotionEvent.AXIS_HAT_Y, // 9
|
||||
MotionEvent.AXIS_GAS, // 10
|
||||
MotionEvent.AXIS_BRAKE, // 11
|
||||
};
|
||||
private class InputDeviceData {
|
||||
private int deviceId;
|
||||
private String descriptor;
|
||||
private int[] axes;
|
||||
private float[] axisValues;
|
||||
private int controllerIndex;
|
||||
private Vibrator vibrator;
|
||||
|
||||
public static int getButtonIndexForKeyCode(int keyCode) {
|
||||
for (int buttonIndex = 0; buttonIndex < buttonKeyCodes.length; buttonIndex++) {
|
||||
if (buttonKeyCodes[buttonIndex] == keyCode)
|
||||
return buttonIndex;
|
||||
}
|
||||
|
||||
Log.e("EmulationSurfaceView", String.format("Button code %d not found", keyCode));
|
||||
return -1;
|
||||
}
|
||||
|
||||
public static int[] getKnownAxisCodes() {
|
||||
return axisCodes;
|
||||
}
|
||||
|
||||
public static int getAxisIndexForAxisCode(int axisCode) {
|
||||
for (int axisIndex = 0; axisIndex < axisCodes.length; axisIndex++) {
|
||||
if (axisCodes[axisIndex] == axisCode)
|
||||
return axisIndex;
|
||||
}
|
||||
|
||||
Log.e("EmulationSurfaceView", String.format("Axis code %d not found", axisCode));
|
||||
return -1;
|
||||
}
|
||||
|
||||
|
||||
private class ButtonMapping {
|
||||
public ButtonMapping(int deviceId, int deviceButton, int controllerIndex, int button) {
|
||||
this.deviceId = deviceId;
|
||||
this.deviceAxisOrButton = deviceButton;
|
||||
public InputDeviceData(InputDevice device, int controllerIndex) {
|
||||
deviceId = device.getId();
|
||||
descriptor = device.getDescriptor();
|
||||
this.controllerIndex = controllerIndex;
|
||||
this.buttonMapping = button;
|
||||
}
|
||||
|
||||
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 boolean handleControllerKey(int deviceId, int keyCode, int repeatCount, boolean pressed) {
|
||||
boolean result = false;
|
||||
for (ButtonMapping mapping : mControllerKeyMapping) {
|
||||
if (mapping.deviceId != deviceId || mapping.deviceAxisOrButton != keyCode)
|
||||
continue;
|
||||
|
||||
if (repeatCount == 0) {
|
||||
AndroidHostInterface.getInstance().handleControllerButtonEvent(0, mapping.buttonMapping, pressed);
|
||||
Log.d("EmulationSurfaceView", String.format("handleControllerKey %d -> %d %d", keyCode, mapping.buttonMapping, pressed ? 1 : 0));
|
||||
List<InputDevice.MotionRange> motionRanges = device.getMotionRanges();
|
||||
if (motionRanges != null && !motionRanges.isEmpty()) {
|
||||
axes = new int[motionRanges.size()];
|
||||
axisValues = new float[motionRanges.size()];
|
||||
for (int i = 0; i < motionRanges.size(); i++)
|
||||
axes[i] = motionRanges.get(i).getAxis();
|
||||
}
|
||||
|
||||
result = true;
|
||||
// device.getVibrator() always returns null, but might return a "null vibrator".
|
||||
final Vibrator potentialVibrator = device.getVibrator();
|
||||
if (potentialVibrator != null && potentialVibrator.hasVibrator())
|
||||
vibrator = potentialVibrator;
|
||||
}
|
||||
}
|
||||
|
||||
private InputDeviceData[] mInputDevices = null;
|
||||
private boolean mHasAnyGamepads = false;
|
||||
|
||||
public boolean hasAnyGamePads() {
|
||||
return mHasAnyGamepads;
|
||||
}
|
||||
|
||||
public synchronized void updateInputDevices() {
|
||||
mInputDevices = null;
|
||||
mHasAnyGamepads = false;
|
||||
|
||||
final ArrayList<InputDeviceData> inputDeviceIds = new ArrayList<>();
|
||||
for (int deviceId : InputDevice.getDeviceIds()) {
|
||||
final InputDevice device = InputDevice.getDevice(deviceId);
|
||||
if (device == null || !isBindableDevice(device))
|
||||
continue;
|
||||
|
||||
if (isGamepadDevice(device))
|
||||
mHasAnyGamepads = true;
|
||||
|
||||
final int controllerIndex = inputDeviceIds.size();
|
||||
Log.d("EmulationSurfaceView", String.format("Tracking device %d/%s (%s)",
|
||||
controllerIndex, device.getDescriptor(), device.getName()));
|
||||
inputDeviceIds.add(new InputDeviceData(device, controllerIndex));
|
||||
}
|
||||
|
||||
return result;
|
||||
if (inputDeviceIds.isEmpty())
|
||||
return;
|
||||
|
||||
mInputDevices = new InputDeviceData[inputDeviceIds.size()];
|
||||
inputDeviceIds.toArray(mInputDevices);
|
||||
}
|
||||
|
||||
public synchronized String[] getInputDeviceNames() {
|
||||
if (mInputDevices == null)
|
||||
return null;
|
||||
|
||||
final String[] deviceNames = new String[mInputDevices.length];
|
||||
for (int i = 0; i < mInputDevices.length; i++) {
|
||||
deviceNames[i] = mInputDevices[i].descriptor;
|
||||
}
|
||||
|
||||
return deviceNames;
|
||||
}
|
||||
|
||||
public synchronized boolean hasInputDeviceVibration(int controllerIndex) {
|
||||
if (mInputDevices == null || controllerIndex >= mInputDevices.length)
|
||||
return false;
|
||||
|
||||
return (mInputDevices[controllerIndex].vibrator != null);
|
||||
}
|
||||
|
||||
public synchronized void setInputDeviceVibration(int controllerIndex, float smallMotor, float largeMotor) {
|
||||
if (mInputDevices == null || controllerIndex >= mInputDevices.length)
|
||||
return;
|
||||
|
||||
// shouldn't get here
|
||||
final InputDeviceData data = mInputDevices[controllerIndex];
|
||||
if (data.vibrator == null)
|
||||
return;
|
||||
|
||||
final float MINIMUM_INTENSITY = 0.1f;
|
||||
if (smallMotor >= MINIMUM_INTENSITY || largeMotor >= MINIMUM_INTENSITY)
|
||||
data.vibrator.vibrate(1000);
|
||||
else
|
||||
data.vibrator.cancel();
|
||||
}
|
||||
|
||||
public InputDeviceData getDataForDeviceId(int deviceId) {
|
||||
if (mInputDevices == null)
|
||||
return null;
|
||||
|
||||
for (InputDeviceData data : mInputDevices) {
|
||||
if (data.deviceId == deviceId)
|
||||
return data;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public int getControllerIndexForDeviceId(int deviceId) {
|
||||
final InputDeviceData data = getDataForDeviceId(deviceId);
|
||||
return (data != null) ? data.controllerIndex : -1;
|
||||
}
|
||||
|
||||
private boolean handleKeyEvent(int keyCode, KeyEvent event, boolean pressed) {
|
||||
if (!isBindableDevice(event.getDevice()))
|
||||
return false;
|
||||
|
||||
/*Log.e("ESV", String.format("Code %d RC %d Pressed %d %s", keyCode,
|
||||
event.getRepeatCount(), pressed? 1 : 0, event.toString()));*/
|
||||
|
||||
final AndroidHostInterface hi = AndroidHostInterface.getInstance();
|
||||
final int controllerIndex = getControllerIndexForDeviceId(event.getDeviceId());
|
||||
if (event.getRepeatCount() == 0 && controllerIndex >= 0)
|
||||
hi.handleControllerButtonEvent(controllerIndex, keyCode, pressed);
|
||||
|
||||
// We don't want to eat external button events unless it's actually bound.
|
||||
if (isExternalKeyCode(keyCode))
|
||||
return (controllerIndex >= 0 && hi.hasControllerButtonBinding(controllerIndex, keyCode));
|
||||
else
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onKeyDown(int keyCode, KeyEvent event) {
|
||||
if (!isDPadOrButtonEvent(event))
|
||||
return false;
|
||||
|
||||
if (handleControllerKey(event.getDeviceId(), keyCode, event.getRepeatCount(), true))
|
||||
return true;
|
||||
|
||||
// eat non-external button events anyway
|
||||
return !isExternalKeyCode(keyCode);
|
||||
return handleKeyEvent(keyCode, event, true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onKeyUp(int keyCode, KeyEvent event) {
|
||||
if (!isDPadOrButtonEvent(event))
|
||||
return false;
|
||||
|
||||
if (handleControllerKey(event.getDeviceId(), keyCode, 0, false))
|
||||
return true;
|
||||
|
||||
// eat non-external button events anyway
|
||||
return !isExternalKeyCode(keyCode);
|
||||
return handleKeyEvent(keyCode, event, false);
|
||||
}
|
||||
|
||||
private float clamp(float value, float min, float max) {
|
||||
@ -214,19 +233,19 @@ public class EmulationSurfaceView extends SurfaceView {
|
||||
|
||||
@Override
|
||||
public boolean onGenericMotionEvent(MotionEvent event) {
|
||||
final int source = event.getSource();
|
||||
if ((source & (InputDevice.SOURCE_JOYSTICK | InputDevice.SOURCE_GAMEPAD | InputDevice.SOURCE_DPAD)) == 0)
|
||||
if (!isBindableDevice(event.getDevice()))
|
||||
return false;
|
||||
|
||||
final int deviceId = event.getDeviceId();
|
||||
for (AxisMapping mapping : mControllerAxisMapping) {
|
||||
if (mapping.deviceId != deviceId)
|
||||
continue;
|
||||
final InputDeviceData data = getDataForDeviceId(event.getDeviceId());
|
||||
if (data == null || data.axes == null)
|
||||
return false;
|
||||
|
||||
final float axisValue = event.getAxisValue(mapping.deviceAxisOrButton);
|
||||
for (int i = 0; i < data.axes.length; i++) {
|
||||
final int axis = data.axes[i];
|
||||
final float axisValue = event.getAxisValue(axis);
|
||||
float emuValue;
|
||||
|
||||
switch (mapping.deviceAxisOrButton) {
|
||||
switch (axis) {
|
||||
case MotionEvent.AXIS_BRAKE:
|
||||
case MotionEvent.AXIS_GAS:
|
||||
case MotionEvent.AXIS_LTRIGGER:
|
||||
@ -241,109 +260,16 @@ public class EmulationSurfaceView extends SurfaceView {
|
||||
break;
|
||||
}
|
||||
|
||||
Log.d("EmulationSurfaceView", String.format("axis %d value %f emuvalue %f", mapping.deviceAxisOrButton, axisValue, emuValue));
|
||||
if (data.axisValues[i] == emuValue)
|
||||
continue;
|
||||
|
||||
if (mapping.axisMapping >= 0) {
|
||||
AndroidHostInterface.getInstance().handleControllerAxisEvent(0, mapping.axisMapping, emuValue);
|
||||
}
|
||||
/*Log.d("EmulationSurfaceView",
|
||||
String.format("axis %d value %f emuvalue %f", axis, axisValue, emuValue));*/
|
||||
|
||||
final float DEAD_ZONE = 0.25f;
|
||||
if (mapping.negativeButton >= 0) {
|
||||
AndroidHostInterface.getInstance().handleControllerButtonEvent(0, mapping.negativeButton, (emuValue <= -DEAD_ZONE));
|
||||
}
|
||||
if (mapping.positiveButton >= 0) {
|
||||
AndroidHostInterface.getInstance().handleControllerButtonEvent(0, mapping.positiveButton, (emuValue >= DEAD_ZONE));
|
||||
}
|
||||
data.axisValues[i] = emuValue;
|
||||
AndroidHostInterface.getInstance().handleControllerAxisEvent(data.controllerIndex, axis, emuValue);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean addControllerKeyMapping(int deviceId, int keyCode, int controllerIndex) {
|
||||
int mapping = getButtonIndexForKeyCode(keyCode);
|
||||
Log.i("EmulationSurfaceView", String.format("Map %d to %d", keyCode, mapping));
|
||||
if (mapping >= 0) {
|
||||
mControllerKeyMapping.add(new ButtonMapping(deviceId, keyCode, controllerIndex, mapping));
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean addControllerAxisMapping(int deviceId, List<InputDevice.MotionRange> motionRanges, int axis, int controllerIndex) {
|
||||
InputDevice.MotionRange range = null;
|
||||
for (InputDevice.MotionRange curRange : motionRanges) {
|
||||
if (curRange.getAxis() == axis) {
|
||||
range = curRange;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (range == null)
|
||||
return false;
|
||||
|
||||
int mapping = getAxisIndexForAxisCode(axis);
|
||||
int negativeButton = -1;
|
||||
int positiveButton = -1;
|
||||
|
||||
if (mapping >= 0) {
|
||||
Log.i("EmulationSurfaceView", String.format("Map axis %d to %d", axis, mapping));
|
||||
mControllerAxisMapping.add(new AxisMapping(deviceId, axis, range, controllerIndex, mapping));
|
||||
return true;
|
||||
}
|
||||
|
||||
if (negativeButton >= 0 && negativeButton >= 0) {
|
||||
Log.i("EmulationSurfaceView", String.format("Map axis %d to buttons %d %d", axis, negativeButton, positiveButton));
|
||||
mControllerAxisMapping.add(new AxisMapping(deviceId, axis, range, controllerIndex, positiveButton, negativeButton));
|
||||
return true;
|
||||
}
|
||||
|
||||
Log.w("EmulationSurfaceView", String.format("Axis %d was not mapped", axis));
|
||||
return false;
|
||||
}
|
||||
|
||||
private static boolean isJoystickDevice(int deviceId) {
|
||||
if (deviceId < 0)
|
||||
return false;
|
||||
|
||||
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;
|
||||
|
||||
return (sources & InputDevice.SOURCE_DPAD) == InputDevice.SOURCE_DPAD;
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
for (int keyCode : buttonKeyCodes) {
|
||||
addControllerKeyMapping(deviceId, keyCode, controllerIndex);
|
||||
}
|
||||
|
||||
if (motionRanges != null) {
|
||||
for (int axisCode : axisCodes) {
|
||||
addControllerAxisMapping(deviceId, motionRanges, axisCode, controllerIndex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return !mControllerKeyMapping.isEmpty() || !mControllerKeyMapping.isEmpty();
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -0,0 +1,10 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24"
|
||||
android:tint="?attr/colorControlNormal">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M0,15h2L2,9L0,9v6zM3,17h2L5,7L3,7v10zM22,9v6h2L24,9h-2zM19,17h2L21,7h-2v10zM16.5,3h-9C6.67,3 6,3.67 6,4.5v15c0,0.83 0.67,1.5 1.5,1.5h9c0.83,0 1.5,-0.67 1.5,-1.5v-15c0,-0.83 -0.67,-1.5 -1.5,-1.5zM16,19L8,19L8,5h8v14z"/>
|
||||
</vector>
|
@ -266,4 +266,5 @@
|
||||
<string name="settings_achievements_disclaimer">DuckStation uses RetroAchievements (retroachievements.org) as an achievement database and for tracking progress.</string>
|
||||
<string name="settings_achievements_confirm_logout_title">Confirm Logout</string>
|
||||
<string name="settings_achievements_confirm_logout_message">After logging out, no more achievements will be unlocked until you log back in again. Achievements already unlocked will not be lost.</string>
|
||||
<string name="controller_binding_device_for_vibration">Device for Vibration</string>
|
||||
</resources>
|
||||
|
Reference in New Issue
Block a user