diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/activities/EmulationActivity.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/activities/EmulationActivity.java index 2adb2f209d..4688cbce05 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/activities/EmulationActivity.java +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/activities/EmulationActivity.java @@ -39,6 +39,7 @@ import org.dolphinemu.dolphinemu.fragments.SaveLoadStateFragment; import org.dolphinemu.dolphinemu.ui.main.MainPresenter; import org.dolphinemu.dolphinemu.ui.platform.Platform; import org.dolphinemu.dolphinemu.utils.Animations; +import org.dolphinemu.dolphinemu.utils.ControllerMappingHelper; import org.dolphinemu.dolphinemu.utils.Java_GCAdapter; import org.dolphinemu.dolphinemu.utils.Java_WiimoteAdapter; import org.dolphinemu.dolphinemu.utils.Log; @@ -57,6 +58,7 @@ public final class EmulationActivity extends AppCompatActivity private EmulationFragment mEmulationFragment; private SharedPreferences mPreferences; + private ControllerMappingHelper mControllerMappingHelper; // So that MainActivity knows which view to invalidate before the return animation. private int mPosition; @@ -164,6 +166,7 @@ public final class EmulationActivity extends AppCompatActivity mScreenPath = gameToEmulate.getStringExtra("ScreenPath"); mPosition = gameToEmulate.getIntExtra("GridPosition", -1); mDeviceHasTouchScreen = getPackageManager().hasSystemFeature("android.hardware.touchscreen"); + mControllerMappingHelper = new ControllerMappingHelper(); int themeId; if (mDeviceHasTouchScreen) @@ -729,7 +732,10 @@ public final class EmulationActivity extends AppCompatActivity for (InputDevice.MotionRange range : motions) { - NativeLibrary.onGamePadMoveEvent(input.getDescriptor(), range.getAxis(), event.getAxisValue(range.getAxis())); + int axis = range.getAxis(); + float origValue = event.getAxisValue(axis); + float value = mControllerMappingHelper.scaleAxis(input, axis, origValue); + NativeLibrary.onGamePadMoveEvent(input.getDescriptor(), axis, value); } return true; diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/dialogs/MotionAlertDialog.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/dialogs/MotionAlertDialog.java index 592aee6838..844d877ac6 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/dialogs/MotionAlertDialog.java +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/dialogs/MotionAlertDialog.java @@ -9,6 +9,7 @@ import android.view.KeyEvent; import android.view.MotionEvent; import org.dolphinemu.dolphinemu.model.settings.view.InputBindingSetting; +import org.dolphinemu.dolphinemu.utils.ControllerMappingHelper; import org.dolphinemu.dolphinemu.utils.Log; import java.util.List; @@ -21,6 +22,7 @@ public final class MotionAlertDialog extends AlertDialog { // The selected input preference private final InputBindingSetting setting; + private final ControllerMappingHelper mControllerMappingHelper; private boolean mWaitingForEvent = true; /** @@ -34,6 +36,7 @@ public final class MotionAlertDialog extends AlertDialog super(context); this.setting = setting; + this.mControllerMappingHelper = new ControllerMappingHelper(); } public boolean onKeyEvent(int keyCode, KeyEvent event) @@ -42,8 +45,11 @@ public final class MotionAlertDialog extends AlertDialog switch (event.getAction()) { case KeyEvent.ACTION_DOWN: - saveKeyInput(event); - + if (!mControllerMappingHelper.shouldKeyBeIgnored(event.getDevice(), keyCode)) + { + saveKeyInput(event); + } + // Even if we ignore the key, we still consume it. Thus return true regardless. return true; default: @@ -69,13 +75,15 @@ public final class MotionAlertDialog extends AlertDialog { if ((event.getSource() & InputDevice.SOURCE_CLASS_JOYSTICK) == 0) return false; - - Log.debug("[MotionAlertDialog] Received motion event: " + event.getAction()); + if (event.getAction() != MotionEvent.ACTION_MOVE) + return false; InputDevice input = event.getDevice(); + List motionRanges = input.getMotionRanges(); int numMovedAxis = 0; + float axisMoveValue = 0.0f; InputDevice.MotionRange lastMovedRange = null; char lastMovedDir = '?'; if (mWaitingForEvent) @@ -84,12 +92,23 @@ public final class MotionAlertDialog extends AlertDialog for (InputDevice.MotionRange range : motionRanges) { int axis = range.getAxis(); - float value = event.getAxisValue(axis); + float origValue = event.getAxisValue(axis); + float value = mControllerMappingHelper.scaleAxis(input, axis, origValue); if (Math.abs(value) > 0.5f) { - numMovedAxis++; - lastMovedRange = range; - lastMovedDir = value < 0.0f ? '-' : '+'; + // It is common to have multiple axis with the same physical input. For example, + // shoulder butters are provided as both AXIS_LTRIGGER and AXIS_BRAKE. + // To handle this, we ignore an axis motion that's the exact same as a motion + // we already saw. This way, we ignore axis with two names, but catch the case + // where a joystick is moved in two directions. + // ref: bottom of https://developer.android.com/training/game-controllers/controller-input.html + if (value != axisMoveValue) + { + axisMoveValue = value; + numMovedAxis++; + lastMovedRange = range; + lastMovedDir = value < 0.0f ? '-' : '+'; + } } } diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/ControllerMappingHelper.java b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/ControllerMappingHelper.java new file mode 100644 index 0000000000..1cdc07604a --- /dev/null +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/ControllerMappingHelper.java @@ -0,0 +1,62 @@ +package org.dolphinemu.dolphinemu.utils; + +import android.view.InputDevice; +import android.view.KeyEvent; +import android.view.MotionEvent; + +/** Some controllers have incorrect mappings. This class has special-case fixes for them. */ +public class ControllerMappingHelper +{ + /** Some controllers report extra button presses that can be ignored. */ + public boolean shouldKeyBeIgnored(InputDevice inputDevice, int keyCode) + { + if (isDualShock4(inputDevice)) { + // The two analog triggers generate analog motion events as well as a keycode. + // We always prefer to use the analog values, so throw away the button press + // Even though the triggers are L/R2, without mappings they generate L/R1 events. + return keyCode == KeyEvent.KEYCODE_BUTTON_L1 || keyCode == KeyEvent.KEYCODE_BUTTON_R1; + } + return false; + } + + /** Scale an axis to be zero-centered with a proper range. */ + public float scaleAxis(InputDevice inputDevice, int axis, float value) + { + if (isDualShock4(inputDevice)) + { + // Android doesn't have correct mappings for this controller's triggers. It reports them + // as RX & RY, centered at -1.0, and with a range of [-1.0, 1.0] + // Scale them to properly zero-centered with a range of [0.0, 1.0]. + if (axis == MotionEvent.AXIS_RX || axis == MotionEvent.AXIS_RY) + { + return (value + 1) / 2.0f; + } + } + else if (isXboxOneWireless(inputDevice)) + { + // Same as the DualShock 4, the mappings are missing. + if (axis == MotionEvent.AXIS_Z || axis == MotionEvent.AXIS_RZ) + { + return (value + 1) / 2.0f; + } + if (axis == MotionEvent.AXIS_GENERIC_1) + { + // This axis is stuck at ~.5. Ignore it. + return 0.0f; + } + } + return value; + } + + private boolean isDualShock4(InputDevice inputDevice) + { + // Sony DualShock 4 controller + return inputDevice.getVendorId() == 0x54c && inputDevice.getProductId() == 0x9cc; + } + + private boolean isXboxOneWireless(InputDevice inputDevice) + { + // Microsoft Xbox One controller + return inputDevice.getVendorId() == 0x45e && inputDevice.getProductId() == 0x2e0; + } +}