Compare commits

..

14 Commits

Author SHA1 Message Date
Cameron Gutman 232b897abc Version 9.5 2020-05-16 21:40:41 -07:00
Cameron Gutman efd076bc6c Ignore absolute touch events from outside the stream view 2020-05-12 00:20:07 -07:00
Cameron Gutman cc877480ff Add an option for absolute touch mode 2020-05-11 23:53:49 -07:00
Christoph Papke 363145a284 Optimize button mapping for 8BitDo controllers (#826)
* Optimize button mapping for 8BitDo controllers #825
2020-05-05 16:04:31 -07:00
Cameron Gutman 755571ad33 Switch on-screen control buttons when flip face buttons is enabled 2020-05-04 22:23:03 -07:00
Eero Kelly 39edb55721 Add option to invert A/B X/Y (#824)
* Add option to invert A/B X/Y

* Remove redundant prefConfig
2020-05-04 22:10:35 -07:00
Cameron Gutman 15aa7ecc2e Add a friendly error message when no video traffic is received 2020-05-01 21:54:26 -07:00
Cameron Gutman ce9e91153e Add special error text for the -1 launch error code 2020-04-25 16:10:44 -07:00
Cameron Gutman 9ee0a46606 Add new init packet to switch out of BT mode 2020-04-24 17:47:31 -07:00
Cameron Gutman 20dc351f4c Fix parsing rare GFE status code of 0xFFFFFFFF 2020-04-23 18:47:01 -07:00
Cameron Gutman c30c54d562 Version 9.2.1 2020-04-23 18:40:40 -07:00
Cameron Gutman 45ff51c0d2 Fix mouse jumping on Shield devices when clicking or scrolling 2020-04-23 00:13:19 -07:00
Cameron Gutman 5b86e99138 Improve dead zone precision for stylus input 2020-04-22 22:46:05 -07:00
Cameron Gutman 0c72910eb7 Fix tap location for styluses without hover support 2020-04-22 22:00:25 -07:00
20 changed files with 521 additions and 78 deletions
+2 -2
View File
@@ -7,8 +7,8 @@ android {
minSdkVersion 16
targetSdkVersion 29
versionName "9.2"
versionCode = 222
versionName "9.5"
versionCode = 224
}
flavorDimensions "root"
+99 -43
View File
@@ -6,9 +6,11 @@ import com.limelight.binding.input.ControllerHandler;
import com.limelight.binding.input.KeyboardTranslator;
import com.limelight.binding.input.capture.InputCaptureManager;
import com.limelight.binding.input.capture.InputCaptureProvider;
import com.limelight.binding.input.TouchContext;
import com.limelight.binding.input.touch.AbsoluteTouchContext;
import com.limelight.binding.input.touch.RelativeTouchContext;
import com.limelight.binding.input.driver.UsbDriverService;
import com.limelight.binding.input.evdev.EvdevListener;
import com.limelight.binding.input.touch.TouchContext;
import com.limelight.binding.input.virtual_controller.VirtualController;
import com.limelight.binding.video.CrashListener;
import com.limelight.binding.video.MediaCodecDecoderRenderer;
@@ -95,8 +97,11 @@ public class Game extends Activity implements SurfaceHolder.Callback,
private static final int REFERENCE_HORIZ_RES = 1280;
private static final int REFERENCE_VERT_RES = 720;
private static final int STYLUS_DEAD_ZONE_DELAY = 250;
private static final int STYLUS_DEAD_ZONE_RADIUS = 50;
private static final int STYLUS_DOWN_DEAD_ZONE_DELAY = 100;
private static final int STYLUS_DOWN_DEAD_ZONE_RADIUS = 20;
private static final int STYLUS_UP_DEAD_ZONE_DELAY = 150;
private static final int STYLUS_UP_DEAD_ZONE_RADIUS = 50;
private static final int THREE_FINGER_TAP_THRESHOLD = 300;
@@ -119,8 +124,10 @@ public class Game extends Activity implements SurfaceHolder.Callback,
private boolean grabbedInput = true;
private boolean grabComboDown = false;
private StreamView streamView;
private long stylusDownTime = 0;
private float stylusDownX, stylusDownY;
private long lastAbsTouchUpTime = 0;
private long lastAbsTouchDownTime = 0;
private float lastAbsTouchUpX, lastAbsTouchUpY;
private float lastAbsTouchDownX, lastAbsTouchDownY;
private boolean isHidingOverlays;
private TextView notificationOverlayView;
@@ -469,9 +476,14 @@ public class Game extends Activity implements SurfaceHolder.Callback,
// Initialize touch contexts
for (int i = 0; i < touchContextMap.length; i++) {
touchContextMap[i] = new TouchContext(conn, i,
REFERENCE_HORIZ_RES, REFERENCE_VERT_RES,
streamView);
if (!prefConfig.touchscreenTrackpad) {
touchContextMap[i] = new AbsoluteTouchContext(conn, i, streamView);
}
else {
touchContextMap[i] = new RelativeTouchContext(conn, i,
REFERENCE_HORIZ_RES, REFERENCE_VERT_RES,
streamView);
}
}
// Use sustained performance mode on N+ to ensure consistent
@@ -1176,6 +1188,23 @@ public class Game extends Activity implements SurfaceHolder.Callback,
return true;
}
// Always update the position before sending any button events. If we're
// dealing with a stylus without hover support, our position might be
// significantly different than before.
if (inputCaptureProvider.eventHasRelativeMouseAxes(event)) {
// Send the deltas straight from the motion event
short deltaX = (short)inputCaptureProvider.getRelativeAxisX(event);
short deltaY = (short)inputCaptureProvider.getRelativeAxisY(event);
if (deltaX != 0 || deltaY != 0) {
conn.sendMouseMove(deltaX, deltaY);
}
}
else if (view != null) {
// Otherwise send absolute position
updateMousePosition(view, event);
}
if (event.getActionMasked() == MotionEvent.ACTION_SCROLL) {
// Send the vertical scroll packet
byte vScrollClicks = (byte) event.getAxisValue(MotionEvent.AXIS_VSCROLL);
@@ -1235,16 +1264,16 @@ public class Game extends Activity implements SurfaceHolder.Callback,
if (event.getPointerCount() == 1 && event.getActionIndex() == 0) {
if (event.getActionMasked() == MotionEvent.ACTION_DOWN) {
if (event.getToolType(0) == MotionEvent.TOOL_TYPE_STYLUS) {
stylusDownTime = SystemClock.uptimeMillis();
stylusDownX = event.getX(0);
stylusDownY = event.getY(0);
lastAbsTouchDownTime = SystemClock.uptimeMillis();
lastAbsTouchDownX = event.getX(0);
lastAbsTouchDownY = event.getY(0);
// Stylus is left click
conn.sendMouseButtonDown(MouseButtonPacket.BUTTON_LEFT);
} else if (event.getToolType(0) == MotionEvent.TOOL_TYPE_ERASER) {
stylusDownTime = SystemClock.uptimeMillis();
stylusDownX = event.getX(0);
stylusDownY = event.getY(0);
lastAbsTouchDownTime = SystemClock.uptimeMillis();
lastAbsTouchDownX = event.getX(0);
lastAbsTouchDownY = event.getY(0);
// Eraser is right click
conn.sendMouseButtonDown(MouseButtonPacket.BUTTON_RIGHT);
@@ -1252,18 +1281,16 @@ public class Game extends Activity implements SurfaceHolder.Callback,
}
else if (event.getActionMasked() == MotionEvent.ACTION_UP || event.getActionMasked() == MotionEvent.ACTION_CANCEL) {
if (event.getToolType(0) == MotionEvent.TOOL_TYPE_STYLUS) {
// It looks odd to set these on ACTION_UP, but it makes sense.
// The last "down" position is actually when we come up.
stylusDownTime = SystemClock.uptimeMillis();
stylusDownX = event.getX(0);
stylusDownY = event.getY(0);
lastAbsTouchUpTime = SystemClock.uptimeMillis();
lastAbsTouchUpX = event.getX(0);
lastAbsTouchUpY = event.getY(0);
// Stylus is left click
conn.sendMouseButtonUp(MouseButtonPacket.BUTTON_LEFT);
} else if (event.getToolType(0) == MotionEvent.TOOL_TYPE_ERASER) {
stylusDownTime = SystemClock.uptimeMillis();
stylusDownX = event.getX(0);
stylusDownY = event.getY(0);
lastAbsTouchUpTime = SystemClock.uptimeMillis();
lastAbsTouchUpX = event.getX(0);
lastAbsTouchUpY = event.getY(0);
// Eraser is right click
conn.sendMouseButtonUp(MouseButtonPacket.BUTTON_RIGHT);
@@ -1271,17 +1298,6 @@ public class Game extends Activity implements SurfaceHolder.Callback,
}
}
// Get relative axis values if we can
if (inputCaptureProvider.eventHasRelativeMouseAxes(event)) {
// Send the deltas straight from the motion event
conn.sendMouseMove((short) inputCaptureProvider.getRelativeAxisX(event),
(short) inputCaptureProvider.getRelativeAxisY(event));
}
else if (view != null) {
// Otherwise send absolute position
updateMousePosition(view, event);
}
lastButtonState = event.getButtonState();
}
// This case is for fingers
@@ -1294,6 +1310,11 @@ public class Game extends Activity implements SurfaceHolder.Callback,
return true;
}
if (view == null && !prefConfig.touchscreenTrackpad) {
// Absolute touch events should be dropped outside our view.
return true;
}
int actionIndex = event.getActionIndex();
int eventX = (int)event.getX(actionIndex);
@@ -1323,7 +1344,10 @@ public class Game extends Activity implements SurfaceHolder.Callback,
{
case MotionEvent.ACTION_POINTER_DOWN:
case MotionEvent.ACTION_DOWN:
context.touchDownEvent(eventX, eventY);
for (TouchContext touchContext : touchContextMap) {
touchContext.setPointerCount(event.getPointerCount());
}
context.touchDownEvent(eventX, eventY, true);
break;
case MotionEvent.ACTION_POINTER_UP:
case MotionEvent.ACTION_UP:
@@ -1336,9 +1360,12 @@ public class Game extends Activity implements SurfaceHolder.Callback,
}
}
context.touchUpEvent(eventX, eventY);
for (TouchContext touchContext : touchContextMap) {
touchContext.setPointerCount(event.getPointerCount() - 1);
}
if (actionIndex == 0 && event.getPointerCount() > 1 && !context.isCancelled()) {
// The original secondary touch now becomes primary
context.touchDownEvent((int)event.getX(1), (int)event.getY(1));
context.touchDownEvent((int)event.getX(1), (int)event.getY(1), false);
}
break;
case MotionEvent.ACTION_MOVE:
@@ -1370,6 +1397,7 @@ public class Game extends Activity implements SurfaceHolder.Callback,
case MotionEvent.ACTION_CANCEL:
for (TouchContext aTouchContext : touchContextMap) {
aTouchContext.cancelTouch();
aTouchContext.setPointerCount(0);
}
break;
default:
@@ -1403,13 +1431,29 @@ public class Game extends Activity implements SurfaceHolder.Callback,
float eventY = event.getY(0);
if (event.getPointerCount() == 1 && event.getActionIndex() == 0 &&
(event.getToolType(0) == MotionEvent.TOOL_TYPE_ERASER) ||
event.getToolType(0) == MotionEvent.TOOL_TYPE_STYLUS)
(event.getToolType(0) == MotionEvent.TOOL_TYPE_ERASER ||
event.getToolType(0) == MotionEvent.TOOL_TYPE_STYLUS))
{
if (SystemClock.uptimeMillis() - stylusDownTime <= STYLUS_DEAD_ZONE_DELAY &&
Math.sqrt(Math.pow(eventX - stylusDownX, 2) + Math.pow(eventY - stylusDownY, 2)) <= STYLUS_DEAD_ZONE_RADIUS) {
// Ignore small inputs shortly after the stylus has been pressed. This ensures users can reliably double click.
return;
switch (event.getActionMasked()) {
case MotionEvent.ACTION_DOWN:
case MotionEvent.ACTION_HOVER_ENTER:
case MotionEvent.ACTION_HOVER_EXIT:
case MotionEvent.ACTION_HOVER_MOVE:
if (SystemClock.uptimeMillis() - lastAbsTouchUpTime <= STYLUS_UP_DEAD_ZONE_DELAY &&
Math.sqrt(Math.pow(eventX - lastAbsTouchUpX, 2) + Math.pow(eventY - lastAbsTouchUpY, 2)) <= STYLUS_UP_DEAD_ZONE_RADIUS) {
// Enforce a small deadzone between touch up and hover or touch down to allow more precise double-clicking
return;
}
break;
case MotionEvent.ACTION_MOVE:
case MotionEvent.ACTION_UP:
if (SystemClock.uptimeMillis() - lastAbsTouchDownTime <= STYLUS_DOWN_DEAD_ZONE_DELAY &&
Math.sqrt(Math.pow(eventX - lastAbsTouchDownX, 2) + Math.pow(eventY - lastAbsTouchDownY, 2)) <= STYLUS_DOWN_DEAD_ZONE_RADIUS) {
// Enforce a small deadzone between touch down and move or touch up to allow more precise double-clicking
return;
}
break;
}
}
@@ -1513,9 +1557,21 @@ public class Game extends Activity implements SurfaceHolder.Callback,
// Display the error dialog if it was an unexpected termination.
// Otherwise, just finish the activity immediately.
if (errorCode != 0) {
if (errorCode != MoonBridge.ML_ERROR_GRACEFUL_TERMINATION) {
String message;
switch (errorCode) {
case MoonBridge.ML_ERROR_NO_VIDEO_TRAFFIC:
message = getResources().getString(R.string.no_video_received_error);
break;
default:
message = getResources().getString(R.string.conn_terminated_msg);
break;
}
Dialog.displayDialog(Game.this, getResources().getString(R.string.conn_terminated_title),
getResources().getString(R.string.conn_terminated_msg), true);
message, true);
}
else {
finish();
@@ -827,6 +827,11 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
}
}
// Override mode button for 8BitDo controllers
if (context.vendorId == 0x2dc8 && event.getScanCode() == 306) {
return KeyEvent.KEYCODE_BUTTON_MODE;
}
if (context.usesLinuxGamepadStandardFaceButtons) {
// Android's Generic.kl swaps BTN_NORTH and BTN_WEST
switch (event.getScanCode()) {
@@ -991,6 +996,21 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
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);
@@ -1256,6 +1276,11 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
}
int keyCode = handleRemapping(context, event);
if (prefConfig.flipFaceButtons) {
keyCode = handleFlipFaceButtons(keyCode);
}
if (keyCode == 0) {
return true;
}
@@ -1417,6 +1442,11 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
}
int keyCode = handleRemapping(context, event);
if (prefConfig.flipFaceButtons) {
keyCode = handleFlipFaceButtons(keyCode);
}
if (keyCode == 0) {
return true;
}
@@ -75,8 +75,10 @@ public class ShieldCaptureProvider extends InputCaptureProvider {
@Override
public boolean eventHasRelativeMouseAxes(MotionEvent event) {
return event.getAxisValue(AXIS_RELATIVE_X) != 0 ||
event.getAxisValue(AXIS_RELATIVE_Y) != 0;
// All mouse events should use relative axes, even if they are zero. This avoids triggering
// cursor jumps if we get an event with no associated motion, like ACTION_DOWN or ACTION_UP.
return event.getPointerCount() == 1 && event.getActionIndex() == 0 &&
event.getToolType(0) == MotionEvent.TOOL_TYPE_MOUSE;
}
@Override
@@ -25,6 +25,7 @@ public class XboxOneController extends AbstractXboxController {
};
private static final byte[] FW2015_INIT = {0x05, 0x20, 0x00, 0x01, 0x00};
private static final byte[] ONE_S_INIT = {0x05, 0x20, 0x00, 0x0f, 0x06};
private static final byte[] HORI_INIT = {0x01, 0x20, 0x00, 0x09, 0x00, 0x04, 0x20, 0x3a,
0x00, 0x00, 0x00, (byte)0x80, 0x00};
private static final byte[] PDP_INIT1 = {0x0a, 0x20, 0x00, 0x03, 0x00, 0x01, 0x14};
@@ -38,6 +39,8 @@ public class XboxOneController extends AbstractXboxController {
new InitPacket(0x0e6f, 0x0165, HORI_INIT),
new InitPacket(0x0f0d, 0x0067, HORI_INIT),
new InitPacket(0x0000, 0x0000, FW2015_INIT),
new InitPacket(0x045e, 0x02ea, ONE_S_INIT),
new InitPacket(0x045e, 0x0b00, ONE_S_INIT),
new InitPacket(0x0e6f, 0x0000, PDP_INIT1),
new InitPacket(0x0e6f, 0x0000, PDP_INIT2),
new InitPacket(0x24c6, 0x541a, RUMBLE_INIT1),
@@ -0,0 +1,272 @@
package com.limelight.binding.input.touch;
import android.os.SystemClock;
import android.view.View;
import com.limelight.nvstream.NvConnection;
import com.limelight.nvstream.input.MouseButtonPacket;
import java.util.Timer;
import java.util.TimerTask;
public class AbsoluteTouchContext implements TouchContext {
private int lastTouchDownX = 0;
private int lastTouchDownY = 0;
private long lastTouchDownTime = 0;
private int lastTouchUpX = 0;
private int lastTouchUpY = 0;
private long lastTouchUpTime = 0;
private int lastTouchLocationX = 0;
private int lastTouchLocationY = 0;
private boolean cancelled;
private boolean confirmedLongPress;
private boolean confirmedTap;
private Timer longPressTimer;
private Timer tapDownTimer;
private float accumulatedScrollDelta;
private final NvConnection conn;
private final int actionIndex;
private final View targetView;
private static final int SCROLL_SPEED_DIVISOR = 20;
private static final int LONG_PRESS_TIME_THRESHOLD = 650;
private static final int LONG_PRESS_DISTANCE_THRESHOLD = 30;
private static final int DOUBLE_TAP_TIME_THRESHOLD = 250;
private static final int DOUBLE_TAP_DISTANCE_THRESHOLD = 60;
private static final int TOUCH_DOWN_DEAD_ZONE_TIME_THRESHOLD = 100;
private static final int TOUCH_DOWN_DEAD_ZONE_DISTANCE_THRESHOLD = 20;
public AbsoluteTouchContext(NvConnection conn, int actionIndex, View view)
{
this.conn = conn;
this.actionIndex = actionIndex;
this.targetView = view;
}
@Override
public int getActionIndex()
{
return actionIndex;
}
@Override
public boolean touchDownEvent(int eventX, int eventY, boolean isNewFinger)
{
if (!isNewFinger) {
// We don't handle finger transitions for absolute mode
return true;
}
lastTouchLocationX = lastTouchDownX = eventX;
lastTouchLocationY = lastTouchDownY = eventY;
lastTouchDownTime = SystemClock.uptimeMillis();
cancelled = confirmedTap = confirmedLongPress = false;
accumulatedScrollDelta = 0;
if (actionIndex == 0) {
// Start the timers
startTapDownTimer();
startLongPressTimer();
}
return true;
}
private boolean distanceExceeds(int deltaX, int deltaY, double limit) {
return Math.sqrt(Math.pow(deltaX, 2) + Math.pow(deltaY, 2)) > limit;
}
private void updatePosition(int eventX, int eventY) {
// We may get values slightly outside our view region on ACTION_HOVER_ENTER and ACTION_HOVER_EXIT.
// Normalize these to the view size. We can't just drop them because we won't always get an event
// right at the boundary of the view, so dropping them would result in our cursor never really
// reaching the sides of the screen.
eventX = Math.min(Math.max(eventX, 0), targetView.getWidth());
eventY = Math.min(Math.max(eventY, 0), targetView.getHeight());
conn.sendMousePosition((short)eventX, (short)eventY, (short)targetView.getWidth(), (short)targetView.getHeight());
}
@Override
public void touchUpEvent(int eventX, int eventY)
{
if (cancelled) {
return;
}
if (actionIndex == 0) {
// Cancel the timers
cancelLongPressTimer();
cancelTapDownTimer();
// Raise the mouse buttons that we currently have down
if (confirmedLongPress) {
conn.sendMouseButtonUp(MouseButtonPacket.BUTTON_RIGHT);
}
else if (confirmedTap) {
conn.sendMouseButtonUp(MouseButtonPacket.BUTTON_LEFT);
}
else {
// If we get here, this means that the tap completed within the touch down
// deadzone time. We'll need to send the touch down and up events now at the
// original touch down position.
tapConfirmed();
try {
// FIXME: Sleeping on the main thread sucks
Thread.sleep(50);
} catch (InterruptedException ignored) {}
conn.sendMouseButtonUp(MouseButtonPacket.BUTTON_LEFT);
}
}
lastTouchLocationX = lastTouchUpX = eventX;
lastTouchLocationY = lastTouchUpY = eventY;
lastTouchUpTime = SystemClock.uptimeMillis();
}
private synchronized void startLongPressTimer() {
longPressTimer = new Timer(true);
longPressTimer.schedule(new TimerTask() {
@Override
public void run() {
synchronized (AbsoluteTouchContext.this) {
// Check if someone cancelled us
if (longPressTimer == null) {
return;
}
// Uncancellable now
longPressTimer = null;
// This timer should have already expired, but cancel it just in case
cancelTapDownTimer();
// Switch from a left click to a right click after a long press
confirmedLongPress = true;
if (confirmedTap) {
conn.sendMouseButtonUp(MouseButtonPacket.BUTTON_LEFT);
}
conn.sendMouseButtonDown(MouseButtonPacket.BUTTON_RIGHT);
}
}
}, LONG_PRESS_TIME_THRESHOLD);
}
private synchronized void cancelLongPressTimer() {
if (longPressTimer != null) {
longPressTimer.cancel();
longPressTimer = null;
}
}
private synchronized void startTapDownTimer() {
tapDownTimer = new Timer(true);
tapDownTimer.schedule(new TimerTask() {
@Override
public void run() {
synchronized (AbsoluteTouchContext.this) {
// Check if someone cancelled us
if (tapDownTimer == null) {
return;
}
// Uncancellable now
tapDownTimer = null;
// Start our tap
tapConfirmed();
}
}
}, TOUCH_DOWN_DEAD_ZONE_TIME_THRESHOLD);
}
private synchronized void cancelTapDownTimer() {
if (tapDownTimer != null) {
tapDownTimer.cancel();
tapDownTimer = null;
}
}
private void tapConfirmed() {
if (confirmedTap || confirmedLongPress) {
return;
}
confirmedTap = true;
cancelTapDownTimer();
// Left button down at original position
if (lastTouchDownTime - lastTouchUpTime > DOUBLE_TAP_TIME_THRESHOLD ||
distanceExceeds(lastTouchDownX - lastTouchUpX, lastTouchDownY - lastTouchUpY, DOUBLE_TAP_DISTANCE_THRESHOLD)) {
// Don't reposition for finger down events within the deadzone. This makes double-clicking easier.
updatePosition(lastTouchDownX, lastTouchDownY);
}
conn.sendMouseButtonDown(MouseButtonPacket.BUTTON_LEFT);
}
@Override
public boolean touchMoveEvent(int eventX, int eventY)
{
if (cancelled) {
return true;
}
if (actionIndex == 0) {
if (distanceExceeds(eventX - lastTouchDownX, eventY - lastTouchDownY, LONG_PRESS_DISTANCE_THRESHOLD)) {
// Moved too far since touch down. Cancel the long press timer.
cancelLongPressTimer();
}
// Ignore motion within the deadzone period after touch down
if (confirmedTap || distanceExceeds(eventX - lastTouchDownX, eventY - lastTouchDownY, TOUCH_DOWN_DEAD_ZONE_DISTANCE_THRESHOLD)) {
tapConfirmed();
updatePosition(eventX, eventY);
}
}
else if (actionIndex == 1) {
accumulatedScrollDelta += (eventY - lastTouchLocationY) / (float)SCROLL_SPEED_DIVISOR;
if ((short)accumulatedScrollDelta != 0) {
conn.sendMouseHighResScroll((short)accumulatedScrollDelta);
accumulatedScrollDelta -= (short)accumulatedScrollDelta;
}
}
lastTouchLocationX = eventX;
lastTouchLocationY = eventY;
return true;
}
@Override
public void cancelTouch() {
cancelled = true;
// Cancel the timers
cancelLongPressTimer();
cancelTapDownTimer();
// Raise the mouse buttons
if (confirmedLongPress) {
conn.sendMouseButtonUp(MouseButtonPacket.BUTTON_RIGHT);
}
else if (confirmedTap) {
conn.sendMouseButtonUp(MouseButtonPacket.BUTTON_LEFT);
}
}
@Override
public boolean isCancelled() {
return cancelled;
}
@Override
public void setPointerCount(int pointerCount) {
if (actionIndex == 0 && pointerCount > 1) {
cancelTouch();
}
}
}
@@ -1,5 +1,6 @@
package com.limelight.binding.input;
package com.limelight.binding.input.touch;
import android.os.SystemClock;
import android.view.View;
import com.limelight.nvstream.NvConnection;
@@ -8,7 +9,7 @@ import com.limelight.nvstream.input.MouseButtonPacket;
import java.util.Timer;
import java.util.TimerTask;
public class TouchContext {
public class RelativeTouchContext implements TouchContext {
private int lastTouchX = 0;
private int lastTouchY = 0;
private int originalTouchX = 0;
@@ -32,8 +33,8 @@ public class TouchContext {
private static final int TAP_TIME_THRESHOLD = 250;
private static final int DRAG_TIME_THRESHOLD = 650;
public TouchContext(NvConnection conn, int actionIndex,
int referenceWidth, int referenceHeight, View view)
public RelativeTouchContext(NvConnection conn, int actionIndex,
int referenceWidth, int referenceHeight, View view)
{
this.conn = conn;
this.actionIndex = actionIndex;
@@ -42,6 +43,7 @@ public class TouchContext {
this.targetView = view;
}
@Override
public int getActionIndex()
{
return actionIndex;
@@ -57,7 +59,7 @@ public class TouchContext {
private boolean isTap()
{
long timeDelta = System.currentTimeMillis() - originalTouchTime;
long timeDelta = SystemClock.uptimeMillis() - originalTouchTime;
return isWithinTapBounds(lastTouchX, lastTouchY) && timeDelta <= TAP_TIME_THRESHOLD;
}
@@ -72,7 +74,8 @@ public class TouchContext {
}
}
public boolean touchDownEvent(int eventX, int eventY)
@Override
public boolean touchDownEvent(int eventX, int eventY, boolean isNewFinger)
{
// Get the view dimensions to scale inputs on this touch
xFactor = referenceWidth / (double)targetView.getWidth();
@@ -80,7 +83,7 @@ public class TouchContext {
originalTouchX = lastTouchX = eventX;
originalTouchY = lastTouchY = eventY;
originalTouchTime = System.currentTimeMillis();
originalTouchTime = SystemClock.uptimeMillis();
cancelled = confirmedDrag = confirmedMove = false;
distanceMoved = 0;
@@ -92,6 +95,7 @@ public class TouchContext {
return true;
}
@Override
public void touchUpEvent(int eventX, int eventY)
{
if (cancelled) {
@@ -128,7 +132,7 @@ public class TouchContext {
dragTimer.schedule(new TimerTask() {
@Override
public void run() {
synchronized (TouchContext.this) {
synchronized (RelativeTouchContext.this) {
// Check if someone already set move
if (confirmedMove) {
return;
@@ -179,6 +183,7 @@ public class TouchContext {
}
}
@Override
public boolean touchMoveEvent(int eventX, int eventY)
{
if (eventX != lastTouchX || eventY != lastTouchY)
@@ -223,6 +228,7 @@ public class TouchContext {
return true;
}
@Override
public void cancelTouch() {
cancelled = true;
@@ -235,7 +241,11 @@ public class TouchContext {
}
}
@Override
public boolean isCancelled() {
return cancelled;
}
@Override
public void setPointerCount(int pointerCount) {}
}
@@ -0,0 +1,11 @@
package com.limelight.binding.input.touch;
public interface TouchContext {
int getActionIndex();
void setPointerCount(int pointerCount);
boolean touchDownEvent(int eventX, int eventY, boolean isNewFinger);
boolean touchMoveEvent(int eventX, int eventY);
void touchUpEvent(int eventX, int eventY);
void cancelTouch();
boolean isCancelled();
}
@@ -205,7 +205,8 @@ public class VirtualControllerConfigurationLoader {
controller.addElement(createDigitalButton(
VirtualControllerElement.EID_A,
ControllerPacket.A_FLAG, 0, 1, "A", -1, controller, context),
!config.flipFaceButtons ? ControllerPacket.A_FLAG : ControllerPacket.B_FLAG, 0, 1,
!config.flipFaceButtons ? "A" : "B", -1, controller, context),
screenScale(BUTTON_BASE_X, height) + rightDisplacement,
screenScale(BUTTON_BASE_Y + 2 * BUTTON_SIZE, height),
screenScale(BUTTON_SIZE, height),
@@ -214,7 +215,8 @@ public class VirtualControllerConfigurationLoader {
controller.addElement(createDigitalButton(
VirtualControllerElement.EID_B,
ControllerPacket.B_FLAG, 0, 1, "B", -1, controller, context),
config.flipFaceButtons ? ControllerPacket.A_FLAG : ControllerPacket.B_FLAG, 0, 1,
config.flipFaceButtons ? "A" : "B", -1, controller, context),
screenScale(BUTTON_BASE_X + BUTTON_SIZE, height) + rightDisplacement,
screenScale(BUTTON_BASE_Y + BUTTON_SIZE, height),
screenScale(BUTTON_SIZE, height),
@@ -223,7 +225,8 @@ public class VirtualControllerConfigurationLoader {
controller.addElement(createDigitalButton(
VirtualControllerElement.EID_X,
ControllerPacket.X_FLAG, 0, 1, "X", -1, controller, context),
!config.flipFaceButtons ? ControllerPacket.X_FLAG : ControllerPacket.Y_FLAG, 0, 1,
!config.flipFaceButtons ? "X" : "Y", -1, controller, context),
screenScale(BUTTON_BASE_X - BUTTON_SIZE, height) + rightDisplacement,
screenScale(BUTTON_BASE_Y + BUTTON_SIZE, height),
screenScale(BUTTON_SIZE, height),
@@ -232,7 +235,8 @@ public class VirtualControllerConfigurationLoader {
controller.addElement(createDigitalButton(
VirtualControllerElement.EID_Y,
ControllerPacket.Y_FLAG, 0, 1, "Y", -1, controller, context),
config.flipFaceButtons ? ControllerPacket.X_FLAG : ControllerPacket.Y_FLAG, 0, 1,
config.flipFaceButtons ? "X" : "Y", -1, controller, context),
screenScale(BUTTON_BASE_X, height) + rightDisplacement,
screenScale(BUTTON_BASE_Y, height),
screenScale(BUTTON_SIZE, height),
@@ -348,6 +348,12 @@ public class NvConnection {
}
}
public void sendMouseHighResScroll(final short scrollAmount) {
if (!isMonkey) {
MoonBridge.sendMouseHighResScroll(scrollAmount);
}
}
public static String findExternalAddressForMdns(String stunHostname, int stunPort) {
return MoonBridge.findExternalAddressIP4(stunHostname, stunPort);
}
@@ -186,20 +186,20 @@ public class NvHTTP {
}
private static void verifyResponseStatus(XmlPullParser xpp) throws GfeHttpResponseException {
String statusCodeText = xpp.getAttributeValue(XmlPullParser.NO_NAMESPACE, "status_code");
if (statusCodeText == null) {
throw new GfeHttpResponseException(418, "Status code is missing");
}
try {
int statusCode = Integer.parseInt(statusCodeText);
if (statusCode != 200) {
throw new GfeHttpResponseException(statusCode, xpp.getAttributeValue(XmlPullParser.NO_NAMESPACE, "status_message"));
// We use Long.parseLong() because in rare cases GFE can send back a status code of
// 0xFFFFFFFF, which will cause Integer.parseInt() to throw a NumberFormatException due
// to exceeding Integer.MAX_VALUE. We'll get the desired error code of -1 by just casting
// the resulting long into an int.
int statusCode = (int)Long.parseLong(xpp.getAttributeValue(XmlPullParser.NO_NAMESPACE, "status_code"));
if (statusCode != 200) {
String statusMsg = xpp.getAttributeValue(XmlPullParser.NO_NAMESPACE, "status_message");
if (statusCode == -1 && "Invalid".equals(statusMsg)) {
// Special case handling an audio capture error which GFE doesn't
// provide any useful status message for.
statusCode = 418;
statusMsg = "Missing audio capture device. Reinstall GeForce Experience.";
}
}
catch (NumberFormatException e) {
// It seems like GFE 3.20.3.63 is returning garbage for status_code in rare cases.
// Surface this in a more friendly way rather than crashing.
throw new GfeHttpResponseException(418, "Status code is not a number: "+statusCodeText);
throw new GfeHttpResponseException(statusCode, statusMsg);
}
}
@@ -33,6 +33,9 @@ public class MoonBridge {
public static final int CONN_STATUS_OKAY = 0;
public static final int CONN_STATUS_POOR = 1;
public static final int ML_ERROR_GRACEFUL_TERMINATION = 0;
public static final int ML_ERROR_NO_VIDEO_TRAFFIC = -100;
private static AudioRenderer audioRenderer;
private static VideoDecoderRenderer videoRenderer;
private static NvConnectionListener connectionListener;
@@ -258,6 +261,8 @@ public class MoonBridge {
public static native void sendMouseScroll(byte scrollClicks);
public static native void sendMouseHighResScroll(short scrollAmount);
public static native String getStageName(int stage);
public static native String findExternalAddressIP4(String stunHostName, int stunPort);
@@ -41,6 +41,8 @@ public class PreferenceConfiguration {
static final String UNLOCK_FPS_STRING = "checkbox_unlock_fps";
private static final String VIBRATE_OSC_PREF_STRING = "checkbox_vibrate_osc";
private static final String VIBRATE_FALLBACK_PREF_STRING = "checkbox_vibrate_fallback";
private static final String FLIP_FACE_BUTTONS_PREF_STRING = "checkbox_flip_face_buttons";
private static final String TOUCHSCREEN_TRACKPAD_PREF_STRING = "checkbox_touchscreen_trackpad";
static final String DEFAULT_RESOLUTION = "720p";
static final String DEFAULT_FPS = "60";
@@ -67,6 +69,8 @@ public class PreferenceConfiguration {
private static final boolean DEFAULT_UNLOCK_FPS = false;
private static final boolean DEFAULT_VIBRATE_OSC = true;
private static final boolean DEFAULT_VIBRATE_FALLBACK = false;
private static final boolean DEFAULT_FLIP_FACE_BUTTONS = false;
private static final boolean DEFAULT_TOUCHSCREEN_TRACKPAD = true;
private static final String DEFAULT_AUDIO_CONFIG = "2"; // Stereo
public static final int FORCE_H265_ON = -1;
@@ -80,7 +84,7 @@ public class PreferenceConfiguration {
public int oscOpacity;
public boolean stretchVideo, enableSops, playHostAudio, disableWarnings;
public String language;
public boolean listMode, smallIconMode, multiController, usbDriver;
public boolean listMode, smallIconMode, multiController, usbDriver, flipFaceButtons;
public boolean onscreenController;
public boolean onlyL3R3;
public boolean disableFrameDrop;
@@ -93,6 +97,7 @@ public class PreferenceConfiguration {
public boolean unlockFps;
public boolean vibrateOsc;
public boolean vibrateFallbackToDevice;
public boolean touchscreenTrackpad;
public MoonBridge.AudioConfiguration audioConfiguration;
private static int getHeightFromResolutionString(String resString) {
@@ -368,6 +373,8 @@ public class PreferenceConfiguration {
config.unlockFps = prefs.getBoolean(UNLOCK_FPS_STRING, DEFAULT_UNLOCK_FPS);
config.vibrateOsc = prefs.getBoolean(VIBRATE_OSC_PREF_STRING, DEFAULT_VIBRATE_OSC);
config.vibrateFallbackToDevice = prefs.getBoolean(VIBRATE_FALLBACK_PREF_STRING, DEFAULT_VIBRATE_FALLBACK);
config.flipFaceButtons = prefs.getBoolean(FLIP_FACE_BUTTONS_PREF_STRING, DEFAULT_FLIP_FACE_BUTTONS);
config.touchscreenTrackpad = prefs.getBoolean(TOUCHSCREEN_TRACKPAD_PREF_STRING, DEFAULT_TOUCHSCREEN_TRACKPAD);
return config;
}
@@ -141,9 +141,17 @@ public class StreamSettings extends Activity {
// hide on-screen controls category on non touch screen devices
if (!getActivity().getPackageManager().
hasSystemFeature("android.hardware.touchscreen")) {
PreferenceCategory category =
(PreferenceCategory) findPreference("category_onscreen_controls");
screen.removePreference(category);
{
PreferenceCategory category =
(PreferenceCategory) findPreference("category_onscreen_controls");
screen.removePreference(category);
}
{
PreferenceCategory category =
(PreferenceCategory) findPreference("category_input_settings");
category.removePreference(findPreference("checkbox_touchscreen_trackpad"));
}
}
// Remove PiP mode on devices pre-Oreo
@@ -49,6 +49,11 @@ Java_com_limelight_nvstream_jni_MoonBridge_sendMouseScroll(JNIEnv *env, jclass c
LiSendScrollEvent(scrollClicks);
}
JNIEXPORT void JNICALL
Java_com_limelight_nvstream_jni_MoonBridge_sendMouseHighResScroll(JNIEnv *env, jclass clazz, jshort scrollAmount) {
LiSendHighResScrollEvent(scrollAmount);
}
JNIEXPORT void JNICALL
Java_com_limelight_nvstream_jni_MoonBridge_stopConnection(JNIEnv *env, jclass clazz) {
LiStopConnection();
+5
View File
@@ -61,6 +61,7 @@
<string name="error_usb_prohibited">USB access is prohibited by your device administrator. Check your Knox or MDM settings.</string>
<string name="unable_to_pin_shortcut">Your current launcher does not allow for creating pinned shortcuts.</string>
<string name="video_decoder_init_failed">Video decoder failed to initialize. Your device may not support the selected resolution or frame rate.</string>
<string name="no_video_received_error">No video received from host. Check the host PC\'s firewall and port forwarding rules.</string>
<!-- Start application messages -->
<string name="conn_establishing_title">Establishing Connection</string>
@@ -139,6 +140,8 @@
<string name="summary_audio_config_list">Enable 5.1 or 7.1 surround sound for home-theater systems</string>
<string name="category_input_settings">Input Settings</string>
<string name="title_checkbox_touchscreen_trackpad">Use the touchscreen as a trackpad</string>
<string name="summary_checkbox_touchscreen_trackpad">If enabled, the touchscreen acts like a trackpad. If disabled, the touchscreen directly controls the mouse cursor.</string>
<string name="title_checkbox_multi_controller">Automatic gamepad presence detection</string>
<string name="summary_checkbox_multi_controller">Unchecking this option forces a gamepad to always be present</string>
<string name="title_checkbox_vibrate_fallback">Emulate rumble support with vibration</string>
@@ -153,6 +156,8 @@
<string name="summary_checkbox_mouse_emulation">Long pressing the Start button will switch the gamepad into mouse mode</string>
<string name="title_checkbox_mouse_nav_buttons">Enable back and forward mouse buttons</string>
<string name="summary_checkbox_mouse_nav_buttons">Enabling this option may break right clicking on some buggy devices</string>
<string name="title_checkbox_flip_face_buttons">Flip face buttons</string>
<string name="summary_checkbox_flip_face_buttons">Switches the face buttons A/B and X/Y for gamepads and the on-screen controls</string>
<string name="category_on_screen_controls_settings">On-screen Controls Settings</string>
<string name="title_checkbox_show_onscreen_controls">Show on-screen controls</string>
+12 -1
View File
@@ -51,13 +51,19 @@
android:entryValues="@array/audio_config_values"
android:defaultValue="2" />
</PreferenceCategory>
<PreferenceCategory android:title="@string/category_input_settings">
<PreferenceCategory android:title="@string/category_input_settings"
android:key="category_input_settings">
<!--com.limelight.preferences.SeekBarPreference
android:key="seekbar_deadzone"
android:defaultValue="15"
android:max="50"
android:text="@string/suffix_seekbar_deadzone"
android:title="@string/title_seekbar_deadzone"/-->
<CheckBoxPreference
android:key="checkbox_touchscreen_trackpad"
android:title="@string/title_checkbox_touchscreen_trackpad"
android:summary="@string/summary_checkbox_touchscreen_trackpad"
android:defaultValue="true" />
<CheckBoxPreference
android:key="checkbox_multi_controller"
android:title="@string/title_checkbox_multi_controller"
@@ -89,6 +95,11 @@
android:title="@string/title_checkbox_vibrate_fallback"
android:summary="@string/summary_checkbox_vibrate_fallback"
android:defaultValue="false" />
<CheckBoxPreference
android:key="checkbox_flip_face_buttons"
android:title="@string/title_checkbox_flip_face_buttons"
android:summary="@string/summary_checkbox_flip_face_buttons"
android:defaultValue="false" />
</PreferenceCategory>
<PreferenceCategory android:title="@string/category_on_screen_controls_settings"
android:key="category_onscreen_controls">
@@ -0,0 +1,3 @@
- Improved stylus deadzone behavior
- Fixed incorrect mouse position on styluses without hover support
- Fixed mouse clicks causing cursor jumping on Nvidia Shield devices
@@ -0,0 +1,5 @@
- Direct touchscreen mouse control is now supported! (disable touchscreen trackpad mode in settings)
- Added an option to switch the A/B and X/Y face buttons on gamepads and on-screen controls
- Added a special error message if no video was received instead of just displaying a black screen
- Improved support for Xbox One S controllers using the new 4.8 firmware
- Improved handling of audio capture errors reported by GeForce Experience