Add multiple controller support

This commit is contained in:
Cameron Gutman 2015-02-01 15:06:18 -05:00
parent 3a0c1db168
commit da7904a767
5 changed files with 248 additions and 217 deletions

View File

@ -193,7 +193,7 @@ public class Game extends Activity implements SurfaceHolder.Callback,
// Initialize the connection // Initialize the connection
conn = new NvConnection(host, uniqueId, Game.this, config, PlatformBinding.getCryptoProvider(this)); conn = new NvConnection(host, uniqueId, Game.this, config, PlatformBinding.getCryptoProvider(this));
keybTranslator = new KeyboardTranslator(conn); keybTranslator = new KeyboardTranslator(conn);
controllerHandler = new ControllerHandler(conn, this, prefConfig.deadzonePercentage); controllerHandler = new ControllerHandler(conn, this, prefConfig.multiController, prefConfig.deadzonePercentage);
SurfaceHolder sh = sv.getHolder(); SurfaceHolder sh = sv.getHolder();
if (prefConfig.stretchVideo || !decoderRenderer.isHardwareAccelerated()) { if (prefConfig.stretchVideo || !decoderRenderer.isHardwareAccelerated()) {

View File

@ -14,25 +14,9 @@ import com.limelight.ui.GameGestures;
import com.limelight.utils.Vector2d; import com.limelight.utils.Vector2d;
public class ControllerHandler { public class ControllerHandler {
private short inputMap = 0x0000;
private byte leftTrigger = 0x00;
private byte rightTrigger = 0x00;
private short rightStickX = 0x0000;
private short rightStickY = 0x0000;
private short leftStickX = 0x0000;
private short leftStickY = 0x0000;
private int emulatingButtonFlags = 0;
// 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.
private long lastLbUpTime = 0;
private long lastRbUpTime = 0;
private static final int MAXIMUM_BUMPER_UP_DELAY_MS = 100; private static final int MAXIMUM_BUMPER_UP_DELAY_MS = 100;
private long startDownTime = 0;
private static final int START_DOWN_TIME_KEYB_MS = 750; private static final int START_DOWN_TIME_KEYB_MS = 750;
private static final int MINIMUM_BUTTON_DOWN_TIME_MS = 25; private static final int MINIMUM_BUTTON_DOWN_TIME_MS = 25;
@ -45,17 +29,21 @@ public class ControllerHandler {
private Vector2d inputVector = new Vector2d(); private Vector2d inputVector = new Vector2d();
private HashMap<String, ControllerMapping> mappings = new HashMap<String, ControllerMapping>(); private HashMap<String, ControllerContext> contexts = new HashMap<String, ControllerContext>();
private NvConnection conn; private NvConnection conn;
private double stickDeadzone; private double stickDeadzone;
private final ControllerMapping defaultMapping = new ControllerMapping(); private final ControllerContext defaultContext = new ControllerContext();
private GameGestures gestures; private GameGestures gestures;
private boolean hasGameController; private boolean hasGameController;
public ControllerHandler(NvConnection conn, GameGestures gestures, int deadzonePercentage) { private boolean multiControllerEnabled;
private short nextControllerNumber;
public ControllerHandler(NvConnection conn, GameGestures gestures, boolean multiControllerEnabled, int deadzonePercentage) {
this.conn = conn; this.conn = conn;
this.gestures = gestures; this.gestures = gestures;
this.multiControllerEnabled = multiControllerEnabled;
// HACK: For now we're hardcoding a 10% deadzone. Some deadzone // HACK: For now we're hardcoding a 10% deadzone. Some deadzone
// is required for controller batching support to work. // is required for controller batching support to work.
@ -82,15 +70,16 @@ public class ControllerHandler {
this.stickDeadzone = (double)deadzonePercentage / 100.0; this.stickDeadzone = (double)deadzonePercentage / 100.0;
// Initialize the default mapping for events with no device // Initialize the default context for events with no device
defaultMapping.leftStickXAxis = MotionEvent.AXIS_X; defaultContext.leftStickXAxis = MotionEvent.AXIS_X;
defaultMapping.leftStickYAxis = MotionEvent.AXIS_Y; defaultContext.leftStickYAxis = MotionEvent.AXIS_Y;
defaultMapping.leftStickDeadzoneRadius = (float) stickDeadzone; defaultContext.leftStickDeadzoneRadius = (float) stickDeadzone;
defaultMapping.rightStickXAxis = MotionEvent.AXIS_Z; defaultContext.rightStickXAxis = MotionEvent.AXIS_Z;
defaultMapping.rightStickYAxis = MotionEvent.AXIS_RZ; defaultContext.rightStickYAxis = MotionEvent.AXIS_RZ;
defaultMapping.rightStickDeadzoneRadius = (float) stickDeadzone; defaultContext.rightStickDeadzoneRadius = (float) stickDeadzone;
defaultMapping.leftTriggerAxis = MotionEvent.AXIS_BRAKE; defaultContext.leftTriggerAxis = MotionEvent.AXIS_BRAKE;
defaultMapping.rightTriggerAxis = MotionEvent.AXIS_GAS; defaultContext.rightTriggerAxis = MotionEvent.AXIS_GAS;
defaultContext.controllerNumber = (short) 0;
} }
private static InputDevice.MotionRange getMotionRangeForJoystickAxis(InputDevice dev, int axis) { private static InputDevice.MotionRange getMotionRangeForJoystickAxis(InputDevice dev, int axis) {
@ -106,19 +95,19 @@ public class ControllerHandler {
return range; return range;
} }
private ControllerMapping createMappingForDevice(InputDevice dev) { private ControllerContext createContextForDevice(InputDevice dev) {
ControllerMapping mapping = new ControllerMapping(); ControllerContext context = new ControllerContext();
String devName = dev.getName(); String devName = dev.getName();
LimeLog.info("Creating controller mapping for device: "+devName); LimeLog.info("Creating controller context for device: "+devName);
mapping.leftStickXAxis = MotionEvent.AXIS_X; context.leftStickXAxis = MotionEvent.AXIS_X;
mapping.leftStickYAxis = MotionEvent.AXIS_Y; context.leftStickYAxis = MotionEvent.AXIS_Y;
if (getMotionRangeForJoystickAxis(dev, mapping.leftStickXAxis) != null && if (getMotionRangeForJoystickAxis(dev, context.leftStickXAxis) != null &&
getMotionRangeForJoystickAxis(dev, mapping.leftStickYAxis) != null) { getMotionRangeForJoystickAxis(dev, context.leftStickYAxis) != null) {
// This is a gamepad // This is a gamepad
hasGameController = true; hasGameController = true;
mapping.hasJoystickAxes = true; context.hasJoystickAxes = true;
} }
InputDevice.MotionRange leftTriggerRange = getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_LTRIGGER); InputDevice.MotionRange leftTriggerRange = getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_LTRIGGER);
@ -128,14 +117,14 @@ public class ControllerHandler {
if (leftTriggerRange != null && rightTriggerRange != null) if (leftTriggerRange != null && rightTriggerRange != null)
{ {
// Some controllers use LTRIGGER and RTRIGGER (like Ouya) // Some controllers use LTRIGGER and RTRIGGER (like Ouya)
mapping.leftTriggerAxis = MotionEvent.AXIS_LTRIGGER; context.leftTriggerAxis = MotionEvent.AXIS_LTRIGGER;
mapping.rightTriggerAxis = MotionEvent.AXIS_RTRIGGER; context.rightTriggerAxis = MotionEvent.AXIS_RTRIGGER;
} }
else if (brakeRange != null && gasRange != null) else if (brakeRange != null && gasRange != null)
{ {
// Others use GAS and BRAKE (like Moga) // Others use GAS and BRAKE (like Moga)
mapping.leftTriggerAxis = MotionEvent.AXIS_BRAKE; context.leftTriggerAxis = MotionEvent.AXIS_BRAKE;
mapping.rightTriggerAxis = MotionEvent.AXIS_GAS; context.rightTriggerAxis = MotionEvent.AXIS_GAS;
} }
else else
{ {
@ -144,34 +133,34 @@ public class ControllerHandler {
if (rxRange != null && ryRange != null && devName != null) { if (rxRange != null && ryRange != null && devName != null) {
if (devName.contains("Xbox") || devName.contains("XBox") || devName.contains("X-Box")) { if (devName.contains("Xbox") || devName.contains("XBox") || devName.contains("X-Box")) {
// Xbox controllers use RX and RY for right stick // Xbox controllers use RX and RY for right stick
mapping.rightStickXAxis = MotionEvent.AXIS_RX; context.rightStickXAxis = MotionEvent.AXIS_RX;
mapping.rightStickYAxis = MotionEvent.AXIS_RY; context.rightStickYAxis = MotionEvent.AXIS_RY;
// Xbox controllers use Z and RZ for triggers // Xbox controllers use Z and RZ for triggers
mapping.leftTriggerAxis = MotionEvent.AXIS_Z; context.leftTriggerAxis = MotionEvent.AXIS_Z;
mapping.rightTriggerAxis = MotionEvent.AXIS_RZ; context.rightTriggerAxis = MotionEvent.AXIS_RZ;
mapping.triggersIdleNegative = true; context.triggersIdleNegative = true;
mapping.isXboxController = true; context.isXboxController = true;
} }
else { else {
// DS4 controller uses RX and RY for triggers // DS4 controller uses RX and RY for triggers
mapping.leftTriggerAxis = MotionEvent.AXIS_RX; context.leftTriggerAxis = MotionEvent.AXIS_RX;
mapping.rightTriggerAxis = MotionEvent.AXIS_RY; context.rightTriggerAxis = MotionEvent.AXIS_RY;
mapping.triggersIdleNegative = true; context.triggersIdleNegative = true;
mapping.isDualShock4 = true; context.isDualShock4 = true;
} }
} }
} }
if (mapping.rightStickXAxis == -1 && mapping.rightStickYAxis == -1) { if (context.rightStickXAxis == -1 && context.rightStickYAxis == -1) {
InputDevice.MotionRange zRange = getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_Z); InputDevice.MotionRange zRange = getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_Z);
InputDevice.MotionRange rzRange = getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_RZ); InputDevice.MotionRange rzRange = getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_RZ);
// Most other controllers use Z and RZ for the right stick // Most other controllers use Z and RZ for the right stick
if (zRange != null && rzRange != null) { if (zRange != null && rzRange != null) {
mapping.rightStickXAxis = MotionEvent.AXIS_Z; context.rightStickXAxis = MotionEvent.AXIS_Z;
mapping.rightStickYAxis = MotionEvent.AXIS_RZ; context.rightStickYAxis = MotionEvent.AXIS_RZ;
} }
else { else {
InputDevice.MotionRange rxRange = getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_RX); InputDevice.MotionRange rxRange = getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_RX);
@ -179,8 +168,8 @@ public class ControllerHandler {
// Try RX and RY now // Try RX and RY now
if (rxRange != null && ryRange != null) { if (rxRange != null && ryRange != null) {
mapping.rightStickXAxis = MotionEvent.AXIS_RX; context.rightStickXAxis = MotionEvent.AXIS_RX;
mapping.rightStickYAxis = MotionEvent.AXIS_RY; context.rightStickYAxis = MotionEvent.AXIS_RY;
} }
} }
} }
@ -189,30 +178,30 @@ public class ControllerHandler {
InputDevice.MotionRange hatXRange = getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_HAT_X); InputDevice.MotionRange hatXRange = getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_HAT_X);
InputDevice.MotionRange hatYRange = getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_HAT_Y); InputDevice.MotionRange hatYRange = getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_HAT_Y);
if (hatXRange != null && hatYRange != null) { if (hatXRange != null && hatYRange != null) {
mapping.hatXAxis = MotionEvent.AXIS_HAT_X; context.hatXAxis = MotionEvent.AXIS_HAT_X;
mapping.hatYAxis = MotionEvent.AXIS_HAT_Y; context.hatYAxis = MotionEvent.AXIS_HAT_Y;
} }
if (mapping.leftStickXAxis != -1 && mapping.leftStickYAxis != -1) { if (context.leftStickXAxis != -1 && context.leftStickYAxis != -1) {
mapping.leftStickDeadzoneRadius = (float) stickDeadzone; context.leftStickDeadzoneRadius = (float) stickDeadzone;
} }
if (mapping.rightStickXAxis != -1 && mapping.rightStickYAxis != -1) { if (context.rightStickXAxis != -1 && context.rightStickYAxis != -1) {
mapping.rightStickDeadzoneRadius = (float) stickDeadzone; context.rightStickDeadzoneRadius = (float) stickDeadzone;
} }
if (mapping.leftTriggerAxis != -1 && mapping.rightTriggerAxis != -1) { if (context.leftTriggerAxis != -1 && context.rightTriggerAxis != -1) {
InputDevice.MotionRange ltRange = getMotionRangeForJoystickAxis(dev, mapping.leftTriggerAxis); InputDevice.MotionRange ltRange = getMotionRangeForJoystickAxis(dev, context.leftTriggerAxis);
InputDevice.MotionRange rtRange = getMotionRangeForJoystickAxis(dev, mapping.rightTriggerAxis); InputDevice.MotionRange rtRange = getMotionRangeForJoystickAxis(dev, context.rightTriggerAxis);
// It's important to have a valid deadzone so controller packet batching works properly // It's important to have a valid deadzone so controller packet batching works properly
mapping.triggerDeadzone = Math.max(Math.abs(ltRange.getFlat()), Math.abs(rtRange.getFlat())); 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) // For triggers without (valid) deadzones, we'll use 13% (around XInput's default)
if (mapping.triggerDeadzone < 0.13f || if (context.triggerDeadzone < 0.13f ||
mapping.triggerDeadzone > 0.30f) context.triggerDeadzone > 0.30f)
{ {
mapping.triggerDeadzone = 0.13f; context.triggerDeadzone = 0.13f;
} }
} }
@ -226,71 +215,82 @@ public class ControllerHandler {
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.KITKAT) { if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.KITKAT) {
boolean[] hasStartKey = dev.hasKeys(KeyEvent.KEYCODE_BUTTON_START, KeyEvent.KEYCODE_MENU, 0); boolean[] hasStartKey = dev.hasKeys(KeyEvent.KEYCODE_BUTTON_START, KeyEvent.KEYCODE_MENU, 0);
if (!hasStartKey[0] && !hasStartKey[1]) { if (!hasStartKey[0] && !hasStartKey[1]) {
mapping.backIsStart = true; context.backIsStart = true;
} }
} }
// The ASUS Gamepad has triggers that sit far forward and are prone to false presses // 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 // so we increase the deadzone on them to minimize this
mapping.triggerDeadzone = 0.30f; context.triggerDeadzone = 0.30f;
} }
// Classify this device as a remote by name // Classify this device as a remote by name
else if (devName.contains("Fire TV Remote") || devName.contains("Nexus Remote")) { else if (devName.contains("Fire TV Remote") || devName.contains("Nexus Remote")) {
// It's only a remote if it doesn't any sticks // It's only a remote if it doesn't any sticks
if (!mapping.hasJoystickAxes) { if (!context.hasJoystickAxes) {
mapping.isRemote = true; context.isRemote = true;
} }
} }
// NYKO Playpad has a fake hat that mimics the left stick for some reason // NYKO Playpad has a fake hat that mimics the left stick for some reason
else if (devName.contains("NYKO PLAYPAD")) { else if (devName.contains("NYKO PLAYPAD")) {
mapping.hatXAxis = -1; context.hatXAxis = -1;
mapping.hatYAxis = -1; context.hatYAxis = -1;
} }
} }
LimeLog.info("Analog stick deadzone: "+mapping.leftStickDeadzoneRadius+" "+mapping.rightStickDeadzoneRadius); LimeLog.info("Analog stick deadzone: "+context.leftStickDeadzoneRadius+" "+context.rightStickDeadzoneRadius);
LimeLog.info("Trigger deadzone: "+mapping.triggerDeadzone); LimeLog.info("Trigger deadzone: "+context.triggerDeadzone);
return mapping; if (multiControllerEnabled) {
context.controllerNumber = nextControllerNumber;
nextControllerNumber = (short)((nextControllerNumber + 1) % 4);
}
else {
context.controllerNumber = 0;
}
LimeLog.info("Assigned as controller "+context.controllerNumber);
return context;
} }
private ControllerMapping getMappingForDevice(InputDevice dev) { private ControllerContext getContextForDevice(InputDevice dev) {
// Unknown devices use the default mapping // Unknown devices use the default context
if (dev == null) { if (dev == null) {
return defaultMapping; return defaultContext;
} }
String descriptor = dev.getDescriptor(); String descriptor = dev.getDescriptor();
// Return the existing mapping if it exists // Return the existing context if it exists
ControllerMapping mapping = mappings.get(descriptor); ControllerContext context = contexts.get(descriptor);
if (mapping != null) { if (context != null) {
return mapping; return context;
} }
// Otherwise create a new mapping // Otherwise create a new context
mapping = createMappingForDevice(dev); context = createContextForDevice(dev);
mappings.put(descriptor, mapping); contexts.put(descriptor, context);
return mapping; return context;
} }
private void sendControllerInputPacket() { private void sendControllerInputPacket(ControllerContext context) {
conn.sendControllerInput(inputMap, leftTrigger, rightTrigger, conn.sendControllerInput(context.controllerNumber, context.inputMap,
leftStickX, leftStickY, rightStickX, rightStickY); context.leftTrigger, context.rightTrigger,
context.leftStickX, context.leftStickY,
context.rightStickX, context.rightStickY);
} }
// Return a valid keycode, 0 to consume, or -1 to not consume the event // Return a valid keycode, 0 to consume, or -1 to not consume the event
// Device MAY BE NULL // Device MAY BE NULL
private int handleRemapping(ControllerMapping mapping, KeyEvent event) { private int handleRemapping(ControllerContext context, KeyEvent event) {
// For remotes, don't capture the back button // For remotes, don't capture the back button
if (mapping.isRemote) { if (context.isRemote) {
if (event.getKeyCode() == KeyEvent.KEYCODE_BACK) { if (event.getKeyCode() == KeyEvent.KEYCODE_BACK) {
return -1; return -1;
} }
} }
if (mapping.isDualShock4) { if (context.isDualShock4) {
switch (event.getKeyCode()) { switch (event.getKeyCode()) {
case KeyEvent.KEYCODE_BUTTON_Y: case KeyEvent.KEYCODE_BUTTON_Y:
return KeyEvent.KEYCODE_BUTTON_L1; return KeyEvent.KEYCODE_BUTTON_L1;
@ -329,7 +329,7 @@ public class ControllerHandler {
} }
} }
if (mapping.hatXAxis != -1 && mapping.hatYAxis != -1) { if (context.hatXAxis != -1 && context.hatYAxis != -1) {
switch (event.getKeyCode()) { switch (event.getKeyCode()) {
// These are duplicate dpad events for hat input // These are duplicate dpad events for hat input
case KeyEvent.KEYCODE_DPAD_LEFT: case KeyEvent.KEYCODE_DPAD_LEFT:
@ -340,9 +340,9 @@ public class ControllerHandler {
return 0; return 0;
} }
} }
else if (mapping.hatXAxis == -1 && else if (context.hatXAxis == -1 &&
mapping.hatYAxis == -1 && context.hatYAxis == -1 &&
mapping.isXboxController && context.isXboxController &&
event.getKeyCode() == KeyEvent.KEYCODE_UNKNOWN) { event.getKeyCode() == KeyEvent.KEYCODE_UNKNOWN) {
// If there's not a proper Xbox controller mapping, we'll translate the raw d-pad // If there's not a proper Xbox controller mapping, we'll translate the raw d-pad
// scan codes into proper key codes // scan codes into proper key codes
@ -375,9 +375,9 @@ public class ControllerHandler {
if (keyCode == KeyEvent.KEYCODE_BUTTON_START || if (keyCode == KeyEvent.KEYCODE_BUTTON_START ||
keyCode == KeyEvent.KEYCODE_MENU) { keyCode == KeyEvent.KEYCODE_MENU) {
// Ensure that we never use back as start if we have a real start // Ensure that we never use back as start if we have a real start
mapping.backIsStart = false; context.backIsStart = false;
} }
else if (mapping.backIsStart && keyCode == KeyEvent.KEYCODE_BACK) { else if (context.backIsStart && keyCode == KeyEvent.KEYCODE_BACK) {
// Emulate the start button with back // Emulate the start button with back
return KeyEvent.KEYCODE_BUTTON_START; return KeyEvent.KEYCODE_BUTTON_START;
} }
@ -402,101 +402,101 @@ public class ControllerHandler {
// evaluates the deadzone. // evaluates the deadzone.
} }
private void handleAxisSet(ControllerMapping mapping, float lsX, float lsY, float rsX, private void handleAxisSet(ControllerContext context, float lsX, float lsY, float rsX,
float rsY, float lt, float rt, float hatX, float hatY) { float rsY, float lt, float rt, float hatX, float hatY) {
if (mapping.leftStickXAxis != -1 && mapping.leftStickYAxis != -1) { if (context.leftStickXAxis != -1 && context.leftStickYAxis != -1) {
Vector2d leftStickVector = populateCachedVector(lsX, lsY); Vector2d leftStickVector = populateCachedVector(lsX, lsY);
handleDeadZone(leftStickVector, mapping.leftStickDeadzoneRadius); handleDeadZone(leftStickVector, context.leftStickDeadzoneRadius);
leftStickX = (short) (leftStickVector.getX() * 0x7FFE); context.leftStickX = (short) (leftStickVector.getX() * 0x7FFE);
leftStickY = (short) (-leftStickVector.getY() * 0x7FFE); context.leftStickY = (short) (-leftStickVector.getY() * 0x7FFE);
} }
if (mapping.rightStickXAxis != -1 && mapping.rightStickYAxis != -1) { if (context.rightStickXAxis != -1 && context.rightStickYAxis != -1) {
Vector2d rightStickVector = populateCachedVector(rsX, rsY); Vector2d rightStickVector = populateCachedVector(rsX, rsY);
handleDeadZone(rightStickVector, mapping.rightStickDeadzoneRadius); handleDeadZone(rightStickVector, context.rightStickDeadzoneRadius);
rightStickX = (short) (rightStickVector.getX() * 0x7FFE); context.rightStickX = (short) (rightStickVector.getX() * 0x7FFE);
rightStickY = (short) (-rightStickVector.getY() * 0x7FFE); context.rightStickY = (short) (-rightStickVector.getY() * 0x7FFE);
} }
if (mapping.leftTriggerAxis != -1 && mapping.rightTriggerAxis != -1) { if (context.leftTriggerAxis != -1 && context.rightTriggerAxis != -1) {
if (mapping.triggersIdleNegative) { if (context.triggersIdleNegative) {
lt = (lt + 1) / 2; lt = (lt + 1) / 2;
rt = (rt + 1) / 2; rt = (rt + 1) / 2;
} }
if (lt <= mapping.triggerDeadzone) { if (lt <= context.triggerDeadzone) {
lt = 0; lt = 0;
} }
if (rt <= mapping.triggerDeadzone) { if (rt <= context.triggerDeadzone) {
rt = 0; rt = 0;
} }
leftTrigger = (byte)(lt * 0xFF); context.leftTrigger = (byte)(lt * 0xFF);
rightTrigger = (byte)(rt * 0xFF); context.rightTrigger = (byte)(rt * 0xFF);
} }
if (mapping.hatXAxis != -1 && mapping.hatYAxis != -1) { if (context.hatXAxis != -1 && context.hatYAxis != -1) {
inputMap &= ~(ControllerPacket.LEFT_FLAG | ControllerPacket.RIGHT_FLAG); context.inputMap &= ~(ControllerPacket.LEFT_FLAG | ControllerPacket.RIGHT_FLAG);
if (hatX < -0.5) { if (hatX < -0.5) {
inputMap |= ControllerPacket.LEFT_FLAG; context.inputMap |= ControllerPacket.LEFT_FLAG;
} }
else if (hatX > 0.5) { else if (hatX > 0.5) {
inputMap |= ControllerPacket.RIGHT_FLAG; context.inputMap |= ControllerPacket.RIGHT_FLAG;
} }
inputMap &= ~(ControllerPacket.UP_FLAG | ControllerPacket.DOWN_FLAG); context.inputMap &= ~(ControllerPacket.UP_FLAG | ControllerPacket.DOWN_FLAG);
if (hatY < -0.5) { if (hatY < -0.5) {
inputMap |= ControllerPacket.UP_FLAG; context.inputMap |= ControllerPacket.UP_FLAG;
} }
else if (hatY > 0.5) { else if (hatY > 0.5) {
inputMap |= ControllerPacket.DOWN_FLAG; context.inputMap |= ControllerPacket.DOWN_FLAG;
} }
} }
sendControllerInputPacket(); sendControllerInputPacket(context);
} }
public boolean handleMotionEvent(MotionEvent event) { public boolean handleMotionEvent(MotionEvent event) {
ControllerMapping mapping = getMappingForDevice(event.getDevice()); ControllerContext context = getContextForDevice(event.getDevice());
float lsX = 0, lsY = 0, rsX = 0, rsY = 0, rt = 0, lt = 0, hatX = 0, hatY = 0; 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 // We purposefully ignore the historical values in the motion event as it makes
// the controller feel sluggish for some users. // the controller feel sluggish for some users.
if (mapping.leftStickXAxis != -1 && mapping.leftStickYAxis != -1) { if (context.leftStickXAxis != -1 && context.leftStickYAxis != -1) {
lsX = event.getAxisValue(mapping.leftStickXAxis); lsX = event.getAxisValue(context.leftStickXAxis);
lsY = event.getAxisValue(mapping.leftStickYAxis); lsY = event.getAxisValue(context.leftStickYAxis);
} }
if (mapping.rightStickXAxis != -1 && mapping.rightStickYAxis != -1) { if (context.rightStickXAxis != -1 && context.rightStickYAxis != -1) {
rsX = event.getAxisValue(mapping.rightStickXAxis); rsX = event.getAxisValue(context.rightStickXAxis);
rsY = event.getAxisValue(mapping.rightStickYAxis); rsY = event.getAxisValue(context.rightStickYAxis);
} }
if (mapping.leftTriggerAxis != -1 && mapping.rightTriggerAxis != -1) { if (context.leftTriggerAxis != -1 && context.rightTriggerAxis != -1) {
lt = event.getAxisValue(mapping.leftTriggerAxis); lt = event.getAxisValue(context.leftTriggerAxis);
rt = event.getAxisValue(mapping.rightTriggerAxis); rt = event.getAxisValue(context.rightTriggerAxis);
} }
if (mapping.hatXAxis != -1 && mapping.hatYAxis != -1) { if (context.hatXAxis != -1 && context.hatYAxis != -1) {
hatX = event.getAxisValue(MotionEvent.AXIS_HAT_X); hatX = event.getAxisValue(MotionEvent.AXIS_HAT_X);
hatY = event.getAxisValue(MotionEvent.AXIS_HAT_Y); hatY = event.getAxisValue(MotionEvent.AXIS_HAT_Y);
} }
handleAxisSet(mapping, lsX, lsY, rsX, rsY, lt, rt, hatX, hatY); handleAxisSet(context, lsX, lsY, rsX, rsY, lt, rt, hatX, hatY);
return true; return true;
} }
public boolean handleButtonUp(KeyEvent event) { public boolean handleButtonUp(KeyEvent event) {
ControllerMapping mapping = getMappingForDevice(event.getDevice()); ControllerContext context = getContextForDevice(event.getDevice());
int keyCode = handleRemapping(mapping, event); int keyCode = handleRemapping(context, event);
if (keyCode == 0) { if (keyCode == 0) {
return true; return true;
} }
@ -515,78 +515,78 @@ public class ControllerHandler {
switch (keyCode) { switch (keyCode) {
case KeyEvent.KEYCODE_BUTTON_MODE: case KeyEvent.KEYCODE_BUTTON_MODE:
inputMap &= ~ControllerPacket.SPECIAL_BUTTON_FLAG; context.inputMap &= ~ControllerPacket.SPECIAL_BUTTON_FLAG;
break; break;
case KeyEvent.KEYCODE_BUTTON_START: case KeyEvent.KEYCODE_BUTTON_START:
case KeyEvent.KEYCODE_MENU: case KeyEvent.KEYCODE_MENU:
if (SystemClock.uptimeMillis() - startDownTime > ControllerHandler.START_DOWN_TIME_KEYB_MS) { if (SystemClock.uptimeMillis() - context.startDownTime > ControllerHandler.START_DOWN_TIME_KEYB_MS) {
gestures.showKeyboard(); gestures.showKeyboard();
} }
inputMap &= ~ControllerPacket.PLAY_FLAG; context.inputMap &= ~ControllerPacket.PLAY_FLAG;
break; break;
case KeyEvent.KEYCODE_BACK: case KeyEvent.KEYCODE_BACK:
case KeyEvent.KEYCODE_BUTTON_SELECT: case KeyEvent.KEYCODE_BUTTON_SELECT:
inputMap &= ~ControllerPacket.BACK_FLAG; context.inputMap &= ~ControllerPacket.BACK_FLAG;
break; break;
case KeyEvent.KEYCODE_DPAD_LEFT: case KeyEvent.KEYCODE_DPAD_LEFT:
inputMap &= ~ControllerPacket.LEFT_FLAG; context.inputMap &= ~ControllerPacket.LEFT_FLAG;
break; break;
case KeyEvent.KEYCODE_DPAD_RIGHT: case KeyEvent.KEYCODE_DPAD_RIGHT:
inputMap &= ~ControllerPacket.RIGHT_FLAG; context.inputMap &= ~ControllerPacket.RIGHT_FLAG;
break; break;
case KeyEvent.KEYCODE_DPAD_UP: case KeyEvent.KEYCODE_DPAD_UP:
inputMap &= ~ControllerPacket.UP_FLAG; context.inputMap &= ~ControllerPacket.UP_FLAG;
break; break;
case KeyEvent.KEYCODE_DPAD_DOWN: case KeyEvent.KEYCODE_DPAD_DOWN:
inputMap &= ~ControllerPacket.DOWN_FLAG; context.inputMap &= ~ControllerPacket.DOWN_FLAG;
break; break;
case KeyEvent.KEYCODE_BUTTON_B: case KeyEvent.KEYCODE_BUTTON_B:
inputMap &= ~ControllerPacket.B_FLAG; context.inputMap &= ~ControllerPacket.B_FLAG;
break; break;
case KeyEvent.KEYCODE_DPAD_CENTER: case KeyEvent.KEYCODE_DPAD_CENTER:
case KeyEvent.KEYCODE_BUTTON_A: case KeyEvent.KEYCODE_BUTTON_A:
inputMap &= ~ControllerPacket.A_FLAG; context.inputMap &= ~ControllerPacket.A_FLAG;
break; break;
case KeyEvent.KEYCODE_BUTTON_X: case KeyEvent.KEYCODE_BUTTON_X:
inputMap &= ~ControllerPacket.X_FLAG; context.inputMap &= ~ControllerPacket.X_FLAG;
break; break;
case KeyEvent.KEYCODE_BUTTON_Y: case KeyEvent.KEYCODE_BUTTON_Y:
inputMap &= ~ControllerPacket.Y_FLAG; context.inputMap &= ~ControllerPacket.Y_FLAG;
break; break;
case KeyEvent.KEYCODE_BUTTON_L1: case KeyEvent.KEYCODE_BUTTON_L1:
inputMap &= ~ControllerPacket.LB_FLAG; context.inputMap &= ~ControllerPacket.LB_FLAG;
lastLbUpTime = SystemClock.uptimeMillis(); context.lastLbUpTime = SystemClock.uptimeMillis();
break; break;
case KeyEvent.KEYCODE_BUTTON_R1: case KeyEvent.KEYCODE_BUTTON_R1:
inputMap &= ~ControllerPacket.RB_FLAG; context.inputMap &= ~ControllerPacket.RB_FLAG;
lastRbUpTime = SystemClock.uptimeMillis(); context.lastRbUpTime = SystemClock.uptimeMillis();
break; break;
case KeyEvent.KEYCODE_BUTTON_THUMBL: case KeyEvent.KEYCODE_BUTTON_THUMBL:
inputMap &= ~ControllerPacket.LS_CLK_FLAG; context.inputMap &= ~ControllerPacket.LS_CLK_FLAG;
break; break;
case KeyEvent.KEYCODE_BUTTON_THUMBR: case KeyEvent.KEYCODE_BUTTON_THUMBR:
inputMap &= ~ControllerPacket.RS_CLK_FLAG; context.inputMap &= ~ControllerPacket.RS_CLK_FLAG;
break; break;
case KeyEvent.KEYCODE_BUTTON_L2: case KeyEvent.KEYCODE_BUTTON_L2:
leftTrigger = 0; context.leftTrigger = 0;
break; break;
case KeyEvent.KEYCODE_BUTTON_R2: case KeyEvent.KEYCODE_BUTTON_R2:
rightTrigger = 0; context.rightTrigger = 0;
break; break;
default: default:
return false; return false;
} }
// Check if we're emulating the select button // Check if we're emulating the select button
if ((emulatingButtonFlags & ControllerHandler.EMULATING_SELECT) != 0) if ((context.emulatingButtonFlags & ControllerHandler.EMULATING_SELECT) != 0)
{ {
// If either start or LB is up, select comes up too // If either start or LB is up, select comes up too
if ((inputMap & ControllerPacket.PLAY_FLAG) == 0 || if ((context.inputMap & ControllerPacket.PLAY_FLAG) == 0 ||
(inputMap & ControllerPacket.LB_FLAG) == 0) (context.inputMap & ControllerPacket.LB_FLAG) == 0)
{ {
inputMap &= ~ControllerPacket.BACK_FLAG; context.inputMap &= ~ControllerPacket.BACK_FLAG;
emulatingButtonFlags &= ~ControllerHandler.EMULATING_SELECT; context.emulatingButtonFlags &= ~ControllerHandler.EMULATING_SELECT;
try { try {
Thread.sleep(EMULATED_SELECT_UP_DELAY_MS); Thread.sleep(EMULATED_SELECT_UP_DELAY_MS);
@ -595,16 +595,16 @@ public class ControllerHandler {
} }
// Check if we're emulating the special button // Check if we're emulating the special button
if ((emulatingButtonFlags & ControllerHandler.EMULATING_SPECIAL) != 0) if ((context.emulatingButtonFlags & ControllerHandler.EMULATING_SPECIAL) != 0)
{ {
// If either start or select and RB is up, the special button comes up too // If either start or select and RB is up, the special button comes up too
if ((inputMap & ControllerPacket.PLAY_FLAG) == 0 || if ((context.inputMap & ControllerPacket.PLAY_FLAG) == 0 ||
((inputMap & ControllerPacket.BACK_FLAG) == 0 && ((context.inputMap & ControllerPacket.BACK_FLAG) == 0 &&
(inputMap & ControllerPacket.RB_FLAG) == 0)) (context.inputMap & ControllerPacket.RB_FLAG) == 0))
{ {
inputMap &= ~ControllerPacket.SPECIAL_BUTTON_FLAG; context.inputMap &= ~ControllerPacket.SPECIAL_BUTTON_FLAG;
emulatingButtonFlags &= ~ControllerHandler.EMULATING_SPECIAL; context.emulatingButtonFlags &= ~ControllerHandler.EMULATING_SPECIAL;
try { try {
Thread.sleep(EMULATED_SPECIAL_UP_DELAY_MS); Thread.sleep(EMULATED_SPECIAL_UP_DELAY_MS);
@ -612,112 +612,112 @@ public class ControllerHandler {
} }
} }
sendControllerInputPacket(); sendControllerInputPacket(context);
return true; return true;
} }
public boolean handleButtonDown(KeyEvent event) { public boolean handleButtonDown(KeyEvent event) {
ControllerMapping mapping = getMappingForDevice(event.getDevice()); ControllerContext context = getContextForDevice(event.getDevice());
int keyCode = handleRemapping(mapping, event); int keyCode = handleRemapping(context, event);
if (keyCode == 0) { if (keyCode == 0) {
return true; return true;
} }
switch (keyCode) { switch (keyCode) {
case KeyEvent.KEYCODE_BUTTON_MODE: case KeyEvent.KEYCODE_BUTTON_MODE:
inputMap |= ControllerPacket.SPECIAL_BUTTON_FLAG; context.inputMap |= ControllerPacket.SPECIAL_BUTTON_FLAG;
break; break;
case KeyEvent.KEYCODE_BUTTON_START: case KeyEvent.KEYCODE_BUTTON_START:
case KeyEvent.KEYCODE_MENU: case KeyEvent.KEYCODE_MENU:
if (event.getRepeatCount() == 0) { if (event.getRepeatCount() == 0) {
startDownTime = SystemClock.uptimeMillis(); context.startDownTime = SystemClock.uptimeMillis();
} }
inputMap |= ControllerPacket.PLAY_FLAG; context.inputMap |= ControllerPacket.PLAY_FLAG;
break; break;
case KeyEvent.KEYCODE_BACK: case KeyEvent.KEYCODE_BACK:
case KeyEvent.KEYCODE_BUTTON_SELECT: case KeyEvent.KEYCODE_BUTTON_SELECT:
inputMap |= ControllerPacket.BACK_FLAG; context.inputMap |= ControllerPacket.BACK_FLAG;
break; break;
case KeyEvent.KEYCODE_DPAD_LEFT: case KeyEvent.KEYCODE_DPAD_LEFT:
inputMap |= ControllerPacket.LEFT_FLAG; context.inputMap |= ControllerPacket.LEFT_FLAG;
break; break;
case KeyEvent.KEYCODE_DPAD_RIGHT: case KeyEvent.KEYCODE_DPAD_RIGHT:
inputMap |= ControllerPacket.RIGHT_FLAG; context.inputMap |= ControllerPacket.RIGHT_FLAG;
break; break;
case KeyEvent.KEYCODE_DPAD_UP: case KeyEvent.KEYCODE_DPAD_UP:
inputMap |= ControllerPacket.UP_FLAG; context.inputMap |= ControllerPacket.UP_FLAG;
break; break;
case KeyEvent.KEYCODE_DPAD_DOWN: case KeyEvent.KEYCODE_DPAD_DOWN:
inputMap |= ControllerPacket.DOWN_FLAG; context.inputMap |= ControllerPacket.DOWN_FLAG;
break; break;
case KeyEvent.KEYCODE_BUTTON_B: case KeyEvent.KEYCODE_BUTTON_B:
inputMap |= ControllerPacket.B_FLAG; context.inputMap |= ControllerPacket.B_FLAG;
break; break;
case KeyEvent.KEYCODE_DPAD_CENTER: case KeyEvent.KEYCODE_DPAD_CENTER:
case KeyEvent.KEYCODE_BUTTON_A: case KeyEvent.KEYCODE_BUTTON_A:
inputMap |= ControllerPacket.A_FLAG; context.inputMap |= ControllerPacket.A_FLAG;
break; break;
case KeyEvent.KEYCODE_BUTTON_X: case KeyEvent.KEYCODE_BUTTON_X:
inputMap |= ControllerPacket.X_FLAG; context.inputMap |= ControllerPacket.X_FLAG;
break; break;
case KeyEvent.KEYCODE_BUTTON_Y: case KeyEvent.KEYCODE_BUTTON_Y:
inputMap |= ControllerPacket.Y_FLAG; context.inputMap |= ControllerPacket.Y_FLAG;
break; break;
case KeyEvent.KEYCODE_BUTTON_L1: case KeyEvent.KEYCODE_BUTTON_L1:
inputMap |= ControllerPacket.LB_FLAG; context.inputMap |= ControllerPacket.LB_FLAG;
break; break;
case KeyEvent.KEYCODE_BUTTON_R1: case KeyEvent.KEYCODE_BUTTON_R1:
inputMap |= ControllerPacket.RB_FLAG; context.inputMap |= ControllerPacket.RB_FLAG;
break; break;
case KeyEvent.KEYCODE_BUTTON_THUMBL: case KeyEvent.KEYCODE_BUTTON_THUMBL:
inputMap |= ControllerPacket.LS_CLK_FLAG; context.inputMap |= ControllerPacket.LS_CLK_FLAG;
break; break;
case KeyEvent.KEYCODE_BUTTON_THUMBR: case KeyEvent.KEYCODE_BUTTON_THUMBR:
inputMap |= ControllerPacket.RS_CLK_FLAG; context.inputMap |= ControllerPacket.RS_CLK_FLAG;
break; break;
case KeyEvent.KEYCODE_BUTTON_L2: case KeyEvent.KEYCODE_BUTTON_L2:
leftTrigger = (byte)0xFF; context.leftTrigger = (byte)0xFF;
break; break;
case KeyEvent.KEYCODE_BUTTON_R2: case KeyEvent.KEYCODE_BUTTON_R2:
rightTrigger = (byte)0xFF; context.rightTrigger = (byte)0xFF;
break; break;
default: default:
return false; return false;
} }
// Start+LB acts like select for controllers with one button // Start+LB acts like select for controllers with one button
if ((inputMap & ControllerPacket.PLAY_FLAG) != 0 && if ((context.inputMap & ControllerPacket.PLAY_FLAG) != 0 &&
((inputMap & ControllerPacket.LB_FLAG) != 0 || ((context.inputMap & ControllerPacket.LB_FLAG) != 0 ||
SystemClock.uptimeMillis() - lastLbUpTime <= MAXIMUM_BUMPER_UP_DELAY_MS)) SystemClock.uptimeMillis() - context.lastLbUpTime <= MAXIMUM_BUMPER_UP_DELAY_MS))
{ {
inputMap &= ~(ControllerPacket.PLAY_FLAG | ControllerPacket.LB_FLAG); context.inputMap &= ~(ControllerPacket.PLAY_FLAG | ControllerPacket.LB_FLAG);
inputMap |= ControllerPacket.BACK_FLAG; context.inputMap |= ControllerPacket.BACK_FLAG;
emulatingButtonFlags |= ControllerHandler.EMULATING_SELECT; context.emulatingButtonFlags |= ControllerHandler.EMULATING_SELECT;
} }
// We detect select+start or start+RB as the special button combo // We detect select+start or start+RB as the special button combo
if (((inputMap & ControllerPacket.RB_FLAG) != 0 || if (((context.inputMap & ControllerPacket.RB_FLAG) != 0 ||
(SystemClock.uptimeMillis() - lastRbUpTime <= MAXIMUM_BUMPER_UP_DELAY_MS) || (SystemClock.uptimeMillis() - context.lastRbUpTime <= MAXIMUM_BUMPER_UP_DELAY_MS) ||
(inputMap & ControllerPacket.BACK_FLAG) != 0) && (context.inputMap & ControllerPacket.BACK_FLAG) != 0) &&
(inputMap & ControllerPacket.PLAY_FLAG) != 0) (context.inputMap & ControllerPacket.PLAY_FLAG) != 0)
{ {
inputMap &= ~(ControllerPacket.BACK_FLAG | ControllerPacket.PLAY_FLAG | ControllerPacket.RB_FLAG); context.inputMap &= ~(ControllerPacket.BACK_FLAG | ControllerPacket.PLAY_FLAG | ControllerPacket.RB_FLAG);
inputMap |= ControllerPacket.SPECIAL_BUTTON_FLAG; context.inputMap |= ControllerPacket.SPECIAL_BUTTON_FLAG;
emulatingButtonFlags |= ControllerHandler.EMULATING_SPECIAL; context.emulatingButtonFlags |= ControllerHandler.EMULATING_SPECIAL;
} }
// Send a new input packet if this is the first instance of a button down event // Send a new input packet if this is the first instance of a button down event
// or anytime if we're emulating a button // or anytime if we're emulating a button
if (event.getRepeatCount() == 0 || emulatingButtonFlags != 0) { if (event.getRepeatCount() == 0 || context.emulatingButtonFlags != 0) {
sendControllerInputPacket(); sendControllerInputPacket(context);
} }
return true; return true;
} }
class ControllerMapping { class ControllerContext {
public int leftStickXAxis = -1; public int leftStickXAxis = -1;
public int leftStickYAxis = -1; public int leftStickYAxis = -1;
public float leftStickDeadzoneRadius; public float leftStickDeadzoneRadius;
@ -739,5 +739,26 @@ public class ControllerHandler {
public boolean backIsStart; public boolean backIsStart;
public boolean isRemote; public boolean isRemote;
public boolean hasJoystickAxes; public boolean hasJoystickAxes;
public short controllerNumber;
public short inputMap = 0x0000;
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 int emulatingButtonFlags = 0;
// 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;
} }
} }

View File

@ -16,6 +16,7 @@ public class PreferenceConfiguration {
private static final String LANGUAGE_PREF_STRING = "list_languages"; private static final String LANGUAGE_PREF_STRING = "list_languages";
private static final String LIST_MODE_PREF_STRING = "checkbox_list_mode"; private static final String LIST_MODE_PREF_STRING = "checkbox_list_mode";
private static final String SMALL_ICONS_PREF_STRING = "checkbox_small_icon_mode"; private static final String SMALL_ICONS_PREF_STRING = "checkbox_small_icon_mode";
private static final String MULTI_CONTROLLER_PREF_STRING = "checkbox_multi_controller";
private static final int BITRATE_DEFAULT_720_30 = 5; private static final int BITRATE_DEFAULT_720_30 = 5;
private static final int BITRATE_DEFAULT_720_60 = 10; private static final int BITRATE_DEFAULT_720_60 = 10;
@ -32,6 +33,7 @@ public class PreferenceConfiguration {
private static final int DEFAULT_DEADZONE = 15; private static final int DEFAULT_DEADZONE = 15;
public static final String DEFAULT_LANGUAGE = "default"; public static final String DEFAULT_LANGUAGE = "default";
private static final boolean DEFAULT_LIST_MODE = false; private static final boolean DEFAULT_LIST_MODE = false;
private static final boolean DEFAULT_MULTI_CONTROLLER = true;
public static final int FORCE_HARDWARE_DECODER = -1; public static final int FORCE_HARDWARE_DECODER = -1;
public static final int AUTOSELECT_DECODER = 0; public static final int AUTOSELECT_DECODER = 0;
@ -43,7 +45,7 @@ public class PreferenceConfiguration {
public int deadzonePercentage; public int deadzonePercentage;
public boolean stretchVideo, enableSops, playHostAudio, disableWarnings; public boolean stretchVideo, enableSops, playHostAudio, disableWarnings;
public String language; public String language;
public boolean listMode, smallIconMode; public boolean listMode, smallIconMode, multiController;
public static int getDefaultBitrate(String resFpsString) { public static int getDefaultBitrate(String resFpsString) {
if (resFpsString.equals("720p30")) { if (resFpsString.equals("720p30")) {
@ -156,6 +158,7 @@ public class PreferenceConfiguration {
config.playHostAudio = prefs.getBoolean(HOST_AUDIO_PREF_STRING, DEFAULT_HOST_AUDIO); config.playHostAudio = prefs.getBoolean(HOST_AUDIO_PREF_STRING, DEFAULT_HOST_AUDIO);
config.listMode = prefs.getBoolean(LIST_MODE_PREF_STRING, DEFAULT_LIST_MODE); config.listMode = prefs.getBoolean(LIST_MODE_PREF_STRING, DEFAULT_LIST_MODE);
config.smallIconMode = prefs.getBoolean(SMALL_ICONS_PREF_STRING, getDefaultSmallMode(context)); config.smallIconMode = prefs.getBoolean(SMALL_ICONS_PREF_STRING, getDefaultSmallMode(context));
config.multiController = prefs.getBoolean(MULTI_CONTROLLER_PREF_STRING, DEFAULT_MULTI_CONTROLLER);
return config; return config;
} }

View File

@ -95,6 +95,8 @@
<string name="summary_checkbox_disable_warnings">Disable on-screen connection warning messages while streaming</string> <string name="summary_checkbox_disable_warnings">Disable on-screen connection warning messages while streaming</string>
<string name="category_gamepad_settings">Gamepad Settings</string> <string name="category_gamepad_settings">Gamepad Settings</string>
<string name="title_checkbox_multi_controller">Enable multiple controller support</string>
<string name="summary_checkbox_multi_controller">When unchecked, all controllers appear as controller one</string>
<string name="title_seekbar_deadzone">Adjust analog stick deadzone</string> <string name="title_seekbar_deadzone">Adjust analog stick deadzone</string>
<string name="suffix_seekbar_deadzone">%</string> <string name="suffix_seekbar_deadzone">%</string>

View File

@ -26,14 +26,19 @@
android:summary="@string/summary_checkbox_disable_warnings" android:summary="@string/summary_checkbox_disable_warnings"
android:defaultValue="false" /> android:defaultValue="false" />
</PreferenceCategory> </PreferenceCategory>
<!--PreferenceCategory android:title="@string/category_gamepad_settings"> <PreferenceCategory android:title="@string/category_gamepad_settings">
<com.limelight.preferences.SeekBarPreference <!--com.limelight.preferences.SeekBarPreference
android:key="seekbar_deadzone" android:key="seekbar_deadzone"
android:defaultValue="15" android:defaultValue="15"
android:max="50" android:max="50"
android:text="@string/suffix_seekbar_deadzone" android:text="@string/suffix_seekbar_deadzone"
android:title="@string/title_seekbar_deadzone"/> android:title="@string/title_seekbar_deadzone"/-->
</PreferenceCategory--> <CheckBoxPreference
android:key="checkbox_multi_controller"
android:title="@string/title_checkbox_multi_controller"
android:summary="@string/summary_checkbox_multi_controller"
android:defaultValue="true" />
</PreferenceCategory>
<PreferenceCategory android:title="@string/category_host_settings"> <PreferenceCategory android:title="@string/category_host_settings">
<CheckBoxPreference <CheckBoxPreference
android:key="checkbox_enable_sops" android:key="checkbox_enable_sops"