2023-07-05 18:57:01 -05:00

2775 lines
118 KiB
Java

package com.limelight.binding.input;
import android.annotation.TargetApi;
import android.app.Activity;
import android.content.Context;
import android.hardware.BatteryState;
import android.hardware.Sensor;
import android.hardware.SensorEvent;
import android.hardware.SensorEventListener;
import android.hardware.SensorManager;
import android.hardware.input.InputManager;
import android.hardware.lights.Light;
import android.hardware.lights.LightState;
import android.hardware.lights.LightsManager;
import android.hardware.lights.LightsRequest;
import android.hardware.usb.UsbDevice;
import android.hardware.usb.UsbManager;
import android.media.AudioAttributes;
import android.os.Build;
import android.os.CombinedVibration;
import android.os.Handler;
import android.os.Looper;
import android.os.VibrationAttributes;
import android.os.VibrationEffect;
import android.os.Vibrator;
import android.os.VibratorManager;
import android.util.SparseArray;
import android.view.InputDevice;
import android.view.InputEvent;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.widget.Toast;
import com.limelight.LimeLog;
import com.limelight.binding.input.driver.AbstractController;
import com.limelight.binding.input.driver.UsbDriverListener;
import com.limelight.binding.input.driver.UsbDriverService;
import com.limelight.nvstream.NvConnection;
import com.limelight.nvstream.input.ControllerPacket;
import com.limelight.nvstream.input.MouseButtonPacket;
import com.limelight.nvstream.jni.MoonBridge;
import com.limelight.preferences.PreferenceConfiguration;
import com.limelight.ui.GameGestures;
import com.limelight.utils.Vector2d;
import org.cgutman.shieldcontrollerextensions.SceChargingState;
import org.cgutman.shieldcontrollerextensions.SceConnectionType;
import org.cgutman.shieldcontrollerextensions.SceManager;
import java.lang.reflect.InvocationTargetException;
import java.util.Map;
public class ControllerHandler implements InputManager.InputDeviceListener, UsbDriverListener {
private static final int MAXIMUM_BUMPER_UP_DELAY_MS = 100;
private static final int START_DOWN_TIME_MOUSE_MODE_MS = 750;
private static final int MINIMUM_BUTTON_DOWN_TIME_MS = 25;
private static final int EMULATING_SPECIAL = 0x1;
private static final int EMULATING_SELECT = 0x2;
private static final short MAX_GAMEPADS = 16; // Limited by bits in activeGamepadMask
private static final int BATTERY_RECHECK_INTERVAL_MS = 120 * 1000;
private static final Map<Integer, Integer> ANDROID_TO_LI_BUTTON_MAP = Map.ofEntries(
Map.entry(KeyEvent.KEYCODE_BUTTON_A, ControllerPacket.A_FLAG),
Map.entry(KeyEvent.KEYCODE_BUTTON_B, ControllerPacket.B_FLAG),
Map.entry(KeyEvent.KEYCODE_BUTTON_X, ControllerPacket.X_FLAG),
Map.entry(KeyEvent.KEYCODE_BUTTON_Y, ControllerPacket.Y_FLAG),
Map.entry(KeyEvent.KEYCODE_DPAD_UP, ControllerPacket.UP_FLAG),
Map.entry(KeyEvent.KEYCODE_DPAD_DOWN, ControllerPacket.DOWN_FLAG),
Map.entry(KeyEvent.KEYCODE_DPAD_LEFT, ControllerPacket.LEFT_FLAG),
Map.entry(KeyEvent.KEYCODE_DPAD_RIGHT, ControllerPacket.RIGHT_FLAG),
Map.entry(KeyEvent.KEYCODE_DPAD_UP_LEFT, ControllerPacket.UP_FLAG | ControllerPacket.LEFT_FLAG),
Map.entry(KeyEvent.KEYCODE_DPAD_UP_RIGHT, ControllerPacket.UP_FLAG | ControllerPacket.RIGHT_FLAG),
Map.entry(KeyEvent.KEYCODE_DPAD_DOWN_LEFT, ControllerPacket.DOWN_FLAG | ControllerPacket.LEFT_FLAG),
Map.entry(KeyEvent.KEYCODE_DPAD_DOWN_RIGHT, ControllerPacket.DOWN_FLAG | ControllerPacket.RIGHT_FLAG),
Map.entry(KeyEvent.KEYCODE_BUTTON_L1, ControllerPacket.LB_FLAG),
Map.entry(KeyEvent.KEYCODE_BUTTON_R1, ControllerPacket.RB_FLAG),
Map.entry(KeyEvent.KEYCODE_BUTTON_THUMBL, ControllerPacket.LS_CLK_FLAG),
Map.entry(KeyEvent.KEYCODE_BUTTON_THUMBR, ControllerPacket.RS_CLK_FLAG),
Map.entry(KeyEvent.KEYCODE_BUTTON_START, ControllerPacket.PLAY_FLAG),
Map.entry(KeyEvent.KEYCODE_MENU, ControllerPacket.PLAY_FLAG),
Map.entry(KeyEvent.KEYCODE_BUTTON_SELECT, ControllerPacket.BACK_FLAG),
Map.entry(KeyEvent.KEYCODE_BACK, ControllerPacket.BACK_FLAG),
Map.entry(KeyEvent.KEYCODE_BUTTON_MODE, ControllerPacket.SPECIAL_BUTTON_FLAG),
// This is the Xbox Series X Share button
Map.entry(KeyEvent.KEYCODE_MEDIA_RECORD, ControllerPacket.MISC_FLAG),
// This is a weird one, but it's what Android does prior to 4.10 kernels
// where DualShock/DualSense touchpads weren't mapped as separate devices.
// https://android.googlesource.com/platform/frameworks/base/+/master/data/keyboards/Vendor_054c_Product_0ce6_fallback.kl
// https://android.googlesource.com/platform/frameworks/base/+/master/data/keyboards/Vendor_054c_Product_09cc.kl
Map.entry(KeyEvent.KEYCODE_BUTTON_1, ControllerPacket.TOUCHPAD_FLAG)
// FIXME: Paddles?
);
private final Vector2d inputVector = new Vector2d();
private final SparseArray<InputDeviceContext> inputDeviceContexts = new SparseArray<>();
private final SparseArray<UsbDeviceContext> usbDeviceContexts = new SparseArray<>();
private final NvConnection conn;
private final Activity activityContext;
private final double stickDeadzone;
private final InputDeviceContext defaultContext = new InputDeviceContext();
private final GameGestures gestures;
private final Vibrator deviceVibrator;
private final SceManager sceManager;
private final Handler handler;
private boolean hasGameController;
private final PreferenceConfiguration prefConfig;
private short currentControllers, initialControllers;
public ControllerHandler(Activity activityContext, NvConnection conn, GameGestures gestures, PreferenceConfiguration prefConfig) {
this.activityContext = activityContext;
this.conn = conn;
this.gestures = gestures;
this.prefConfig = prefConfig;
this.deviceVibrator = (Vibrator) activityContext.getSystemService(Context.VIBRATOR_SERVICE);
this.handler = new Handler(Looper.getMainLooper());
this.sceManager = new SceManager(activityContext);
this.sceManager.start();
int deadzonePercentage = prefConfig.deadzonePercentage;
int[] ids = InputDevice.getDeviceIds();
for (int id : ids) {
InputDevice dev = InputDevice.getDevice(id);
if (dev == null) {
// This device was removed during enumeration
continue;
}
if ((dev.getSources() & InputDevice.SOURCE_JOYSTICK) != 0 ||
(dev.getSources() & InputDevice.SOURCE_GAMEPAD) != 0) {
// This looks like a gamepad, but we'll check X and Y to be sure
if (getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_X) != null &&
getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_Y) != null) {
// This is a gamepad
hasGameController = true;
}
}
}
// 1% is the lowest possible deadzone we support
if (deadzonePercentage <= 0) {
deadzonePercentage = 1;
}
this.stickDeadzone = (double)deadzonePercentage / 100.0;
// Initialize the default context for events with no device
defaultContext.leftStickXAxis = MotionEvent.AXIS_X;
defaultContext.leftStickYAxis = MotionEvent.AXIS_Y;
defaultContext.leftStickDeadzoneRadius = (float) stickDeadzone;
defaultContext.rightStickXAxis = MotionEvent.AXIS_Z;
defaultContext.rightStickYAxis = MotionEvent.AXIS_RZ;
defaultContext.rightStickDeadzoneRadius = (float) stickDeadzone;
defaultContext.leftTriggerAxis = MotionEvent.AXIS_BRAKE;
defaultContext.rightTriggerAxis = MotionEvent.AXIS_GAS;
defaultContext.hatXAxis = MotionEvent.AXIS_HAT_X;
defaultContext.hatYAxis = MotionEvent.AXIS_HAT_Y;
defaultContext.controllerNumber = (short) 0;
defaultContext.assignedControllerNumber = true;
defaultContext.external = false;
// Some devices (GPD XD) have a back button which sends input events
// with device ID == 0. This hits the default context which would normally
// consume these. Instead, let's ignore them since that's probably the
// most likely case.
defaultContext.ignoreBack = true;
// Get the initially attached set of gamepads. As each gamepad receives
// its initial InputEvent, we will move these from this set onto the
// currentControllers set which will allow them to properly unplug
// if they are removed.
initialControllers = getAttachedControllerMask(activityContext);
}
private static InputDevice.MotionRange getMotionRangeForJoystickAxis(InputDevice dev, int axis) {
InputDevice.MotionRange range;
// First get the axis for SOURCE_JOYSTICK
range = dev.getMotionRange(axis, InputDevice.SOURCE_JOYSTICK);
if (range == null) {
// Now try the axis for SOURCE_GAMEPAD
range = dev.getMotionRange(axis, InputDevice.SOURCE_GAMEPAD);
}
return range;
}
@Override
public void onInputDeviceAdded(int deviceId) {
// Nothing happening here yet
}
@Override
public void onInputDeviceRemoved(int deviceId) {
InputDeviceContext context = inputDeviceContexts.get(deviceId);
if (context != null) {
LimeLog.info("Removed controller: "+context.name+" ("+deviceId+")");
releaseControllerNumber(context);
context.destroy();
inputDeviceContexts.remove(deviceId);
}
}
// This can happen when gaining/losing input focus with some devices.
// Input devices that have a trackpad may gain/lose AXIS_RELATIVE_X/Y.
@Override
public void onInputDeviceChanged(int deviceId) {
InputDevice device = InputDevice.getDevice(deviceId);
if (device == null) {
return;
}
// If we don't have a context for this device, we don't need to update anything
InputDeviceContext existingContext = inputDeviceContexts.get(deviceId);
if (existingContext == null) {
return;
}
LimeLog.info("Device changed: "+existingContext.name+" ("+deviceId+")");
// Don't release the controller number, because we will carry it over if it is present.
// We also want to make sure the change is invisible to the host PC to avoid an add/remove
// cycle for the gamepad which may break some games.
existingContext.destroy();
InputDeviceContext newContext = createInputDeviceContextForDevice(device);
// Copy over existing controller number state
newContext.assignedControllerNumber = existingContext.assignedControllerNumber;
newContext.reservedControllerNumber = existingContext.reservedControllerNumber;
newContext.controllerNumber = existingContext.controllerNumber;
inputDeviceContexts.put(deviceId, newContext);
}
public void stop() {
for (int i = 0; i < inputDeviceContexts.size(); i++) {
InputDeviceContext deviceContext = inputDeviceContexts.valueAt(i);
deviceContext.destroy();
}
for (int i = 0; i < usbDeviceContexts.size(); i++) {
UsbDeviceContext deviceContext = usbDeviceContexts.valueAt(i);
deviceContext.destroy();
}
sceManager.stop();
deviceVibrator.cancel();
}
private static boolean hasJoystickAxes(InputDevice device) {
return (device.getSources() & InputDevice.SOURCE_JOYSTICK) == InputDevice.SOURCE_JOYSTICK &&
getMotionRangeForJoystickAxis(device, MotionEvent.AXIS_X) != null &&
getMotionRangeForJoystickAxis(device, MotionEvent.AXIS_Y) != null;
}
private static boolean hasGamepadButtons(InputDevice device) {
return (device.getSources() & InputDevice.SOURCE_GAMEPAD) == InputDevice.SOURCE_GAMEPAD;
}
public static boolean isGameControllerDevice(InputDevice device) {
if (device == null) {
return true;
}
if (hasJoystickAxes(device) || hasGamepadButtons(device)) {
// Has real joystick axes or gamepad buttons
return true;
}
// HACK for https://issuetracker.google.com/issues/163120692
if (Build.VERSION.SDK_INT == Build.VERSION_CODES.R) {
if (device.getId() == -1) {
// This "virtual" device could be input from any of the attached devices.
// Look to see if any gamepads are connected.
int[] ids = InputDevice.getDeviceIds();
for (int id : ids) {
InputDevice dev = InputDevice.getDevice(id);
if (dev == null) {
// This device was removed during enumeration
continue;
}
// If there are any gamepad devices connected, we'll
// report that this virtual device is a gamepad.
if (hasJoystickAxes(dev) || hasGamepadButtons(dev)) {
return true;
}
}
}
}
// Otherwise, we'll try anything that claims to be a non-alphabetic keyboard
return device.getKeyboardType() != InputDevice.KEYBOARD_TYPE_ALPHABETIC;
}
public static short getAttachedControllerMask(Context context) {
int count = 0;
short mask = 0;
// Count all input devices that are gamepads
InputManager im = (InputManager) context.getSystemService(Context.INPUT_SERVICE);
for (int id : im.getInputDeviceIds()) {
InputDevice dev = im.getInputDevice(id);
if (dev == null) {
continue;
}
if (hasJoystickAxes(dev)) {
LimeLog.info("Counting InputDevice: "+dev.getName());
mask |= 1 << count++;
}
}
// Count all USB devices that match our drivers
if (PreferenceConfiguration.readPreferences(context).usbDriver) {
UsbManager usbManager = (UsbManager) context.getSystemService(Context.USB_SERVICE);
for (UsbDevice dev : usbManager.getDeviceList().values()) {
// We explicitly check not to claim devices that appear as InputDevices
// otherwise we will double count them.
if (UsbDriverService.shouldClaimDevice(dev, false) &&
!UsbDriverService.isRecognizedInputDevice(dev)) {
LimeLog.info("Counting UsbDevice: "+dev.getDeviceName());
mask |= 1 << count++;
}
}
}
if (PreferenceConfiguration.readPreferences(context).onscreenController) {
LimeLog.info("Counting OSC gamepad");
mask |= 1;
}
LimeLog.info("Enumerated "+count+" gamepads");
return mask;
}
private void releaseControllerNumber(GenericControllerContext context) {
// If we reserved a controller number, remove that reservation
if (context.reservedControllerNumber) {
LimeLog.info("Controller number "+context.controllerNumber+" is now available");
currentControllers &= ~(1 << context.controllerNumber);
}
// If this device sent data as a gamepad, zero the values before removing.
// We must do this after clearing the currentControllers entry so this
// causes the device to be removed on the server PC.
if (context.assignedControllerNumber) {
conn.sendControllerInput(context.controllerNumber, getActiveControllerMask(),
(short) 0,
(byte) 0, (byte) 0,
(short) 0, (short) 0,
(short) 0, (short) 0);
}
}
// Called before sending input but after we've determined that this
// is definitely a controller (not a keyboard, mouse, or something else)
private void assignControllerNumberIfNeeded(GenericControllerContext context) {
if (context.assignedControllerNumber) {
return;
}
if (context instanceof InputDeviceContext) {
InputDeviceContext devContext = (InputDeviceContext) context;
LimeLog.info(devContext.name+" ("+context.id+") needs a controller number assigned");
if (!devContext.external) {
LimeLog.info("Built-in buttons hardcoded as controller 0");
context.controllerNumber = 0;
}
else if (prefConfig.multiController && devContext.hasJoystickAxes) {
context.controllerNumber = 0;
LimeLog.info("Reserving the next available controller number");
for (short i = 0; i < MAX_GAMEPADS; i++) {
if ((currentControllers & (1 << i)) == 0) {
// Found an unused controller value
currentControllers |= (1 << i);
// Take this value out of the initial gamepad set
initialControllers &= ~(1 << i);
context.controllerNumber = i;
context.reservedControllerNumber = true;
break;
}
}
}
else {
LimeLog.info("Not reserving a controller number");
context.controllerNumber = 0;
}
}
else {
if (prefConfig.multiController) {
context.controllerNumber = 0;
LimeLog.info("Reserving the next available controller number");
for (short i = 0; i < MAX_GAMEPADS; i++) {
if ((currentControllers & (1 << i)) == 0) {
// Found an unused controller value
currentControllers |= (1 << i);
// Take this value out of the initial gamepad set
initialControllers &= ~(1 << i);
context.controllerNumber = i;
context.reservedControllerNumber = true;
break;
}
}
}
else {
LimeLog.info("Not reserving a controller number");
context.controllerNumber = 0;
}
}
LimeLog.info("Assigned as controller "+context.controllerNumber);
context.assignedControllerNumber = true;
// Report attributes of this new controller to the host
context.sendControllerArrival();
}
private UsbDeviceContext createUsbDeviceContextForDevice(AbstractController device) {
UsbDeviceContext context = new UsbDeviceContext();
context.id = device.getControllerId();
context.device = device;
context.external = true;
context.vendorId = device.getVendorId();
context.productId = device.getProductId();
context.leftStickDeadzoneRadius = (float) stickDeadzone;
context.rightStickDeadzoneRadius = (float) stickDeadzone;
context.triggerDeadzone = 0.13f;
return context;
}
private static boolean isExternal(InputDevice dev) {
// The ASUS Tinker Board inaccurately reports Bluetooth gamepads as internal,
// causing shouldIgnoreBack() to believe it should pass through back as a
// navigation event for any attached gamepads.
if (Build.MODEL.equals("Tinker Board")) {
return true;
}
String deviceName = dev.getName();
if (deviceName.contains("gpio") || // This is the back button on Shield portable consoles
deviceName.contains("joy_key") || // These are the gamepad buttons on the Archos Gamepad 2
deviceName.contains("keypad") || // These are gamepad buttons on the XPERIA Play
deviceName.equalsIgnoreCase("NVIDIA Corporation NVIDIA Controller v01.01") || // Gamepad on Shield Portable
deviceName.equalsIgnoreCase("NVIDIA Corporation NVIDIA Controller v01.02")) // Gamepad on Shield Portable (?)
{
LimeLog.info(dev.getName()+" is internal by hardcoded mapping");
return false;
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
// Landroid/view/InputDevice;->isExternal()Z is officially public on Android Q
return dev.isExternal();
}
else {
try {
// Landroid/view/InputDevice;->isExternal()Z is on the light graylist in Android P
return (Boolean)dev.getClass().getMethod("isExternal").invoke(dev);
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
} catch (ClassCastException e) {
e.printStackTrace();
}
}
// Answer true if we don't know
return true;
}
private boolean shouldIgnoreBack(InputDevice dev) {
String devName = dev.getName();
// The Serval has a Select button but the framework doesn't
// know about that because it uses a non-standard scancode.
if (devName.contains("Razer Serval")) {
return true;
}
// Classify this device as a remote by name if it has no joystick axes
if (!hasJoystickAxes(dev) && devName.toLowerCase().contains("remote")) {
return true;
}
// Otherwise, dynamically try to determine whether we should allow this
// back button to function for navigation.
//
// First, check if this is an internal device we're being called on.
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT && !isExternal(dev)) {
InputManager im = (InputManager) activityContext.getSystemService(Context.INPUT_SERVICE);
boolean foundInternalGamepad = false;
boolean foundInternalSelect = false;
for (int id : im.getInputDeviceIds()) {
InputDevice currentDev = im.getInputDevice(id);
// Ignore external devices
if (currentDev == null || isExternal(currentDev)) {
continue;
}
// Note that we are explicitly NOT excluding the current device we're examining here,
// since the other gamepad buttons may be on our current device and that's fine.
if (currentDev.hasKeys(KeyEvent.KEYCODE_BUTTON_SELECT)[0]) {
foundInternalSelect = true;
}
// We don't check KEYCODE_BUTTON_A here, since the Shield Android TV has a
// virtual mouse device that claims to have KEYCODE_BUTTON_A. Instead, we rely
// on the SOURCE_GAMEPAD flag to be set on gamepad devices.
if (hasGamepadButtons(currentDev)) {
foundInternalGamepad = true;
}
}
// Allow the back button to function for navigation if we either:
// a) have no internal gamepad (most phones)
// b) have an internal gamepad but also have an internal select button (GPD XD)
// but not:
// c) have an internal gamepad but no internal select button (NVIDIA SHIELD Portable)
return !foundInternalGamepad || foundInternalSelect;
}
else {
// For external devices, we want to pass through the back button if the device
// has no gamepad axes or gamepad buttons.
return !hasJoystickAxes(dev) && !hasGamepadButtons(dev);
}
}
private InputDeviceContext createInputDeviceContextForDevice(InputDevice dev) {
InputDeviceContext context = new InputDeviceContext();
String devName = dev.getName();
LimeLog.info("Creating controller context for device: "+devName);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
LimeLog.info("Vendor ID: "+dev.getVendorId());
LimeLog.info("Product ID: "+dev.getProductId());
}
LimeLog.info(dev.toString());
context.inputDevice = dev;
context.name = devName;
context.id = dev.getId();
context.external = isExternal(dev);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
context.vendorId = dev.getVendorId();
context.productId = dev.getProductId();
// These aren't always present in the Android key layout files, so they won't show up
// in our normal InputDevice.hasKeys() probing.
context.hasPaddles = MoonBridge.guessControllerHasPaddles(context.vendorId, context.productId);
context.hasShare = MoonBridge.guessControllerHasShareButton(context.vendorId, context.productId);
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && hasQuadAmplitudeControlledRumbleVibrators(dev.getVibratorManager())) {
context.vibratorManager = dev.getVibratorManager();
context.quadVibrators = true;
}
else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && hasDualAmplitudeControlledRumbleVibrators(dev.getVibratorManager())) {
context.vibratorManager = dev.getVibratorManager();
context.quadVibrators = false;
}
else if (dev.getVibrator().hasVibrator()) {
context.vibrator = dev.getVibrator();
}
// Detect if the gamepad has Mode and Select buttons according to the Android key layouts.
// We do this first because other codepaths below may override these defaults.
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
boolean[] buttons = dev.hasKeys(KeyEvent.KEYCODE_BUTTON_MODE, KeyEvent.KEYCODE_BUTTON_SELECT, KeyEvent.KEYCODE_BACK, 0);
context.hasMode = buttons[0];
context.hasSelect = buttons[1] || buttons[2];
}
context.touchpadXRange = dev.getMotionRange(MotionEvent.AXIS_X, InputDevice.SOURCE_TOUCHPAD);
context.touchpadYRange = dev.getMotionRange(MotionEvent.AXIS_Y, InputDevice.SOURCE_TOUCHPAD);
context.touchpadPressureRange = dev.getMotionRange(MotionEvent.AXIS_PRESSURE, InputDevice.SOURCE_TOUCHPAD);
context.leftStickXAxis = MotionEvent.AXIS_X;
context.leftStickYAxis = MotionEvent.AXIS_Y;
if (getMotionRangeForJoystickAxis(dev, context.leftStickXAxis) != null &&
getMotionRangeForJoystickAxis(dev, context.leftStickYAxis) != null) {
// This is a gamepad
hasGameController = true;
context.hasJoystickAxes = true;
}
InputDevice.MotionRange leftTriggerRange = getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_LTRIGGER);
InputDevice.MotionRange rightTriggerRange = getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_RTRIGGER);
InputDevice.MotionRange brakeRange = getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_BRAKE);
InputDevice.MotionRange gasRange = getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_GAS);
InputDevice.MotionRange throttleRange = getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_THROTTLE);
if (leftTriggerRange != null && rightTriggerRange != null)
{
// Some controllers use LTRIGGER and RTRIGGER (like Ouya)
context.leftTriggerAxis = MotionEvent.AXIS_LTRIGGER;
context.rightTriggerAxis = MotionEvent.AXIS_RTRIGGER;
}
else if (brakeRange != null && gasRange != null)
{
// Others use GAS and BRAKE (like Moga)
context.leftTriggerAxis = MotionEvent.AXIS_BRAKE;
context.rightTriggerAxis = MotionEvent.AXIS_GAS;
}
else if (brakeRange != null && throttleRange != null)
{
// Others use THROTTLE and BRAKE (like Xiaomi)
context.leftTriggerAxis = MotionEvent.AXIS_BRAKE;
context.rightTriggerAxis = MotionEvent.AXIS_THROTTLE;
}
else
{
InputDevice.MotionRange rxRange = getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_RX);
InputDevice.MotionRange ryRange = getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_RY);
if (rxRange != null && ryRange != null && devName != null) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
if (dev.getVendorId() == 0x054c) { // Sony
if (dev.hasKeys(KeyEvent.KEYCODE_BUTTON_C)[0]) {
LimeLog.info("Detected non-standard DualShock 4 mapping");
context.isNonStandardDualShock4 = true;
}
else {
LimeLog.info("Detected DualShock 4 (Linux standard mapping)");
context.usesLinuxGamepadStandardFaceButtons = true;
}
}
}
else if (!devName.contains("Xbox") && !devName.contains("XBox") && !devName.contains("X-Box")) {
LimeLog.info("Assuming non-standard DualShock 4 mapping on < 4.4");
context.isNonStandardDualShock4 = true;
}
if (context.isNonStandardDualShock4) {
// The old DS4 driver uses RX and RY for triggers
context.leftTriggerAxis = MotionEvent.AXIS_RX;
context.rightTriggerAxis = MotionEvent.AXIS_RY;
// DS4 has Select and Mode buttons (possibly mapped non-standard)
context.hasSelect = true;
context.hasMode = true;
}
else {
// If it's not a non-standard DS4 controller, it's probably an Xbox controller or
// other sane controller that uses RX and RY for right stick and Z and RZ for triggers.
context.rightStickXAxis = MotionEvent.AXIS_RX;
context.rightStickYAxis = MotionEvent.AXIS_RY;
// While it's likely that Z and RZ are triggers, we may have digital trigger buttons
// instead. We must check that we actually have Z and RZ axes before assigning them.
if (getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_Z) != null &&
getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_RZ) != null) {
context.leftTriggerAxis = MotionEvent.AXIS_Z;
context.rightTriggerAxis = MotionEvent.AXIS_RZ;
}
}
// Triggers always idle negative on axes that are centered at zero
context.triggersIdleNegative = true;
}
}
if (context.rightStickXAxis == -1 && context.rightStickYAxis == -1) {
InputDevice.MotionRange zRange = getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_Z);
InputDevice.MotionRange rzRange = getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_RZ);
// Most other controllers use Z and RZ for the right stick
if (zRange != null && rzRange != null) {
context.rightStickXAxis = MotionEvent.AXIS_Z;
context.rightStickYAxis = MotionEvent.AXIS_RZ;
}
else {
InputDevice.MotionRange rxRange = getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_RX);
InputDevice.MotionRange ryRange = getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_RY);
// Try RX and RY now
if (rxRange != null && ryRange != null) {
context.rightStickXAxis = MotionEvent.AXIS_RX;
context.rightStickYAxis = MotionEvent.AXIS_RY;
}
}
}
// Some devices have "hats" for d-pads
InputDevice.MotionRange hatXRange = getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_HAT_X);
InputDevice.MotionRange hatYRange = getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_HAT_Y);
if (hatXRange != null && hatYRange != null) {
context.hatXAxis = MotionEvent.AXIS_HAT_X;
context.hatYAxis = MotionEvent.AXIS_HAT_Y;
}
if (context.leftStickXAxis != -1 && context.leftStickYAxis != -1) {
context.leftStickDeadzoneRadius = (float) stickDeadzone;
}
if (context.rightStickXAxis != -1 && context.rightStickYAxis != -1) {
context.rightStickDeadzoneRadius = (float) stickDeadzone;
}
if (context.leftTriggerAxis != -1 && context.rightTriggerAxis != -1) {
InputDevice.MotionRange ltRange = getMotionRangeForJoystickAxis(dev, context.leftTriggerAxis);
InputDevice.MotionRange rtRange = getMotionRangeForJoystickAxis(dev, context.rightTriggerAxis);
// It's important to have a valid deadzone so controller packet batching works properly
context.triggerDeadzone = Math.max(Math.abs(ltRange.getFlat()), Math.abs(rtRange.getFlat()));
// For triggers without (valid) deadzones, we'll use 13% (around XInput's default)
if (context.triggerDeadzone < 0.13f ||
context.triggerDeadzone > 0.30f)
{
context.triggerDeadzone = 0.13f;
}
}
// The ADT-1 controller needs a similar fixup to the ASUS Gamepad
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
// The device name provided is just "Gamepad" which is pretty useless, so we
// use VID/PID instead
if (dev.getVendorId() == 0x18d1 && dev.getProductId() == 0x2c40) {
context.backIsStart = true;
context.modeIsSelect = true;
context.triggerDeadzone = 0.30f;
context.hasSelect = true;
context.hasMode = false;
}
}
context.ignoreBack = shouldIgnoreBack(dev);
if (devName != null) {
// For the Nexus Player (and probably other ATV devices), we should
// use the back button as start since it doesn't have a start/menu button
// on the controller
if (devName.contains("ASUS Gamepad")) {
// We can only do this check on KitKat or higher, but it doesn't matter since ATV
// is Android 5.0 anyway
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
boolean[] hasStartKey = dev.hasKeys(KeyEvent.KEYCODE_BUTTON_START, KeyEvent.KEYCODE_MENU, 0);
if (!hasStartKey[0] && !hasStartKey[1]) {
context.backIsStart = true;
context.modeIsSelect = true;
context.hasSelect = true;
context.hasMode = false;
}
}
// The ASUS Gamepad has triggers that sit far forward and are prone to false presses
// so we increase the deadzone on them to minimize this
context.triggerDeadzone = 0.30f;
}
// SHIELD controllers will use small stick deadzones
else if (devName.contains("SHIELD") || devName.contains("NVIDIA Controller")) {
// The big Nvidia button on the Shield controllers acts like a Search button. It
// summons the Google Assistant on the Shield TV. On my Pixel 4, it seems to do
// nothing, so we can hijack it to act like a mode button.
if (devName.contains("NVIDIA Controller v01.03") || devName.contains("NVIDIA Controller v01.04")) {
context.searchIsMode = true;
context.hasMode = true;
}
}
// The Serval has a couple of unknown buttons that are start and select. It also has
// a back button which we want to ignore since there's already a select button.
else if (devName.contains("Razer Serval")) {
context.isServal = true;
// Serval has Select and Mode buttons (possibly mapped non-standard)
context.hasMode = true;
context.hasSelect = true;
}
// The Xbox One S Bluetooth controller has some mappings that need fixing up.
// However, Microsoft released a firmware update with no change to VID/PID
// or device name that fixed the mappings for Android. Since there's
// no good way to detect this, we'll use the presence of GAS/BRAKE axes
// that were added in the latest firmware. If those are present, the only
// required fixup is ignoring the select button.
else if (devName.equals("Xbox Wireless Controller")) {
if (gasRange == null) {
context.isNonStandardXboxBtController = true;
// Xbox One S has Select and Mode buttons (possibly mapped non-standard)
context.hasMode = true;
context.hasSelect = true;
}
}
}
LimeLog.info("Analog stick deadzone: "+context.leftStickDeadzoneRadius+" "+context.rightStickDeadzoneRadius);
LimeLog.info("Trigger deadzone: "+context.triggerDeadzone);
return context;
}
private InputDeviceContext getContextForEvent(InputEvent event) {
// Unknown devices use the default context
if (event.getDeviceId() == 0) {
return defaultContext;
}
else if (event.getDevice() == null) {
// During device removal, sometimes we can get events after the
// input device has been destroyed. In this case we'll see a
// != 0 device ID but no device attached.
return null;
}
// HACK for https://issuetracker.google.com/issues/163120692
if (Build.VERSION.SDK_INT == Build.VERSION_CODES.R) {
if (event.getDeviceId() == -1) {
return defaultContext;
}
}
// Return the existing context if it exists
InputDeviceContext context = inputDeviceContexts.get(event.getDeviceId());
if (context != null) {
return context;
}
// Otherwise create a new context
context = createInputDeviceContextForDevice(event.getDevice());
inputDeviceContexts.put(event.getDeviceId(), context);
return context;
}
private byte maxByMagnitude(byte a, byte b) {
int absA = Math.abs(a);
int absB = Math.abs(b);
if (absA > absB) {
return a;
}
else {
return b;
}
}
private short maxByMagnitude(short a, short b) {
int absA = Math.abs(a);
int absB = Math.abs(b);
if (absA > absB) {
return a;
}
else {
return b;
}
}
private short getActiveControllerMask() {
if (prefConfig.multiController) {
return (short)(currentControllers | initialControllers | (prefConfig.onscreenController ? 1 : 0));
}
else {
// Only Player 1 is active with multi-controller disabled
return 1;
}
}
private static boolean areBatteryCapacitiesEqual(float first, float second) {
// With no NaNs involved, it is a simple equality comparison.
if (!Float.isNaN(first) && !Float.isNaN(second)) {
return first == second;
}
else {
// If we have a NaN in one or both positions, compare NaN-ness instead.
// Equality comparisons will always return false for NaN.
return Float.isNaN(first) == Float.isNaN(second);
}
}
private void sendControllerBatteryPacket(InputDeviceContext context) {
int currentBatteryStatus;
float currentBatteryCapacity;
// Use the BatteryState object introduced in Android S, if it's available and present.
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && context.inputDevice.getBatteryState().isPresent()) {
currentBatteryStatus = context.inputDevice.getBatteryState().getStatus();
currentBatteryCapacity = context.inputDevice.getBatteryState().getCapacity();
}
else if (sceManager.isRecognizedDevice(context.inputDevice)) {
// On the SHIELD Android TV, we can use a proprietary API to access battery/charge state.
// We will convert it to the same form used by BatteryState to share code.
int batteryPercentage = sceManager.getBatteryPercentage(context.inputDevice);
if (batteryPercentage < 0) {
currentBatteryCapacity = Float.NaN;
}
else {
currentBatteryCapacity = batteryPercentage / 100.f;
}
SceConnectionType connectionType = sceManager.getConnectionType(context.inputDevice);
SceChargingState chargingState = sceManager.getChargingState(context.inputDevice);
// We can make some assumptions about charge state based on the connection type
if (connectionType == SceConnectionType.WIRED || connectionType == SceConnectionType.BOTH) {
if (batteryPercentage == 100) {
currentBatteryStatus = BatteryState.STATUS_FULL;
}
else if (chargingState == SceChargingState.NOT_CHARGING) {
currentBatteryStatus = BatteryState.STATUS_NOT_CHARGING;
}
else {
currentBatteryStatus = BatteryState.STATUS_CHARGING;
}
}
else if (connectionType == SceConnectionType.WIRELESS) {
if (chargingState == SceChargingState.CHARGING) {
currentBatteryStatus = BatteryState.STATUS_CHARGING;
}
else {
currentBatteryStatus = BatteryState.STATUS_DISCHARGING;
}
}
else {
// If connection type is unknown, just use the charge state
if (batteryPercentage == 100) {
currentBatteryStatus = BatteryState.STATUS_FULL;
}
else if (chargingState == SceChargingState.NOT_CHARGING) {
currentBatteryStatus = BatteryState.STATUS_DISCHARGING;
}
else if (chargingState == SceChargingState.CHARGING) {
currentBatteryStatus = BatteryState.STATUS_CHARGING;
}
else {
currentBatteryStatus = BatteryState.STATUS_UNKNOWN;
}
}
}
else {
return;
}
if (currentBatteryStatus != context.lastReportedBatteryStatus ||
!areBatteryCapacitiesEqual(currentBatteryCapacity, context.lastReportedBatteryCapacity)) {
byte state;
byte percentage;
switch (currentBatteryStatus) {
case BatteryState.STATUS_UNKNOWN:
state = MoonBridge.LI_BATTERY_STATE_UNKNOWN;
break;
case BatteryState.STATUS_CHARGING:
state = MoonBridge.LI_BATTERY_STATE_CHARGING;
break;
case BatteryState.STATUS_DISCHARGING:
state = MoonBridge.LI_BATTERY_STATE_DISCHARGING;
break;
case BatteryState.STATUS_NOT_CHARGING:
state = MoonBridge.LI_BATTERY_STATE_NOT_CHARGING;
break;
case BatteryState.STATUS_FULL:
state = MoonBridge.LI_BATTERY_STATE_FULL;
break;
default:
return;
}
if (Float.isNaN(currentBatteryCapacity)) {
percentage = MoonBridge.LI_BATTERY_PERCENTAGE_UNKNOWN;
}
else {
percentage = (byte)(currentBatteryCapacity * 100);
}
conn.sendControllerBatteryEvent((byte)context.controllerNumber, state, percentage);
context.lastReportedBatteryStatus = currentBatteryStatus;
context.lastReportedBatteryCapacity = currentBatteryCapacity;
}
}
private void sendControllerInputPacket(GenericControllerContext originalContext) {
assignControllerNumberIfNeeded(originalContext);
// Take the context's controller number and fuse all inputs with the same number
short controllerNumber = originalContext.controllerNumber;
int inputMap = 0;
byte leftTrigger = 0;
byte rightTrigger = 0;
short leftStickX = 0;
short leftStickY = 0;
short rightStickX = 0;
short rightStickY = 0;
// In order to properly handle controllers that are split into multiple devices,
// we must aggregate all controllers with the same controller number into a single
// device before we send it.
for (int i = 0; i < inputDeviceContexts.size(); i++) {
GenericControllerContext context = inputDeviceContexts.valueAt(i);
if (context.assignedControllerNumber &&
context.controllerNumber == controllerNumber &&
context.mouseEmulationActive == originalContext.mouseEmulationActive) {
inputMap |= context.inputMap;
leftTrigger |= maxByMagnitude(leftTrigger, context.leftTrigger);
rightTrigger |= maxByMagnitude(rightTrigger, context.rightTrigger);
leftStickX |= maxByMagnitude(leftStickX, context.leftStickX);
leftStickY |= maxByMagnitude(leftStickY, context.leftStickY);
rightStickX |= maxByMagnitude(rightStickX, context.rightStickX);
rightStickY |= maxByMagnitude(rightStickY, context.rightStickY);
}
}
for (int i = 0; i < usbDeviceContexts.size(); i++) {
GenericControllerContext context = usbDeviceContexts.valueAt(i);
if (context.assignedControllerNumber &&
context.controllerNumber == controllerNumber &&
context.mouseEmulationActive == originalContext.mouseEmulationActive) {
inputMap |= context.inputMap;
leftTrigger |= maxByMagnitude(leftTrigger, context.leftTrigger);
rightTrigger |= maxByMagnitude(rightTrigger, context.rightTrigger);
leftStickX |= maxByMagnitude(leftStickX, context.leftStickX);
leftStickY |= maxByMagnitude(leftStickY, context.leftStickY);
rightStickX |= maxByMagnitude(rightStickX, context.rightStickX);
rightStickY |= maxByMagnitude(rightStickY, context.rightStickY);
}
}
if (defaultContext.controllerNumber == controllerNumber) {
inputMap |= defaultContext.inputMap;
leftTrigger |= maxByMagnitude(leftTrigger, defaultContext.leftTrigger);
rightTrigger |= maxByMagnitude(rightTrigger, defaultContext.rightTrigger);
leftStickX |= maxByMagnitude(leftStickX, defaultContext.leftStickX);
leftStickY |= maxByMagnitude(leftStickY, defaultContext.leftStickY);
rightStickX |= maxByMagnitude(rightStickX, defaultContext.rightStickX);
rightStickY |= maxByMagnitude(rightStickY, defaultContext.rightStickY);
}
if (originalContext.mouseEmulationActive) {
int changedMask = inputMap ^ originalContext.mouseEmulationLastInputMap;
boolean aDown = (inputMap & ControllerPacket.A_FLAG) != 0;
boolean bDown = (inputMap & ControllerPacket.B_FLAG) != 0;
originalContext.mouseEmulationLastInputMap = inputMap;
if ((changedMask & ControllerPacket.A_FLAG) != 0) {
if (aDown) {
conn.sendMouseButtonDown(MouseButtonPacket.BUTTON_LEFT);
}
else {
conn.sendMouseButtonUp(MouseButtonPacket.BUTTON_LEFT);
}
}
if ((changedMask & ControllerPacket.B_FLAG) != 0) {
if (bDown) {
conn.sendMouseButtonDown(MouseButtonPacket.BUTTON_RIGHT);
}
else {
conn.sendMouseButtonUp(MouseButtonPacket.BUTTON_RIGHT);
}
}
if ((changedMask & ControllerPacket.UP_FLAG) != 0) {
if ((inputMap & ControllerPacket.UP_FLAG) != 0) {
conn.sendMouseScroll((byte) 1);
}
}
if ((changedMask & ControllerPacket.DOWN_FLAG) != 0) {
if ((inputMap & ControllerPacket.DOWN_FLAG) != 0) {
conn.sendMouseScroll((byte) -1);
}
}
if ((changedMask & ControllerPacket.RIGHT_FLAG) != 0) {
if ((inputMap & ControllerPacket.RIGHT_FLAG) != 0) {
conn.sendMouseHScroll((byte) 1);
}
}
if ((changedMask & ControllerPacket.LEFT_FLAG) != 0) {
if ((inputMap & ControllerPacket.LEFT_FLAG) != 0) {
conn.sendMouseHScroll((byte) -1);
}
}
conn.sendControllerInput(controllerNumber, getActiveControllerMask(),
(short)0, (byte)0, (byte)0, (short)0, (short)0, (short)0, (short)0);
}
else {
conn.sendControllerInput(controllerNumber, getActiveControllerMask(),
inputMap,
leftTrigger, rightTrigger,
leftStickX, leftStickY,
rightStickX, rightStickY);
}
}
private final int REMAP_IGNORE = -1;
private final int REMAP_CONSUME = -2;
// Return a valid keycode, -2 to consume, or -1 to not consume the event
// Device MAY BE NULL
private int handleRemapping(InputDeviceContext context, KeyEvent event) {
// Don't capture the back button if configured
if (context.ignoreBack) {
if (event.getKeyCode() == KeyEvent.KEYCODE_BACK) {
return REMAP_IGNORE;
}
}
// If we know this gamepad has a share button and receive an unmapped
// KEY_RECORD event, report that as a share button press.
if (context.hasShare) {
if (event.getKeyCode() == KeyEvent.KEYCODE_UNKNOWN &&
event.getScanCode() == 167) {
return KeyEvent.KEYCODE_MEDIA_RECORD;
}
}
// Override mode button for 8BitDo controllers
if (context.vendorId == 0x2dc8 && event.getScanCode() == 306) {
return KeyEvent.KEYCODE_BUTTON_MODE;
}
// This mapping was adding in Android 10, then changed based on
// kernel changes (adding hid-nintendo) in Android 11. If we're
// on anything newer than Pie, just use the built-in mapping.
if ((context.vendorId == 0x057e && context.productId == 0x2009 && Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) || // Switch Pro controller
(context.vendorId == 0x0f0d && context.productId == 0x00c1)) { // HORIPAD for Switch
switch (event.getScanCode()) {
case 0x130:
return KeyEvent.KEYCODE_BUTTON_A;
case 0x131:
return KeyEvent.KEYCODE_BUTTON_B;
case 0x132:
return KeyEvent.KEYCODE_BUTTON_X;
case 0x133:
return KeyEvent.KEYCODE_BUTTON_Y;
case 0x134:
return KeyEvent.KEYCODE_BUTTON_L1;
case 0x135:
return KeyEvent.KEYCODE_BUTTON_R1;
case 0x136:
return KeyEvent.KEYCODE_BUTTON_L2;
case 0x137:
return KeyEvent.KEYCODE_BUTTON_R2;
case 0x138:
return KeyEvent.KEYCODE_BUTTON_SELECT;
case 0x139:
return KeyEvent.KEYCODE_BUTTON_START;
case 0x13A:
return KeyEvent.KEYCODE_BUTTON_THUMBL;
case 0x13B:
return KeyEvent.KEYCODE_BUTTON_THUMBR;
case 0x13D:
return KeyEvent.KEYCODE_BUTTON_MODE;
}
}
if (context.usesLinuxGamepadStandardFaceButtons) {
// Android's Generic.kl swaps BTN_NORTH and BTN_WEST
switch (event.getScanCode()) {
case 304:
return KeyEvent.KEYCODE_BUTTON_A;
case 305:
return KeyEvent.KEYCODE_BUTTON_B;
case 307:
return KeyEvent.KEYCODE_BUTTON_Y;
case 308:
return KeyEvent.KEYCODE_BUTTON_X;
}
}
if (context.isNonStandardDualShock4) {
switch (event.getScanCode()) {
case 304:
return KeyEvent.KEYCODE_BUTTON_X;
case 305:
return KeyEvent.KEYCODE_BUTTON_A;
case 306:
return KeyEvent.KEYCODE_BUTTON_B;
case 307:
return KeyEvent.KEYCODE_BUTTON_Y;
case 308:
return KeyEvent.KEYCODE_BUTTON_L1;
case 309:
return KeyEvent.KEYCODE_BUTTON_R1;
/*
**** Using analog triggers instead ****
case 310:
return KeyEvent.KEYCODE_BUTTON_L2;
case 311:
return KeyEvent.KEYCODE_BUTTON_R2;
*/
case 312:
return KeyEvent.KEYCODE_BUTTON_SELECT;
case 313:
return KeyEvent.KEYCODE_BUTTON_START;
case 314:
return KeyEvent.KEYCODE_BUTTON_THUMBL;
case 315:
return KeyEvent.KEYCODE_BUTTON_THUMBR;
case 316:
return KeyEvent.KEYCODE_BUTTON_MODE;
default:
return REMAP_CONSUME;
}
}
// If this is a Serval controller sending an unknown key code, it's probably
// the start and select buttons
else if (context.isServal && event.getKeyCode() == KeyEvent.KEYCODE_UNKNOWN) {
switch (event.getScanCode()) {
case 314:
return KeyEvent.KEYCODE_BUTTON_SELECT;
case 315:
return KeyEvent.KEYCODE_BUTTON_START;
}
}
else if (context.isNonStandardXboxBtController) {
switch (event.getScanCode()) {
case 306:
return KeyEvent.KEYCODE_BUTTON_X;
case 307:
return KeyEvent.KEYCODE_BUTTON_Y;
case 308:
return KeyEvent.KEYCODE_BUTTON_L1;
case 309:
return KeyEvent.KEYCODE_BUTTON_R1;
case 310:
return KeyEvent.KEYCODE_BUTTON_SELECT;
case 311:
return KeyEvent.KEYCODE_BUTTON_START;
case 312:
return KeyEvent.KEYCODE_BUTTON_THUMBL;
case 313:
return KeyEvent.KEYCODE_BUTTON_THUMBR;
case 139:
return KeyEvent.KEYCODE_BUTTON_MODE;
default:
// Other buttons are mapped correctly
}
// The Xbox button is sent as MENU
if (event.getKeyCode() == KeyEvent.KEYCODE_MENU) {
return KeyEvent.KEYCODE_BUTTON_MODE;
}
}
else if (context.vendorId == 0x0b05 && // ASUS
(context.productId == 0x7900 || // Kunai - USB
context.productId == 0x7902)) // Kunai - Bluetooth
{
// ROG Kunai has special M1-M4 buttons that are accessible via the
// joycon-style detachable controllers that we should map to Start
// and Select.
switch (event.getScanCode()) {
case 264:
case 266:
return KeyEvent.KEYCODE_BUTTON_START;
case 265:
case 267:
return KeyEvent.KEYCODE_BUTTON_SELECT;
}
}
if (context.hatXAxis == -1 &&
context.hatYAxis == -1 &&
/* FIXME: There's no good way to know for sure if xpad is bound
to this device, so we won't use the name to validate if these
scancodes should be mapped to DPAD
context.isXboxController &&
*/
event.getKeyCode() == KeyEvent.KEYCODE_UNKNOWN) {
// If there's not a proper Xbox controller mapping, we'll translate the raw d-pad
// scan codes into proper key codes
switch (event.getScanCode())
{
case 704:
return KeyEvent.KEYCODE_DPAD_LEFT;
case 705:
return KeyEvent.KEYCODE_DPAD_RIGHT;
case 706:
return KeyEvent.KEYCODE_DPAD_UP;
case 707:
return KeyEvent.KEYCODE_DPAD_DOWN;
}
}
// Past here we can fixup the keycode and potentially trigger
// another special case so we need to remember what keycode we're using
int keyCode = event.getKeyCode();
// This is a hack for (at least) the "Tablet Remote" app
// which sends BACK with META_ALT_ON instead of KEYCODE_BUTTON_B
if (keyCode == KeyEvent.KEYCODE_BACK &&
!event.hasNoModifiers() &&
(event.getFlags() & KeyEvent.FLAG_SOFT_KEYBOARD) != 0)
{
keyCode = KeyEvent.KEYCODE_BUTTON_B;
}
if (keyCode == KeyEvent.KEYCODE_BUTTON_START ||
keyCode == KeyEvent.KEYCODE_MENU) {
// Ensure that we never use back as start if we have a real start
context.backIsStart = false;
}
else if (keyCode == KeyEvent.KEYCODE_BUTTON_SELECT) {
// Don't use mode as select if we have a select
context.modeIsSelect = false;
}
else if (context.backIsStart && keyCode == KeyEvent.KEYCODE_BACK) {
// Emulate the start button with back
return KeyEvent.KEYCODE_BUTTON_START;
}
else if (context.modeIsSelect && keyCode == KeyEvent.KEYCODE_BUTTON_MODE) {
// Emulate the select button with mode
return KeyEvent.KEYCODE_BUTTON_SELECT;
}
else if (context.searchIsMode && keyCode == KeyEvent.KEYCODE_SEARCH) {
// Emulate the mode button with search
return KeyEvent.KEYCODE_BUTTON_MODE;
}
return keyCode;
}
private int handleFlipFaceButtons(int keyCode) {
switch (keyCode) {
case KeyEvent.KEYCODE_BUTTON_A:
return KeyEvent.KEYCODE_BUTTON_B;
case KeyEvent.KEYCODE_BUTTON_B:
return KeyEvent.KEYCODE_BUTTON_A;
case KeyEvent.KEYCODE_BUTTON_X:
return KeyEvent.KEYCODE_BUTTON_Y;
case KeyEvent.KEYCODE_BUTTON_Y:
return KeyEvent.KEYCODE_BUTTON_X;
default:
return keyCode;
}
}
private Vector2d populateCachedVector(float x, float y) {
// Reinitialize our cached Vector2d object
inputVector.initialize(x, y);
return inputVector;
}
private void handleDeadZone(Vector2d stickVector, float deadzoneRadius) {
if (stickVector.getMagnitude() <= deadzoneRadius) {
// Deadzone
stickVector.initialize(0, 0);
}
// We're not normalizing here because we let the computer handle the deadzones.
// Normalizing can make the deadzones larger than they should be after the computer also
// evaluates the deadzone.
}
private void handleAxisSet(InputDeviceContext context, float lsX, float lsY, float rsX,
float rsY, float lt, float rt, float hatX, float hatY) {
if (context.leftStickXAxis != -1 && context.leftStickYAxis != -1) {
Vector2d leftStickVector = populateCachedVector(lsX, lsY);
handleDeadZone(leftStickVector, context.leftStickDeadzoneRadius);
context.leftStickX = (short) (leftStickVector.getX() * 0x7FFE);
context.leftStickY = (short) (-leftStickVector.getY() * 0x7FFE);
}
if (context.rightStickXAxis != -1 && context.rightStickYAxis != -1) {
Vector2d rightStickVector = populateCachedVector(rsX, rsY);
handleDeadZone(rightStickVector, context.rightStickDeadzoneRadius);
context.rightStickX = (short) (rightStickVector.getX() * 0x7FFE);
context.rightStickY = (short) (-rightStickVector.getY() * 0x7FFE);
}
if (context.leftTriggerAxis != -1 && context.rightTriggerAxis != -1) {
// Android sends an initial 0 value for trigger axes even if the trigger
// should be negative when idle. After the first touch, the axes will go back
// to normal behavior, so ignore triggersIdleNegative for each trigger until
// first touch.
if (lt != 0) {
context.leftTriggerAxisUsed = true;
}
if (rt != 0) {
context.rightTriggerAxisUsed = true;
}
if (context.triggersIdleNegative) {
if (context.leftTriggerAxisUsed) {
lt = (lt + 1) / 2;
}
if (context.rightTriggerAxisUsed) {
rt = (rt + 1) / 2;
}
}
if (lt <= context.triggerDeadzone) {
lt = 0;
}
if (rt <= context.triggerDeadzone) {
rt = 0;
}
context.leftTrigger = (byte)(lt * 0xFF);
context.rightTrigger = (byte)(rt * 0xFF);
}
if (context.hatXAxis != -1 && context.hatYAxis != -1) {
context.inputMap &= ~(ControllerPacket.LEFT_FLAG | ControllerPacket.RIGHT_FLAG);
if (hatX < -0.5) {
context.inputMap |= ControllerPacket.LEFT_FLAG;
context.hatXAxisUsed = true;
}
else if (hatX > 0.5) {
context.inputMap |= ControllerPacket.RIGHT_FLAG;
context.hatXAxisUsed = true;
}
context.inputMap &= ~(ControllerPacket.UP_FLAG | ControllerPacket.DOWN_FLAG);
if (hatY < -0.5) {
context.inputMap |= ControllerPacket.UP_FLAG;
context.hatYAxisUsed = true;
}
else if (hatY > 0.5) {
context.inputMap |= ControllerPacket.DOWN_FLAG;
context.hatYAxisUsed = true;
}
}
sendControllerInputPacket(context);
}
// Normalize the given raw float value into a 0.0-1.0f range
private float normalizeRawValueWithRange(float value, InputDevice.MotionRange range) {
value = Math.max(value, range.getMin());
value = Math.min(value, range.getMax());
value -= range.getMin();
return value / range.getRange();
}
public boolean tryHandleTouchpadEvent(MotionEvent event) {
// Bail if this is not a touchpad event
if (event.getSource() != InputDevice.SOURCE_TOUCHPAD) {
return false;
}
// Only get a context if one already exists. We want to ensure we don't report non-gamepads.
InputDeviceContext context = inputDeviceContexts.get(event.getDeviceId());
if (context == null) {
return false;
}
byte touchType;
switch (event.getActionMasked()) {
case MotionEvent.ACTION_DOWN:
case MotionEvent.ACTION_POINTER_DOWN:
touchType = MoonBridge.LI_TOUCH_EVENT_DOWN;
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_POINTER_UP:
if ((event.getFlags() & MotionEvent.FLAG_CANCELED) != 0) {
touchType = MoonBridge.LI_TOUCH_EVENT_CANCEL;
}
else {
touchType = MoonBridge.LI_TOUCH_EVENT_UP;
}
break;
case MotionEvent.ACTION_MOVE:
touchType = MoonBridge.LI_TOUCH_EVENT_MOVE;
break;
case MotionEvent.ACTION_CANCEL:
touchType = MoonBridge.LI_TOUCH_EVENT_CANCEL;
break;
case MotionEvent.ACTION_BUTTON_PRESS:
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && event.getActionButton() == MotionEvent.BUTTON_PRIMARY) {
context.inputMap |= ControllerPacket.TOUCHPAD_FLAG;
sendControllerInputPacket(context);
return true;
}
return false;
case MotionEvent.ACTION_BUTTON_RELEASE:
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && event.getActionButton() == MotionEvent.BUTTON_PRIMARY) {
context.inputMap &= ~ControllerPacket.TOUCHPAD_FLAG;
sendControllerInputPacket(context);
return true;
}
return false;
default:
return false;
}
// If we don't have X and Y ranges, we can't process this event
if (context.touchpadXRange == null || context.touchpadYRange == null) {
return false;
}
float normalizedX = normalizeRawValueWithRange(event.getX(event.getActionIndex()), context.touchpadXRange);
float normalizedY = normalizeRawValueWithRange(event.getY(event.getActionIndex()), context.touchpadYRange);
float normalizedPressure = context.touchpadPressureRange != null ?
normalizeRawValueWithRange(event.getPressure(event.getActionIndex()), context.touchpadPressureRange)
: 0;
return conn.sendControllerTouchEvent((byte)context.controllerNumber, touchType,
event.getPointerId(event.getActionIndex()),
normalizedX, normalizedY, normalizedPressure) != MoonBridge.LI_ERR_UNSUPPORTED;
}
public boolean handleMotionEvent(MotionEvent event) {
InputDeviceContext context = getContextForEvent(event);
if (context == null) {
return true;
}
float lsX = 0, lsY = 0, rsX = 0, rsY = 0, rt = 0, lt = 0, hatX = 0, hatY = 0;
// We purposefully ignore the historical values in the motion event as it makes
// the controller feel sluggish for some users.
if (context.leftStickXAxis != -1 && context.leftStickYAxis != -1) {
lsX = event.getAxisValue(context.leftStickXAxis);
lsY = event.getAxisValue(context.leftStickYAxis);
}
if (context.rightStickXAxis != -1 && context.rightStickYAxis != -1) {
rsX = event.getAxisValue(context.rightStickXAxis);
rsY = event.getAxisValue(context.rightStickYAxis);
}
if (context.leftTriggerAxis != -1 && context.rightTriggerAxis != -1) {
lt = event.getAxisValue(context.leftTriggerAxis);
rt = event.getAxisValue(context.rightTriggerAxis);
}
if (context.hatXAxis != -1 && context.hatYAxis != -1) {
hatX = event.getAxisValue(MotionEvent.AXIS_HAT_X);
hatY = event.getAxisValue(MotionEvent.AXIS_HAT_Y);
}
handleAxisSet(context, lsX, lsY, rsX, rsY, lt, rt, hatX, hatY);
return true;
}
private short scaleRawStickAxis(float stickValue) {
return (short)Math.pow(stickValue, 3);
}
private void sendEmulatedMouseEvent(short x, short y) {
Vector2d vector = new Vector2d();
vector.initialize(x, y);
vector.scalarMultiply(1 / 32766.0f);
vector.scalarMultiply(4);
if (vector.getMagnitude() > 0) {
// Move faster as the stick is pressed further from center
vector.scalarMultiply(Math.pow(vector.getMagnitude(), 2));
if (vector.getMagnitude() >= 1) {
conn.sendMouseMove((short)vector.getX(), (short)-vector.getY());
}
}
}
@TargetApi(31)
private boolean hasDualAmplitudeControlledRumbleVibrators(VibratorManager vm) {
int[] vibratorIds = vm.getVibratorIds();
// There must be exactly 2 vibrators on this device
if (vibratorIds.length != 2) {
return false;
}
// Both vibrators must have amplitude control
for (int vid : vibratorIds) {
if (!vm.getVibrator(vid).hasAmplitudeControl()) {
return false;
}
}
return true;
}
// This must only be called if hasDualAmplitudeControlledRumbleVibrators() is true!
@TargetApi(31)
private void rumbleDualVibrators(VibratorManager vm, short lowFreqMotor, short highFreqMotor) {
// Normalize motor values to 0-255 amplitudes for VibrationManager
highFreqMotor = (short)((highFreqMotor >> 8) & 0xFF);
lowFreqMotor = (short)((lowFreqMotor >> 8) & 0xFF);
// If they're both zero, we can just call cancel().
if (lowFreqMotor == 0 && highFreqMotor == 0) {
vm.cancel();
return;
}
// There's no documentation that states that vibrators for FF_RUMBLE input devices will
// always be enumerated in this order, but it seems consistent between Xbox Series X (USB),
// PS3 (USB), and PS4 (USB+BT) controllers on Android 12 Beta 3.
int[] vibratorIds = vm.getVibratorIds();
int[] vibratorAmplitudes = new int[] { highFreqMotor, lowFreqMotor };
CombinedVibration.ParallelCombination combo = CombinedVibration.startParallel();
for (int i = 0; i < vibratorIds.length; i++) {
// It's illegal to create a VibrationEffect with an amplitude of 0.
// Simply excluding that vibrator from our ParallelCombination will turn it off.
if (vibratorAmplitudes[i] != 0) {
combo.addVibrator(vibratorIds[i], VibrationEffect.createOneShot(60000, vibratorAmplitudes[i]));
}
}
VibrationAttributes.Builder vibrationAttributes = new VibrationAttributes.Builder();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
vibrationAttributes.setUsage(VibrationAttributes.USAGE_MEDIA);
}
vm.vibrate(combo.combine(), vibrationAttributes.build());
}
@TargetApi(31)
private boolean hasQuadAmplitudeControlledRumbleVibrators(VibratorManager vm) {
int[] vibratorIds = vm.getVibratorIds();
// There must be exactly 4 vibrators on this device
if (vibratorIds.length != 4) {
return false;
}
// All vibrators must have amplitude control
for (int vid : vibratorIds) {
if (!vm.getVibrator(vid).hasAmplitudeControl()) {
return false;
}
}
return true;
}
// This must only be called if hasQuadAmplitudeControlledRumbleVibrators() is true!
@TargetApi(31)
private void rumbleQuadVibrators(VibratorManager vm, short lowFreqMotor, short highFreqMotor, short leftTrigger, short rightTrigger) {
// Normalize motor values to 0-255 amplitudes for VibrationManager
highFreqMotor = (short)((highFreqMotor >> 8) & 0xFF);
lowFreqMotor = (short)((lowFreqMotor >> 8) & 0xFF);
leftTrigger = (short)((leftTrigger >> 8) & 0xFF);
rightTrigger = (short)((rightTrigger >> 8) & 0xFF);
// If they're all zero, we can just call cancel().
if (lowFreqMotor == 0 && highFreqMotor == 0 && leftTrigger == 0 && rightTrigger == 0) {
vm.cancel();
return;
}
// This is a guess based upon the behavior of FF_RUMBLE, but untested due to lack of Linux
// support for trigger rumble!
int[] vibratorIds = vm.getVibratorIds();
int[] vibratorAmplitudes = new int[] { highFreqMotor, lowFreqMotor, leftTrigger, rightTrigger };
CombinedVibration.ParallelCombination combo = CombinedVibration.startParallel();
for (int i = 0; i < vibratorIds.length; i++) {
// It's illegal to create a VibrationEffect with an amplitude of 0.
// Simply excluding that vibrator from our ParallelCombination will turn it off.
if (vibratorAmplitudes[i] != 0) {
combo.addVibrator(vibratorIds[i], VibrationEffect.createOneShot(60000, vibratorAmplitudes[i]));
}
}
VibrationAttributes.Builder vibrationAttributes = new VibrationAttributes.Builder();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
vibrationAttributes.setUsage(VibrationAttributes.USAGE_MEDIA);
}
vm.vibrate(combo.combine(), vibrationAttributes.build());
}
private void rumbleSingleVibrator(Vibrator vibrator, short lowFreqMotor, short highFreqMotor) {
// Since we can only use a single amplitude value, compute the desired amplitude
// by taking 80% of the big motor and 33% of the small motor, then capping to 255.
// NB: This value is now 0-255 as required by VibrationEffect.
short lowFreqMotorMSB = (short)((lowFreqMotor >> 8) & 0xFF);
short highFreqMotorMSB = (short)((highFreqMotor >> 8) & 0xFF);
int simulatedAmplitude = Math.min(255, (int)((lowFreqMotorMSB * 0.80) + (highFreqMotorMSB * 0.33)));
if (simulatedAmplitude == 0) {
// This case is easy - just cancel the current effect and get out.
// NB: We cannot simply check lowFreqMotor == highFreqMotor == 0
// because our simulatedAmplitude could be 0 even though our inputs
// are not (ex: lowFreqMotor == 0 && highFreqMotor == 1).
vibrator.cancel();
return;
}
// Attempt to use amplitude-based control if we're on Oreo and the device
// supports amplitude-based vibration control.
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
if (vibrator.hasAmplitudeControl()) {
VibrationEffect effect = VibrationEffect.createOneShot(60000, simulatedAmplitude);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
VibrationAttributes vibrationAttributes = new VibrationAttributes.Builder()
.setUsage(VibrationAttributes.USAGE_MEDIA)
.build();
vibrator.vibrate(effect, vibrationAttributes);
}
else {
AudioAttributes audioAttributes = new AudioAttributes.Builder()
.setUsage(AudioAttributes.USAGE_GAME)
.build();
vibrator.vibrate(effect, audioAttributes);
}
return;
}
}
// If we reach this point, we don't have amplitude controls available, so
// we must emulate it by PWMing the vibration. Ick.
long pwmPeriod = 20;
long onTime = (long)((simulatedAmplitude / 255.0) * pwmPeriod);
long offTime = pwmPeriod - onTime;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
VibrationAttributes vibrationAttributes = new VibrationAttributes.Builder()
.setUsage(VibrationAttributes.USAGE_MEDIA)
.build();
vibrator.vibrate(VibrationEffect.createWaveform(new long[]{0, onTime, offTime}, 0), vibrationAttributes);
}
else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
AudioAttributes audioAttributes = new AudioAttributes.Builder()
.setUsage(AudioAttributes.USAGE_GAME)
.build();
vibrator.vibrate(new long[]{0, onTime, offTime}, 0, audioAttributes);
}
else {
vibrator.vibrate(new long[]{0, onTime, offTime}, 0);
}
}
public void handleRumble(short controllerNumber, short lowFreqMotor, short highFreqMotor) {
boolean foundMatchingDevice = false;
boolean vibrated = false;
for (int i = 0; i < inputDeviceContexts.size(); i++) {
InputDeviceContext deviceContext = inputDeviceContexts.valueAt(i);
if (deviceContext.controllerNumber == controllerNumber) {
foundMatchingDevice = true;
deviceContext.lowFreqMotor = lowFreqMotor;
deviceContext.highFreqMotor = highFreqMotor;
// Prefer the documented Android 12 rumble API which can handle dual vibrators on PS/Xbox controllers
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && deviceContext.vibratorManager != null) {
vibrated = true;
if (deviceContext.quadVibrators) {
rumbleQuadVibrators(deviceContext.vibratorManager,
deviceContext.lowFreqMotor, deviceContext.highFreqMotor,
deviceContext.leftTriggerMotor, deviceContext.rightTriggerMotor);
}
else {
rumbleDualVibrators(deviceContext.vibratorManager,
deviceContext.lowFreqMotor, deviceContext.highFreqMotor);
}
}
// On Shield devices, we can use their special API to rumble Shield controllers
else if (sceManager.rumble(deviceContext.inputDevice, deviceContext.lowFreqMotor, deviceContext.highFreqMotor)) {
vibrated = true;
}
// If all else fails, we have to try the old Vibrator API
else if (deviceContext.vibrator != null) {
vibrated = true;
rumbleSingleVibrator(deviceContext.vibrator, deviceContext.lowFreqMotor, deviceContext.highFreqMotor);
}
}
}
for (int i = 0; i < usbDeviceContexts.size(); i++) {
UsbDeviceContext deviceContext = usbDeviceContexts.valueAt(i);
if (deviceContext.controllerNumber == controllerNumber) {
foundMatchingDevice = vibrated = true;
deviceContext.device.rumble(lowFreqMotor, highFreqMotor);
}
}
// We may decide to rumble the device for player 1
if (controllerNumber == 0) {
// If we didn't find a matching device, it must be the on-screen
// controls that triggered the rumble. Vibrate the device if
// the user has requested that behavior.
if (!foundMatchingDevice && prefConfig.onscreenController && !prefConfig.onlyL3R3 && prefConfig.vibrateOsc) {
rumbleSingleVibrator(deviceVibrator, lowFreqMotor, highFreqMotor);
}
else if (foundMatchingDevice && !vibrated && prefConfig.vibrateFallbackToDevice) {
// We found a device to vibrate but it didn't have rumble support. The user
// has requested us to vibrate the device in this case.
rumbleSingleVibrator(deviceVibrator, lowFreqMotor, highFreqMotor);
}
}
}
public void handleRumbleTriggers(short controllerNumber, short leftTrigger, short rightTrigger) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
for (int i = 0; i < inputDeviceContexts.size(); i++) {
InputDeviceContext deviceContext = inputDeviceContexts.valueAt(i);
if (deviceContext.controllerNumber == controllerNumber) {
deviceContext.leftTriggerMotor = leftTrigger;
deviceContext.rightTriggerMotor = rightTrigger;
if (deviceContext.quadVibrators) {
rumbleQuadVibrators(deviceContext.vibratorManager,
deviceContext.lowFreqMotor, deviceContext.highFreqMotor,
deviceContext.leftTriggerMotor, deviceContext.rightTriggerMotor);
}
}
}
}
for (int i = 0; i < usbDeviceContexts.size(); i++) {
UsbDeviceContext deviceContext = usbDeviceContexts.valueAt(i);
if (deviceContext.controllerNumber == controllerNumber) {
deviceContext.device.rumbleTriggers(leftTrigger, rightTrigger);
}
}
}
public void handleSetMotionEventState(final short controllerNumber, final byte motionType, short reportRateHz) {
// Report rate is restricted to <= 200 Hz without the HIGH_SAMPLING_RATE_SENSORS permission
reportRateHz = (short) Math.min(200, reportRateHz);
SensorEventListener newSensorListener = new SensorEventListener() {
private float[] lastValues = new float[3];
@Override
public void onSensorChanged(SensorEvent sensorEvent) {
// Android will invoke our callback any time we get a new reading,
// even if the values are the same as last time. Don't report a
// duplicate set of values to save bandwidth.
if (sensorEvent.values[0] == lastValues[0] &&
sensorEvent.values[1] == lastValues[1] &&
sensorEvent.values[2] == lastValues[2]) {
return;
}
else {
lastValues[0] = sensorEvent.values[0];
lastValues[1] = sensorEvent.values[1];
lastValues[2] = sensorEvent.values[2];
}
if (motionType == MoonBridge.LI_MOTION_TYPE_GYRO) {
// Convert from rad/s to deg/s
conn.sendControllerMotionEvent((byte) controllerNumber,
motionType,
sensorEvent.values[0] * 57.2957795f,
sensorEvent.values[1] * 57.2957795f,
sensorEvent.values[2] * 57.2957795f);
}
else {
// Pass m/s^2 directly without conversion
conn.sendControllerMotionEvent((byte) controllerNumber,
motionType,
sensorEvent.values[0],
sensorEvent.values[1],
sensorEvent.values[2]);
}
}
@Override
public void onAccuracyChanged(Sensor sensor, int accuracy) {}
};
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
for (int i = 0; i < inputDeviceContexts.size(); i++) {
InputDeviceContext deviceContext = inputDeviceContexts.valueAt(i);
if (deviceContext.controllerNumber == controllerNumber) {
SensorManager sm = deviceContext.inputDevice.getSensorManager();
switch (motionType) {
case MoonBridge.LI_MOTION_TYPE_ACCEL:
if (deviceContext.accelListener != null) {
sm.unregisterListener(deviceContext.accelListener);
deviceContext.accelListener = null;
}
// Enable the accelerometer if requested
Sensor accelSensor = sm.getDefaultSensor(Sensor.TYPE_ACCELEROMETER, false);
if (reportRateHz != 0 && accelSensor != null) {
sm.registerListener(newSensorListener, accelSensor, 1000000 / reportRateHz);
deviceContext.accelListener = newSensorListener;
}
break;
case MoonBridge.LI_MOTION_TYPE_GYRO:
if (deviceContext.gyroListener != null) {
sm.unregisterListener(deviceContext.gyroListener);
deviceContext.gyroListener = null;
}
// Enable the gyroscope if requested
Sensor gyroSensor = sm.getDefaultSensor(Sensor.TYPE_GYROSCOPE, false);
if (reportRateHz != 0 && gyroSensor != null) {
sm.registerListener(newSensorListener, gyroSensor,1000000 / reportRateHz);
deviceContext.gyroListener = newSensorListener;
}
break;
}
break;
}
}
}
}
public void handleSetControllerLED(short controllerNumber, byte r, byte g, byte b) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
for (int i = 0; i < inputDeviceContexts.size(); i++) {
InputDeviceContext deviceContext = inputDeviceContexts.valueAt(i);
if (deviceContext.controllerNumber == controllerNumber) {
// Create a new light session if one doesn't already exist
if (deviceContext.lightsSession == null) {
deviceContext.lightsSession = deviceContext.inputDevice.getLightsManager().openSession();
}
// Convert the RGB components into the integer value that LightState uses
int argbValue = 0xFF000000 | ((r << 16) & 0xFF0000) | ((g << 8) & 0xFF00) | (b & 0xFF);
LightState lightState = new LightState.Builder().setColor(argbValue).build();
// Set the RGB value for each RGB-controllable LED on the device
LightsRequest.Builder lightsRequestBuilder = new LightsRequest.Builder();
for (Light light : deviceContext.inputDevice.getLightsManager().getLights()) {
if (light.hasRgbControl()) {
lightsRequestBuilder.addLight(light, lightState);
}
}
// Apply the LED changes
deviceContext.lightsSession.requestLights(lightsRequestBuilder.build());
}
}
}
}
public boolean handleButtonUp(KeyEvent event) {
InputDeviceContext context = getContextForEvent(event);
if (context == null) {
return true;
}
int keyCode = handleRemapping(context, event);
if (keyCode < 0) {
return (keyCode == REMAP_CONSUME);
}
if (prefConfig.flipFaceButtons) {
keyCode = handleFlipFaceButtons(keyCode);
}
// If the button hasn't been down long enough, sleep for a bit before sending the up event
// This allows "instant" button presses (like OUYA's virtual menu button) to work. This
// path should not be triggered during normal usage.
int buttonDownTime = (int)(event.getEventTime() - event.getDownTime());
if (buttonDownTime < ControllerHandler.MINIMUM_BUTTON_DOWN_TIME_MS)
{
// Since our sleep time is so short (<= 25 ms), it shouldn't cause a problem doing this
// in the UI thread.
try {
Thread.sleep(ControllerHandler.MINIMUM_BUTTON_DOWN_TIME_MS - buttonDownTime);
} catch (InterruptedException e) {
e.printStackTrace();
// InterruptedException clears the thread's interrupt status. Since we can't
// handle that here, we will re-interrupt the thread to set the interrupt
// status back to true.
Thread.currentThread().interrupt();
}
}
switch (keyCode) {
case KeyEvent.KEYCODE_BUTTON_MODE:
context.inputMap &= ~ControllerPacket.SPECIAL_BUTTON_FLAG;
break;
case KeyEvent.KEYCODE_BUTTON_START:
case KeyEvent.KEYCODE_MENU:
// Sometimes we'll get a spurious key up event on controller disconnect.
// Make sure it's real by checking that the key is actually down before taking
// any action.
if ((context.inputMap & ControllerPacket.PLAY_FLAG) != 0 &&
event.getEventTime() - context.startDownTime > ControllerHandler.START_DOWN_TIME_MOUSE_MODE_MS &&
prefConfig.mouseEmulation) {
context.toggleMouseEmulation();
}
context.inputMap &= ~ControllerPacket.PLAY_FLAG;
break;
case KeyEvent.KEYCODE_BACK:
case KeyEvent.KEYCODE_BUTTON_SELECT:
context.inputMap &= ~ControllerPacket.BACK_FLAG;
break;
case KeyEvent.KEYCODE_DPAD_LEFT:
if (context.hatXAxisUsed) {
// Suppress this duplicate event if we have a hat
return true;
}
context.inputMap &= ~ControllerPacket.LEFT_FLAG;
break;
case KeyEvent.KEYCODE_DPAD_RIGHT:
if (context.hatXAxisUsed) {
// Suppress this duplicate event if we have a hat
return true;
}
context.inputMap &= ~ControllerPacket.RIGHT_FLAG;
break;
case KeyEvent.KEYCODE_DPAD_UP:
if (context.hatYAxisUsed) {
// Suppress this duplicate event if we have a hat
return true;
}
context.inputMap &= ~ControllerPacket.UP_FLAG;
break;
case KeyEvent.KEYCODE_DPAD_DOWN:
if (context.hatYAxisUsed) {
// Suppress this duplicate event if we have a hat
return true;
}
context.inputMap &= ~ControllerPacket.DOWN_FLAG;
break;
case KeyEvent.KEYCODE_DPAD_UP_LEFT:
if (context.hatXAxisUsed && context.hatYAxisUsed) {
// Suppress this duplicate event if we have a hat
return true;
}
context.inputMap &= ~(ControllerPacket.UP_FLAG | ControllerPacket.LEFT_FLAG);
break;
case KeyEvent.KEYCODE_DPAD_UP_RIGHT:
if (context.hatXAxisUsed && context.hatYAxisUsed) {
// Suppress this duplicate event if we have a hat
return true;
}
context.inputMap &= ~(ControllerPacket.UP_FLAG | ControllerPacket.RIGHT_FLAG);
break;
case KeyEvent.KEYCODE_DPAD_DOWN_LEFT:
if (context.hatXAxisUsed && context.hatYAxisUsed) {
// Suppress this duplicate event if we have a hat
return true;
}
context.inputMap &= ~(ControllerPacket.DOWN_FLAG | ControllerPacket.LEFT_FLAG);
break;
case KeyEvent.KEYCODE_DPAD_DOWN_RIGHT:
if (context.hatXAxisUsed && context.hatYAxisUsed) {
// Suppress this duplicate event if we have a hat
return true;
}
context.inputMap &= ~(ControllerPacket.DOWN_FLAG | ControllerPacket.RIGHT_FLAG);
break;
case KeyEvent.KEYCODE_BUTTON_B:
context.inputMap &= ~ControllerPacket.B_FLAG;
break;
case KeyEvent.KEYCODE_DPAD_CENTER:
case KeyEvent.KEYCODE_BUTTON_A:
context.inputMap &= ~ControllerPacket.A_FLAG;
break;
case KeyEvent.KEYCODE_BUTTON_X:
context.inputMap &= ~ControllerPacket.X_FLAG;
break;
case KeyEvent.KEYCODE_BUTTON_Y:
context.inputMap &= ~ControllerPacket.Y_FLAG;
break;
case KeyEvent.KEYCODE_BUTTON_L1:
context.inputMap &= ~ControllerPacket.LB_FLAG;
context.lastLbUpTime = event.getEventTime();
break;
case KeyEvent.KEYCODE_BUTTON_R1:
context.inputMap &= ~ControllerPacket.RB_FLAG;
context.lastRbUpTime = event.getEventTime();
break;
case KeyEvent.KEYCODE_BUTTON_THUMBL:
context.inputMap &= ~ControllerPacket.LS_CLK_FLAG;
break;
case KeyEvent.KEYCODE_BUTTON_THUMBR:
context.inputMap &= ~ControllerPacket.RS_CLK_FLAG;
break;
case KeyEvent.KEYCODE_MEDIA_RECORD: // Xbox Series X Share button
context.inputMap &= ~ControllerPacket.MISC_FLAG;
break;
case KeyEvent.KEYCODE_BUTTON_1: // PS4/PS5 touchpad button (prior to 4.10)
context.inputMap &= ~ControllerPacket.TOUCHPAD_FLAG;
break;
case KeyEvent.KEYCODE_BUTTON_L2:
if (context.leftTriggerAxisUsed) {
// Suppress this digital event if an analog trigger is active
return true;
}
context.leftTrigger = 0;
break;
case KeyEvent.KEYCODE_BUTTON_R2:
if (context.rightTriggerAxisUsed) {
// Suppress this digital event if an analog trigger is active
return true;
}
context.rightTrigger = 0;
break;
case KeyEvent.KEYCODE_UNKNOWN:
// Paddles aren't mapped in any of the Android key layout files,
// so we need to handle the evdev key codes directly.
if (context.hasPaddles) {
switch (event.getScanCode()) {
case 0x2c4: // BTN_TRIGGER_HAPPY5
context.inputMap &= ~ControllerPacket.PADDLE1_FLAG;
break;
case 0x2c5: // BTN_TRIGGER_HAPPY6
context.inputMap &= ~ControllerPacket.PADDLE2_FLAG;
break;
case 0x2c6: // BTN_TRIGGER_HAPPY7
context.inputMap &= ~ControllerPacket.PADDLE3_FLAG;
break;
case 0x2c7: // BTN_TRIGGER_HAPPY8
context.inputMap &= ~ControllerPacket.PADDLE4_FLAG;
break;
default:
return false;
}
}
else {
return false;
}
break;
default:
return false;
}
// Check if we're emulating the select button
if ((context.emulatingButtonFlags & ControllerHandler.EMULATING_SELECT) != 0)
{
// If either start or LB is up, select comes up too
if ((context.inputMap & ControllerPacket.PLAY_FLAG) == 0 ||
(context.inputMap & ControllerPacket.LB_FLAG) == 0)
{
context.inputMap &= ~ControllerPacket.BACK_FLAG;
context.emulatingButtonFlags &= ~ControllerHandler.EMULATING_SELECT;
}
}
// Check if we're emulating the special button
if ((context.emulatingButtonFlags & ControllerHandler.EMULATING_SPECIAL) != 0)
{
// If either start or select and RB is up, the special button comes up too
if ((context.inputMap & ControllerPacket.PLAY_FLAG) == 0 ||
((context.inputMap & ControllerPacket.BACK_FLAG) == 0 &&
(context.inputMap & ControllerPacket.RB_FLAG) == 0))
{
context.inputMap &= ~ControllerPacket.SPECIAL_BUTTON_FLAG;
context.emulatingButtonFlags &= ~ControllerHandler.EMULATING_SPECIAL;
}
}
sendControllerInputPacket(context);
if (context.pendingExit && context.inputMap == 0) {
// All buttons from the quit combo are lifted. Finish the activity now.
activityContext.finish();
}
return true;
}
public boolean handleButtonDown(KeyEvent event) {
InputDeviceContext context = getContextForEvent(event);
if (context == null) {
return true;
}
int keyCode = handleRemapping(context, event);
if (keyCode < 0) {
return (keyCode == REMAP_CONSUME);
}
if (prefConfig.flipFaceButtons) {
keyCode = handleFlipFaceButtons(keyCode);
}
switch (keyCode) {
case KeyEvent.KEYCODE_BUTTON_MODE:
context.hasMode = true;
context.inputMap |= ControllerPacket.SPECIAL_BUTTON_FLAG;
break;
case KeyEvent.KEYCODE_BUTTON_START:
case KeyEvent.KEYCODE_MENU:
if (event.getRepeatCount() == 0) {
context.startDownTime = event.getEventTime();
}
context.inputMap |= ControllerPacket.PLAY_FLAG;
break;
case KeyEvent.KEYCODE_BACK:
case KeyEvent.KEYCODE_BUTTON_SELECT:
context.hasSelect = true;
context.inputMap |= ControllerPacket.BACK_FLAG;
break;
case KeyEvent.KEYCODE_DPAD_LEFT:
if (context.hatXAxisUsed) {
// Suppress this duplicate event if we have a hat
return true;
}
context.inputMap |= ControllerPacket.LEFT_FLAG;
break;
case KeyEvent.KEYCODE_DPAD_RIGHT:
if (context.hatXAxisUsed) {
// Suppress this duplicate event if we have a hat
return true;
}
context.inputMap |= ControllerPacket.RIGHT_FLAG;
break;
case KeyEvent.KEYCODE_DPAD_UP:
if (context.hatYAxisUsed) {
// Suppress this duplicate event if we have a hat
return true;
}
context.inputMap |= ControllerPacket.UP_FLAG;
break;
case KeyEvent.KEYCODE_DPAD_DOWN:
if (context.hatYAxisUsed) {
// Suppress this duplicate event if we have a hat
return true;
}
context.inputMap |= ControllerPacket.DOWN_FLAG;
break;
case KeyEvent.KEYCODE_DPAD_UP_LEFT:
if (context.hatXAxisUsed && context.hatYAxisUsed) {
// Suppress this duplicate event if we have a hat
return true;
}
context.inputMap |= ControllerPacket.UP_FLAG | ControllerPacket.LEFT_FLAG;
break;
case KeyEvent.KEYCODE_DPAD_UP_RIGHT:
if (context.hatXAxisUsed && context.hatYAxisUsed) {
// Suppress this duplicate event if we have a hat
return true;
}
context.inputMap |= ControllerPacket.UP_FLAG | ControllerPacket.RIGHT_FLAG;
break;
case KeyEvent.KEYCODE_DPAD_DOWN_LEFT:
if (context.hatXAxisUsed && context.hatYAxisUsed) {
// Suppress this duplicate event if we have a hat
return true;
}
context.inputMap |= ControllerPacket.DOWN_FLAG | ControllerPacket.LEFT_FLAG;
break;
case KeyEvent.KEYCODE_DPAD_DOWN_RIGHT:
if (context.hatXAxisUsed && context.hatYAxisUsed) {
// Suppress this duplicate event if we have a hat
return true;
}
context.inputMap |= ControllerPacket.DOWN_FLAG | ControllerPacket.RIGHT_FLAG;
break;
case KeyEvent.KEYCODE_BUTTON_B:
context.inputMap |= ControllerPacket.B_FLAG;
break;
case KeyEvent.KEYCODE_DPAD_CENTER:
case KeyEvent.KEYCODE_BUTTON_A:
context.inputMap |= ControllerPacket.A_FLAG;
break;
case KeyEvent.KEYCODE_BUTTON_X:
context.inputMap |= ControllerPacket.X_FLAG;
break;
case KeyEvent.KEYCODE_BUTTON_Y:
context.inputMap |= ControllerPacket.Y_FLAG;
break;
case KeyEvent.KEYCODE_BUTTON_L1:
context.inputMap |= ControllerPacket.LB_FLAG;
break;
case KeyEvent.KEYCODE_BUTTON_R1:
context.inputMap |= ControllerPacket.RB_FLAG;
break;
case KeyEvent.KEYCODE_BUTTON_THUMBL:
context.inputMap |= ControllerPacket.LS_CLK_FLAG;
break;
case KeyEvent.KEYCODE_BUTTON_THUMBR:
context.inputMap |= ControllerPacket.RS_CLK_FLAG;
break;
case KeyEvent.KEYCODE_MEDIA_RECORD: // Xbox Series X Share button
context.inputMap |= ControllerPacket.MISC_FLAG;
break;
case KeyEvent.KEYCODE_BUTTON_1: // PS4/PS5 touchpad button (prior to 4.10)
context.inputMap |= ControllerPacket.TOUCHPAD_FLAG;
break;
case KeyEvent.KEYCODE_BUTTON_L2:
if (context.leftTriggerAxisUsed) {
// Suppress this digital event if an analog trigger is active
return true;
}
context.leftTrigger = (byte)0xFF;
break;
case KeyEvent.KEYCODE_BUTTON_R2:
if (context.rightTriggerAxisUsed) {
// Suppress this digital event if an analog trigger is active
return true;
}
context.rightTrigger = (byte)0xFF;
break;
case KeyEvent.KEYCODE_UNKNOWN:
// Paddles aren't mapped in any of the Android key layout files,
// so we need to handle the evdev key codes directly.
if (context.hasPaddles) {
switch (event.getScanCode()) {
case 0x2c4: // BTN_TRIGGER_HAPPY5
context.inputMap |= ControllerPacket.PADDLE1_FLAG;
break;
case 0x2c5: // BTN_TRIGGER_HAPPY6
context.inputMap |= ControllerPacket.PADDLE2_FLAG;
break;
case 0x2c6: // BTN_TRIGGER_HAPPY7
context.inputMap |= ControllerPacket.PADDLE3_FLAG;
break;
case 0x2c7: // BTN_TRIGGER_HAPPY8
context.inputMap |= ControllerPacket.PADDLE4_FLAG;
break;
default:
return false;
}
}
else {
return false;
}
break;
default:
return false;
}
// Start+Back+LB+RB is the quit combo
if (context.inputMap == (ControllerPacket.BACK_FLAG | ControllerPacket.PLAY_FLAG |
ControllerPacket.LB_FLAG | ControllerPacket.RB_FLAG)) {
// Wait for the combo to lift and then finish the activity
context.pendingExit = true;
}
// Start+LB acts like select for controllers with one button
if (!context.hasSelect) {
if (context.inputMap == (ControllerPacket.PLAY_FLAG | ControllerPacket.LB_FLAG) ||
(context.inputMap == ControllerPacket.PLAY_FLAG &&
event.getEventTime() - context.lastLbUpTime <= MAXIMUM_BUMPER_UP_DELAY_MS))
{
context.inputMap &= ~(ControllerPacket.PLAY_FLAG | ControllerPacket.LB_FLAG);
context.inputMap |= ControllerPacket.BACK_FLAG;
context.emulatingButtonFlags |= ControllerHandler.EMULATING_SELECT;
}
}
// If there is a physical select button, we'll use Start+Select as the special button combo
// otherwise we'll use Start+RB.
if (!context.hasMode) {
if (context.hasSelect) {
if (context.inputMap == (ControllerPacket.PLAY_FLAG | ControllerPacket.BACK_FLAG)) {
context.inputMap &= ~(ControllerPacket.PLAY_FLAG | ControllerPacket.BACK_FLAG);
context.inputMap |= ControllerPacket.SPECIAL_BUTTON_FLAG;
context.emulatingButtonFlags |= ControllerHandler.EMULATING_SPECIAL;
}
}
else {
if (context.inputMap == (ControllerPacket.PLAY_FLAG | ControllerPacket.RB_FLAG) ||
(context.inputMap == ControllerPacket.PLAY_FLAG &&
event.getEventTime() - context.lastRbUpTime <= MAXIMUM_BUMPER_UP_DELAY_MS))
{
context.inputMap &= ~(ControllerPacket.PLAY_FLAG | ControllerPacket.RB_FLAG);
context.inputMap |= ControllerPacket.SPECIAL_BUTTON_FLAG;
context.emulatingButtonFlags |= ControllerHandler.EMULATING_SPECIAL;
}
}
}
// We don't need to send repeat key down events, but the platform
// sends us events that claim to be repeats but they're from different
// devices, so we just send them all and deal with some duplicates.
sendControllerInputPacket(context);
return true;
}
public void reportOscState(int buttonFlags,
short leftStickX, short leftStickY,
short rightStickX, short rightStickY,
byte leftTrigger, byte rightTrigger) {
defaultContext.leftStickX = leftStickX;
defaultContext.leftStickY = leftStickY;
defaultContext.rightStickX = rightStickX;
defaultContext.rightStickY = rightStickY;
defaultContext.leftTrigger = leftTrigger;
defaultContext.rightTrigger = rightTrigger;
defaultContext.inputMap = buttonFlags;
sendControllerInputPacket(defaultContext);
}
@Override
public void reportControllerState(int controllerId, int buttonFlags,
float leftStickX, float leftStickY,
float rightStickX, float rightStickY,
float leftTrigger, float rightTrigger) {
GenericControllerContext context = usbDeviceContexts.get(controllerId);
if (context == null) {
return;
}
Vector2d leftStickVector = populateCachedVector(leftStickX, leftStickY);
handleDeadZone(leftStickVector, context.leftStickDeadzoneRadius);
context.leftStickX = (short) (leftStickVector.getX() * 0x7FFE);
context.leftStickY = (short) (-leftStickVector.getY() * 0x7FFE);
Vector2d rightStickVector = populateCachedVector(rightStickX, rightStickY);
handleDeadZone(rightStickVector, context.rightStickDeadzoneRadius);
context.rightStickX = (short) (rightStickVector.getX() * 0x7FFE);
context.rightStickY = (short) (-rightStickVector.getY() * 0x7FFE);
if (leftTrigger <= context.triggerDeadzone) {
leftTrigger = 0;
}
if (rightTrigger <= context.triggerDeadzone) {
rightTrigger = 0;
}
context.leftTrigger = (byte)(leftTrigger * 0xFF);
context.rightTrigger = (byte)(rightTrigger * 0xFF);
context.inputMap = buttonFlags;
sendControllerInputPacket(context);
}
@Override
public void deviceRemoved(AbstractController controller) {
UsbDeviceContext context = usbDeviceContexts.get(controller.getControllerId());
if (context != null) {
LimeLog.info("Removed controller: "+controller.getControllerId());
releaseControllerNumber(context);
context.destroy();
usbDeviceContexts.remove(controller.getControllerId());
}
}
@Override
public void deviceAdded(AbstractController controller) {
UsbDeviceContext context = createUsbDeviceContextForDevice(controller);
usbDeviceContexts.put(controller.getControllerId(), context);
}
class GenericControllerContext {
public int id;
public boolean external;
public int vendorId;
public int productId;
public float leftStickDeadzoneRadius;
public float rightStickDeadzoneRadius;
public float triggerDeadzone;
public boolean assignedControllerNumber;
public boolean reservedControllerNumber;
public short controllerNumber;
public int inputMap = 0;
public byte leftTrigger = 0x00;
public byte rightTrigger = 0x00;
public short rightStickX = 0x0000;
public short rightStickY = 0x0000;
public short leftStickX = 0x0000;
public short leftStickY = 0x0000;
public boolean mouseEmulationActive;
public int mouseEmulationLastInputMap;
public final int mouseEmulationReportPeriod = 50;
public final Runnable mouseEmulationRunnable = new Runnable() {
@Override
public void run() {
if (!mouseEmulationActive) {
return;
}
// Send mouse movement events from analog sticks
sendEmulatedMouseEvent(leftStickX, leftStickY);
sendEmulatedMouseEvent(rightStickX, rightStickY);
// Requeue the callback
handler.postDelayed(this, mouseEmulationReportPeriod);
}
};
public void toggleMouseEmulation() {
handler.removeCallbacks(mouseEmulationRunnable);
mouseEmulationActive = !mouseEmulationActive;
Toast.makeText(activityContext, "Mouse emulation is: " + (mouseEmulationActive ? "ON" : "OFF"), Toast.LENGTH_SHORT).show();
if (mouseEmulationActive) {
handler.postDelayed(mouseEmulationRunnable, mouseEmulationReportPeriod);
}
}
public void destroy() {
mouseEmulationActive = false;
handler.removeCallbacks(mouseEmulationRunnable);
}
public void sendControllerArrival() {}
}
class InputDeviceContext extends GenericControllerContext {
public String name;
public VibratorManager vibratorManager;
public Vibrator vibrator;
public boolean quadVibrators;
public short lowFreqMotor, highFreqMotor;
public short leftTriggerMotor, rightTriggerMotor;
public SensorEventListener gyroListener;
public SensorEventListener accelListener;
public InputDevice inputDevice;
public LightsManager.LightsSession lightsSession;
// These are BatteryState values, not Moonlight values
public int lastReportedBatteryStatus;
public float lastReportedBatteryCapacity;
public int leftStickXAxis = -1;
public int leftStickYAxis = -1;
public int rightStickXAxis = -1;
public int rightStickYAxis = -1;
public int leftTriggerAxis = -1;
public int rightTriggerAxis = -1;
public boolean triggersIdleNegative;
public boolean leftTriggerAxisUsed, rightTriggerAxisUsed;
public int hatXAxis = -1;
public int hatYAxis = -1;
public boolean hatXAxisUsed, hatYAxisUsed;
InputDevice.MotionRange touchpadXRange;
InputDevice.MotionRange touchpadYRange;
InputDevice.MotionRange touchpadPressureRange;
public boolean isNonStandardDualShock4;
public boolean usesLinuxGamepadStandardFaceButtons;
public boolean isNonStandardXboxBtController;
public boolean isServal;
public boolean backIsStart;
public boolean modeIsSelect;
public boolean searchIsMode;
public boolean ignoreBack;
public boolean hasJoystickAxes;
public boolean pendingExit;
public int emulatingButtonFlags = 0;
public boolean hasSelect;
public boolean hasMode;
public boolean hasPaddles;
public boolean hasShare;
// Used for OUYA bumper state tracking since they force all buttons
// up when the OUYA button goes down. We watch the last time we get
// a bumper up and compare that to our maximum delay when we receive
// a Start button press to see if we should activate one of our
// emulated button combos.
public long lastLbUpTime = 0;
public long lastRbUpTime = 0;
public long startDownTime = 0;
public final Runnable batteryStateUpdateRunnable = new Runnable() {
@Override
public void run() {
sendControllerBatteryPacket(InputDeviceContext.this);
// Requeue the callback
handler.postDelayed(this, BATTERY_RECHECK_INTERVAL_MS);
}
};
@Override
public void destroy() {
super.destroy();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && vibratorManager != null) {
vibratorManager.cancel();
}
else if (vibrator != null) {
vibrator.cancel();
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
if (gyroListener != null) {
inputDevice.getSensorManager().unregisterListener(gyroListener);
}
if (accelListener != null) {
inputDevice.getSensorManager().unregisterListener(accelListener);
}
if (lightsSession != null) {
lightsSession.close();
}
}
handler.removeCallbacks(batteryStateUpdateRunnable);
}
@Override
public void sendControllerArrival() {
// Below KitKat we can't get enough information to report controller details accurately
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) {
return;
}
byte type = MoonBridge.LI_CTYPE_UNKNOWN;
switch (inputDevice.getVendorId()) {
case 0x045e: // Microsoft
type = MoonBridge.LI_CTYPE_XBOX;
break;
case 0x054c: // Sony
type = MoonBridge.LI_CTYPE_PS;
break;
case 0x057e: // Nintendo
type = MoonBridge.LI_CTYPE_NINTENDO;
break;
default:
// Consult SDL's controller type list to see if it knows
type = MoonBridge.guessControllerType(inputDevice.getVendorId(), inputDevice.getProductId());
break;
}
int supportedButtonFlags = 0;
for (Map.Entry<Integer, Integer> entry : ANDROID_TO_LI_BUTTON_MAP.entrySet()) {
if (inputDevice.hasKeys(entry.getKey())[0]) {
supportedButtonFlags |= entry.getValue();
}
}
// Add non-standard button flags that may not be mapped in the Android kl file
if (hasPaddles) {
supportedButtonFlags |=
ControllerPacket.PADDLE1_FLAG |
ControllerPacket.PADDLE2_FLAG |
ControllerPacket.PADDLE3_FLAG |
ControllerPacket.PADDLE4_FLAG;
}
if (hasShare) {
supportedButtonFlags |= ControllerPacket.MISC_FLAG;
}
if (getMotionRangeForJoystickAxis(inputDevice, MotionEvent.AXIS_HAT_X) != null) {
supportedButtonFlags |= ControllerPacket.LEFT_FLAG | ControllerPacket.RIGHT_FLAG;
}
if (getMotionRangeForJoystickAxis(inputDevice, MotionEvent.AXIS_HAT_Y) != null) {
supportedButtonFlags |= ControllerPacket.UP_FLAG | ControllerPacket.DOWN_FLAG;
}
short capabilities = 0;
// Most of the advanced InputDevice capabilities came in Android S
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
if (hasQuadAmplitudeControlledRumbleVibrators(inputDevice.getVibratorManager())) {
capabilities |= MoonBridge.LI_CCAP_RUMBLE | MoonBridge.LI_CCAP_TRIGGER_RUMBLE;
}
else if (hasDualAmplitudeControlledRumbleVibrators(inputDevice.getVibratorManager())) {
capabilities |= MoonBridge.LI_CCAP_RUMBLE;
}
if (inputDevice.getSensorManager().getDefaultSensor(Sensor.TYPE_ACCELEROMETER) != null) {
capabilities |= MoonBridge.LI_CCAP_ACCEL;
}
if (inputDevice.getSensorManager().getDefaultSensor(Sensor.TYPE_GYROSCOPE) != null) {
capabilities |= MoonBridge.LI_CCAP_GYRO;
}
if (inputDevice.getBatteryState().isPresent()) {
capabilities |= MoonBridge.LI_CCAP_BATTERY_STATE;
}
for (Light light : inputDevice.getLightsManager().getLights()) {
if (light.hasRgbControl()) {
capabilities |= MoonBridge.LI_CCAP_RGB_LED;
}
}
}
// Report analog triggers if we have at least one trigger axis
if (leftTriggerAxis != -1 || rightTriggerAxis != -1) {
capabilities |= MoonBridge.LI_CCAP_ANALOG_TRIGGERS;
}
// We can perform basic rumble with any vibrator
if (vibrator != null) {
capabilities |= MoonBridge.LI_CCAP_RUMBLE;
}
// Shield controllers use special APIs for rumble and battery state
if (sceManager.isRecognizedDevice(inputDevice)) {
capabilities |= MoonBridge.LI_CCAP_RUMBLE | MoonBridge.LI_CCAP_BATTERY_STATE;
}
if ((inputDevice.getSources() & InputDevice.SOURCE_TOUCHPAD) == InputDevice.SOURCE_TOUCHPAD) {
capabilities |= MoonBridge.LI_CCAP_TOUCHPAD;
// If this is a PlayStation controller with a touchpad, we know it has a clickpad.
// FIXME: Can we actually tell a clickpad from a touchpad using Android APIs?
if (type == MoonBridge.LI_CTYPE_PS) {
supportedButtonFlags |= ControllerPacket.TOUCHPAD_FLAG;
}
}
conn.sendControllerArrivalEvent((byte)controllerNumber, getActiveControllerMask(),
type, supportedButtonFlags, capabilities);
// After reporting arrival to the host, send initial battery state and begin monitoring
sendControllerBatteryPacket(this);
handler.postDelayed(batteryStateUpdateRunnable, BATTERY_RECHECK_INTERVAL_MS);
}
}
class UsbDeviceContext extends GenericControllerContext {
public AbstractController device;
@Override
public void destroy() {
super.destroy();
// Nothing for now
}
@Override
public void sendControllerArrival() {
conn.sendControllerArrivalEvent((byte)controllerNumber, getActiveControllerMask(),
device.getType(), device.getSupportedButtonFlags(), device.getCapabilities());
}
}
}