Compare commits
143 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 56625dfe4b | |||
| 2eab5a3b7b | |||
| f9e811862a | |||
| 25ccc3d0e1 | |||
| 8853bf0670 | |||
| 71fa3a824b | |||
| 56fd50834c | |||
| 48ba812cf6 | |||
| 019dc6d45f | |||
| cbcb784a79 | |||
| 39fa0258ad | |||
| d0dd5bfa8c | |||
| b948c47618 | |||
| 18cae8ac53 | |||
| 0576231dfc | |||
| 6ad35a83dd | |||
| 33d4dfc745 | |||
| f3bf63a668 | |||
| 2dbb7395a4 | |||
| 7c1eb80d62 | |||
| f2bf093691 | |||
| 2f002bfa4a | |||
| 4a19038d54 | |||
| 15fb3dd92c | |||
| e0982d3961 | |||
| 7fb2f15f54 | |||
| f93dbb4116 | |||
| bc34fe3a9f | |||
| bbe49491c1 | |||
| d5ccb80f26 | |||
| 50fd15379a | |||
| ed479f1155 | |||
| 04db9ba714 | |||
| 6a973e3248 | |||
| 96d9e4977b | |||
| 5a3897f22a | |||
| ceef00b79a | |||
| 94ee24ea11 | |||
| 1a201f2e94 | |||
| e0c6d41d4b | |||
| 44a0ae86d2 | |||
| 06822ad385 | |||
| 3be52280ba | |||
| 5142f978cf | |||
| 667ffd4dfd | |||
| 17626f1853 | |||
| 5c79567a2c | |||
| 0f5fd9af62 | |||
| 99643537d1 | |||
| 47650386e0 | |||
| aa3fc34646 | |||
| 92f5f1ac71 | |||
| eb739f73c7 | |||
| 20a646106b | |||
| 0dc14517cd | |||
| 04713c007b | |||
| 1cac7660b8 | |||
| edb286f9af | |||
| fb15ff99ca | |||
| a455e75e37 | |||
| 2b452e51f9 | |||
| 9d2b6f8854 | |||
| 3be10a1b59 | |||
| 01950c25a8 | |||
| 7ad1ebd0e8 | |||
| ee01a8b5a0 | |||
| 23c54f6813 | |||
| ceef4510fb | |||
| 042a6b943e | |||
| e114b73654 | |||
| da0a505978 | |||
| cb6d4a385c | |||
| 2806aee0fc | |||
| 52736f5162 | |||
| 6d45ad7fe8 | |||
| 2fc53644bc | |||
| b33eaec493 | |||
| 63d6f3ac78 | |||
| fd4caac013 | |||
| ada875cdb0 | |||
| 49ddfa573d | |||
| b58ac367ee | |||
| cf62b4ed95 | |||
| b05c62e141 | |||
| 095556106c | |||
| 5cdd72a45c | |||
| 5d84f8af43 | |||
| d9483d9214 | |||
| 250475830f | |||
| b8a0a823e0 | |||
| 6a54d669a3 | |||
| 62559c4e66 | |||
| e04ecaaf7a | |||
| fa4706c95f | |||
| 7067c0e02e | |||
| cc71ce6180 | |||
| f409a3583c | |||
| ac7504e017 | |||
| 345bd3f7c1 | |||
| 2e2960ec69 | |||
| e93b103d1e | |||
| 22977a4c5b | |||
| 7da5d5322b | |||
| 49e2c40ba4 | |||
| 8041a004c2 | |||
| db62d78e04 | |||
| bd79318b1e | |||
| 2736bd9165 | |||
| b6bd48584f | |||
| 7b4f3c975a | |||
| b165fadc55 | |||
| 274e0d0557 | |||
| 7594e51a18 | |||
| bf22819b53 | |||
| 3dea4b15e0 | |||
| 5836b3292b | |||
| a8fd49a234 | |||
| 006ad72eb2 | |||
| dc254e1ee5 | |||
| b0d31a4d35 | |||
| 24155feea4 | |||
| db0a4e35c6 | |||
| 68ef98d346 | |||
| f23bb9fac1 | |||
| d20dde0b6d | |||
| f76b30d109 | |||
| ee1a047cde | |||
| 4c533fedfd | |||
| f8ab7b8e13 | |||
| 46c5eaf0e1 | |||
| 1d6b5a35bd | |||
| 1ff6ee14ac | |||
| d2e51e97c0 | |||
| 9f94465979 | |||
| d83526ff5c | |||
| 1d6b7e1b2e | |||
| 1c9458d056 | |||
| 4e29f2ae8b | |||
| 69321636b5 | |||
| d190b254bd | |||
| 005a96f3d3 | |||
| e39e0910a1 | |||
| 56a6cee8f2 |
@@ -0,0 +1,3 @@
|
||||
[submodule "app/src/main/jni/jnienet/enet"]
|
||||
path = app/src/main/jni/jnienet/enet
|
||||
url = https://github.com/cgutman/enet.git
|
||||
+14
-3
@@ -5,14 +5,14 @@ apply plugin: 'com.android.application'
|
||||
|
||||
android {
|
||||
compileSdkVersion 23
|
||||
buildToolsVersion "23.0.2"
|
||||
buildToolsVersion "23.0.3"
|
||||
|
||||
defaultConfig {
|
||||
minSdkVersion 16
|
||||
targetSdkVersion 23
|
||||
|
||||
versionName "4.0.2"
|
||||
versionCode = 79
|
||||
versionName "4.5.10"
|
||||
versionCode = 101
|
||||
}
|
||||
|
||||
productFlavors {
|
||||
@@ -25,6 +25,10 @@ android {
|
||||
}
|
||||
}
|
||||
|
||||
lintOptions {
|
||||
disable 'MissingTranslation'
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
minifyEnabled false
|
||||
@@ -32,6 +36,13 @@ android {
|
||||
}
|
||||
}
|
||||
|
||||
// These lines are required to avoid dexing issues with the BouncyCastle library
|
||||
// bundled with limelight-common.jar
|
||||
packagingOptions {
|
||||
exclude 'META-INF/BCKEY.SF'
|
||||
exclude 'META-INF/BCKEY.DSA'
|
||||
}
|
||||
|
||||
sourceSets.main.jni.srcDirs = []
|
||||
|
||||
//noinspection GroovyAssignabilityCheck,GroovyAssignabilityCheck
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -12,17 +12,18 @@
|
||||
<uses-feature android:name="android.hardware.wifi" android:required="false" />
|
||||
<uses-feature android:name="android.hardware.gamepad" android:required="false" />
|
||||
<uses-feature android:name="android.hardware.usb.host" android:required="false" />
|
||||
<uses-feature android:name="android.software.leanback" android:required="false" />
|
||||
|
||||
<application
|
||||
android:allowBackup="true"
|
||||
android:icon="@drawable/ic_launcher"
|
||||
android:banner="@drawable/atv_banner"
|
||||
android:theme="@style/AppTheme" >
|
||||
|
||||
<!-- Samsung multi-window support -->
|
||||
<uses-library android:name="com.sec.android.app.multiwindow" android:required="false" />
|
||||
<meta-data android:name="com.sec.android.support.multiwindow" android:value="true" />
|
||||
|
||||
<!-- Launcher for traditional devices -->
|
||||
<activity
|
||||
android:name=".PcView"
|
||||
android:configChanges="mcc|mnc|locale|touchscreen|keyboard|keyboardHidden|navigation|screenLayout|fontScale|uiMode|orientation|screenSize|smallestScreenSize|layoutDirection">
|
||||
@@ -30,25 +31,13 @@
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
<category android:name="android.intent.category.MULTIWINDOW_LAUNCHER" />
|
||||
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
|
||||
<category android:name="tv.ouya.intent.category.APP" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<!-- Launcher for Android TV devices -->
|
||||
<activity
|
||||
android:name=".PcViewTv"
|
||||
android:logo="@drawable/atv_banner"
|
||||
android:icon="@drawable/atv_banner"
|
||||
android:configChanges="mcc|mnc|locale|touchscreen|keyboard|keyboardHidden|navigation|screenLayout|fontScale|uiMode|orientation|screenSize|smallestScreenSize|layoutDirection">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:name=".AppView"
|
||||
android:configChanges="mcc|mnc|locale|touchscreen|keyboard|keyboardHidden|navigation|screenLayout|fontScale|uiMode|orientation|screenSize|smallestScreenSize|layoutDirection" >
|
||||
android:configChanges="mcc|mnc|locale|touchscreen|keyboard|keyboardHidden|navigation|fontScale|uiMode" >
|
||||
<meta-data
|
||||
android:name="android.support.PARENT_ACTIVITY"
|
||||
android:value="com.limelight.PcView" />
|
||||
@@ -76,6 +65,7 @@
|
||||
android:name="android.support.PARENT_ACTIVITY"
|
||||
android:value="com.limelight.AppView" />
|
||||
</activity>
|
||||
|
||||
<service
|
||||
android:name=".discovery.DiscoveryService"
|
||||
android:label="mDNS PC Auto-Discovery Service" />
|
||||
|
||||
@@ -50,6 +50,7 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
|
||||
private String lastRawApplist;
|
||||
private int lastRunningAppId;
|
||||
private boolean suspendGridUpdates;
|
||||
private boolean inForeground;
|
||||
|
||||
private final static int START_OR_RESUME_ID = 1;
|
||||
private final static int QUIT_ID = 2;
|
||||
@@ -95,9 +96,25 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
|
||||
// Load the app grid with cached data (if possible)
|
||||
populateAppGridWithCache();
|
||||
|
||||
getFragmentManager().beginTransaction()
|
||||
.replace(R.id.appFragmentContainer, new AdapterFragment())
|
||||
.commitAllowingStateLoss();
|
||||
runOnUiThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (isFinishing() || isChangingConfigurations()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Despite my best efforts to catch all conditions that could
|
||||
// cause the activity to be destroyed when we try to commit
|
||||
// I haven't been able to, so we have this try-catch block.
|
||||
try {
|
||||
getFragmentManager().beginTransaction()
|
||||
.replace(R.id.appFragmentContainer, new AdapterFragment())
|
||||
.commitAllowingStateLoss();
|
||||
} catch (IllegalStateException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}.start();
|
||||
}
|
||||
@@ -108,7 +125,8 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
|
||||
};
|
||||
|
||||
private void startComputerUpdates() {
|
||||
if (managerBinder == null) {
|
||||
// Don't start polling if we're not bound or in the foreground
|
||||
if (managerBinder == null || !inForeground) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -252,6 +270,7 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
|
||||
protected void onResume() {
|
||||
super.onResume();
|
||||
|
||||
inForeground = true;
|
||||
startComputerUpdates();
|
||||
}
|
||||
|
||||
@@ -259,6 +278,7 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
|
||||
protected void onPause() {
|
||||
super.onPause();
|
||||
|
||||
inForeground = false;
|
||||
stopComputerUpdates();
|
||||
}
|
||||
|
||||
|
||||
@@ -4,10 +4,12 @@ package com.limelight;
|
||||
import com.limelight.binding.PlatformBinding;
|
||||
import com.limelight.binding.input.ControllerHandler;
|
||||
import com.limelight.binding.input.KeyboardTranslator;
|
||||
import com.limelight.binding.input.NvMouseHelper;
|
||||
import com.limelight.binding.input.TouchContext;
|
||||
import com.limelight.binding.input.driver.UsbDriverService;
|
||||
import com.limelight.binding.input.evdev.EvdevHandler;
|
||||
import com.limelight.binding.input.evdev.EvdevListener;
|
||||
import com.limelight.binding.input.virtual_controller.VirtualController;
|
||||
import com.limelight.binding.video.EnhancedDecoderRenderer;
|
||||
import com.limelight.binding.video.MediaCodecDecoderRenderer;
|
||||
import com.limelight.binding.video.MediaCodecHelper;
|
||||
@@ -20,6 +22,7 @@ import com.limelight.nvstream.input.KeyboardPacket;
|
||||
import com.limelight.nvstream.input.MouseButtonPacket;
|
||||
import com.limelight.preferences.PreferenceConfiguration;
|
||||
import com.limelight.ui.GameGestures;
|
||||
import com.limelight.ui.StreamView;
|
||||
import com.limelight.utils.Dialog;
|
||||
import com.limelight.utils.SpinnerDialog;
|
||||
|
||||
@@ -53,6 +56,7 @@ import android.view.View.OnTouchListener;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.Window;
|
||||
import android.view.WindowManager;
|
||||
import android.widget.FrameLayout;
|
||||
import android.view.inputmethod.InputMethodManager;
|
||||
import android.widget.Toast;
|
||||
|
||||
@@ -71,28 +75,28 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
||||
private final TouchContext[] touchContextMap = new TouchContext[2];
|
||||
private long threeFingerDownTime = 0;
|
||||
|
||||
private static final double REFERENCE_HORIZ_RES = 1280;
|
||||
private static final double REFERENCE_VERT_RES = 720;
|
||||
private static final int REFERENCE_HORIZ_RES = 1280;
|
||||
private static final int REFERENCE_VERT_RES = 720;
|
||||
|
||||
private static final int THREE_FINGER_TAP_THRESHOLD = 300;
|
||||
|
||||
private ControllerHandler controllerHandler;
|
||||
private VirtualController virtualController;
|
||||
private KeyboardTranslator keybTranslator;
|
||||
|
||||
private PreferenceConfiguration prefConfig;
|
||||
private final Point screenSize = new Point(0, 0);
|
||||
|
||||
private NvConnection conn;
|
||||
private SpinnerDialog spinner;
|
||||
private boolean displayedFailureDialog = false;
|
||||
private boolean connecting = false;
|
||||
private boolean connected = false;
|
||||
private boolean deferredSurfaceResize = false;
|
||||
|
||||
private EvdevHandler evdevHandler;
|
||||
private int modifierFlags = 0;
|
||||
private boolean grabbedInput = true;
|
||||
private boolean grabComboDown = false;
|
||||
private StreamView streamView;
|
||||
|
||||
private EnhancedDecoderRenderer decoderRenderer;
|
||||
|
||||
@@ -171,13 +175,10 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
||||
drFlags |= VideoDecoderRenderer.FLAG_FILL_SCREEN;
|
||||
}
|
||||
|
||||
Display display = getWindowManager().getDefaultDisplay();
|
||||
display.getSize(screenSize);
|
||||
|
||||
// Listen for events on the game surface
|
||||
SurfaceView sv = (SurfaceView) findViewById(R.id.surfaceView);
|
||||
sv.setOnGenericMotionListener(this);
|
||||
sv.setOnTouchListener(this);
|
||||
streamView = (StreamView) findViewById(R.id.surfaceView);
|
||||
streamView.setOnGenericMotionListener(this);
|
||||
streamView.setOnTouchListener(this);
|
||||
|
||||
// Warn the user if they're on a metered connection
|
||||
checkDataConnection();
|
||||
@@ -254,6 +255,10 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
||||
// >= 4K screens have exactly 4K screens, so we'll be able to hit this good path
|
||||
// on these devices. On Marshmallow, we can start changing to 4K manually but no
|
||||
// 4K devices run 6.0 at the moment.
|
||||
Display display = getWindowManager().getDefaultDisplay();
|
||||
Point screenSize = new Point(0, 0);
|
||||
display.getSize(screenSize);
|
||||
|
||||
double screenAspectRatio = ((double)screenSize.y) / screenSize.x;
|
||||
double streamAspectRatio = ((double)prefConfig.height) / prefConfig.width;
|
||||
if (Math.abs(screenAspectRatio - streamAspectRatio) < 0.001) {
|
||||
@@ -262,20 +267,21 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
||||
}
|
||||
}
|
||||
|
||||
SurfaceHolder sh = sv.getHolder();
|
||||
SurfaceHolder sh = streamView.getHolder();
|
||||
if (prefConfig.stretchVideo || aspectRatioMatch) {
|
||||
// Set the surface to the size of the video
|
||||
sh.setFixedSize(prefConfig.width, prefConfig.height);
|
||||
}
|
||||
else {
|
||||
deferredSurfaceResize = true;
|
||||
// Set the surface to scale based on the aspect ratio of the stream
|
||||
streamView.setDesiredAspectRatio((double)prefConfig.width / (double)prefConfig.height);
|
||||
}
|
||||
|
||||
// Initialize touch contexts
|
||||
for (int i = 0; i < touchContextMap.length; i++) {
|
||||
touchContextMap[i] = new TouchContext(conn, i,
|
||||
(REFERENCE_HORIZ_RES / (double)screenSize.x),
|
||||
(REFERENCE_VERT_RES / (double)screenSize.y));
|
||||
REFERENCE_HORIZ_RES, REFERENCE_VERT_RES,
|
||||
streamView);
|
||||
}
|
||||
|
||||
if (LimelightBuildProps.ROOT_BUILD) {
|
||||
@@ -284,6 +290,14 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
||||
evdevHandler.start();
|
||||
}
|
||||
|
||||
if (prefConfig.onscreenController) {
|
||||
// create virtual onscreen controller
|
||||
virtualController = new VirtualController(conn,
|
||||
(FrameLayout)findViewById(R.id.surfaceView).getParent(),
|
||||
this);
|
||||
virtualController.refreshLayout();
|
||||
}
|
||||
|
||||
if (prefConfig.usbDriver) {
|
||||
// Start the USB driver
|
||||
bindService(new Intent(this, UsbDriverService.class),
|
||||
@@ -294,21 +308,6 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
||||
sh.addCallback(this);
|
||||
}
|
||||
|
||||
private void resizeSurfaceWithAspectRatio(SurfaceView sv, double vidWidth, double vidHeight)
|
||||
{
|
||||
// Get the visible width of the activity
|
||||
double visibleWidth = getWindow().getDecorView().getWidth();
|
||||
|
||||
ViewGroup.LayoutParams lp = sv.getLayoutParams();
|
||||
|
||||
// Calculate the new size of the SurfaceView
|
||||
lp.width = (int) visibleWidth;
|
||||
lp.height = (int) ((vidHeight / vidWidth) * visibleWidth);
|
||||
|
||||
// Apply the size change
|
||||
sv.setLayoutParams(lp);
|
||||
}
|
||||
|
||||
private void checkDataConnection()
|
||||
{
|
||||
ConnectivityManager mgr = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE);
|
||||
@@ -354,8 +353,10 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
||||
SpinnerDialog.closeDialogs(this);
|
||||
Dialog.closeDialogs();
|
||||
|
||||
InputManager inputManager = (InputManager) getSystemService(Context.INPUT_SERVICE);
|
||||
inputManager.unregisterInputDeviceListener(controllerHandler);
|
||||
if (controllerHandler != null) {
|
||||
InputManager inputManager = (InputManager) getSystemService(Context.INPUT_SERVICE);
|
||||
inputManager.unregisterInputDeviceListener(controllerHandler);
|
||||
}
|
||||
|
||||
wifiLock.release();
|
||||
|
||||
@@ -364,36 +365,38 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
||||
unbindService(usbDriverServiceConnection);
|
||||
}
|
||||
|
||||
VideoDecoderRenderer.VideoFormat videoFormat = conn.getActiveVideoFormat();
|
||||
if (conn != null) {
|
||||
VideoDecoderRenderer.VideoFormat videoFormat = conn.getActiveVideoFormat();
|
||||
|
||||
displayedFailureDialog = true;
|
||||
stopConnection();
|
||||
displayedFailureDialog = true;
|
||||
stopConnection();
|
||||
|
||||
int averageEndToEndLat = decoderRenderer.getAverageEndToEndLatency();
|
||||
int averageDecoderLat = decoderRenderer.getAverageDecoderLatency();
|
||||
String message = null;
|
||||
if (averageEndToEndLat > 0) {
|
||||
message = getResources().getString(R.string.conn_client_latency)+" "+averageEndToEndLat+" ms";
|
||||
if (averageDecoderLat > 0) {
|
||||
message += " ("+getResources().getString(R.string.conn_client_latency_hw)+" "+averageDecoderLat+" ms)";
|
||||
int averageEndToEndLat = decoderRenderer.getAverageEndToEndLatency();
|
||||
int averageDecoderLat = decoderRenderer.getAverageDecoderLatency();
|
||||
String message = null;
|
||||
if (averageEndToEndLat > 0) {
|
||||
message = getResources().getString(R.string.conn_client_latency)+" "+averageEndToEndLat+" ms";
|
||||
if (averageDecoderLat > 0) {
|
||||
message += " ("+getResources().getString(R.string.conn_client_latency_hw)+" "+averageDecoderLat+" ms)";
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (averageDecoderLat > 0) {
|
||||
message = getResources().getString(R.string.conn_hardware_latency)+" "+averageDecoderLat+" ms";
|
||||
}
|
||||
|
||||
// Add the video codec to the post-stream toast
|
||||
if (message != null && videoFormat != VideoDecoderRenderer.VideoFormat.Unknown) {
|
||||
if (videoFormat == VideoDecoderRenderer.VideoFormat.H265) {
|
||||
message += " [H.265]";
|
||||
else if (averageDecoderLat > 0) {
|
||||
message = getResources().getString(R.string.conn_hardware_latency)+" "+averageDecoderLat+" ms";
|
||||
}
|
||||
else {
|
||||
message += " [H.264]";
|
||||
}
|
||||
}
|
||||
|
||||
if (message != null) {
|
||||
Toast.makeText(this, message, Toast.LENGTH_LONG).show();
|
||||
// Add the video codec to the post-stream toast
|
||||
if (message != null && videoFormat != VideoDecoderRenderer.VideoFormat.Unknown) {
|
||||
if (videoFormat == VideoDecoderRenderer.VideoFormat.H265) {
|
||||
message += " [H.265]";
|
||||
}
|
||||
else {
|
||||
message += " [H.264]";
|
||||
}
|
||||
}
|
||||
|
||||
if (message != null) {
|
||||
Toast.makeText(this, message, Toast.LENGTH_LONG).show();
|
||||
}
|
||||
}
|
||||
|
||||
finish();
|
||||
@@ -402,11 +405,15 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
||||
private final Runnable toggleGrab = new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (evdevHandler != null) {
|
||||
if (grabbedInput) {
|
||||
if (grabbedInput) {
|
||||
NvMouseHelper.setCursorVisibility(Game.this, true);
|
||||
if (evdevHandler != null) {
|
||||
evdevHandler.ungrabAll();
|
||||
}
|
||||
else {
|
||||
}
|
||||
else {
|
||||
NvMouseHelper.setCursorVisibility(Game.this, false);
|
||||
if (evdevHandler != null) {
|
||||
evdevHandler.regrabAll();
|
||||
}
|
||||
}
|
||||
@@ -596,7 +603,9 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
||||
else if ((event.getSource() & InputDevice.SOURCE_CLASS_POINTER) != 0)
|
||||
{
|
||||
// This case is for mice
|
||||
if (event.getSource() == InputDevice.SOURCE_MOUSE)
|
||||
if (event.getSource() == InputDevice.SOURCE_MOUSE ||
|
||||
(event.getPointerCount() >= 1 &&
|
||||
event.getToolType(0) == MotionEvent.TOOL_TYPE_MOUSE))
|
||||
{
|
||||
int changedButtons = event.getButtonState() ^ lastButtonState;
|
||||
|
||||
@@ -633,19 +642,38 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
||||
}
|
||||
}
|
||||
|
||||
// First process the history
|
||||
for (int i = 0; i < event.getHistorySize(); i++) {
|
||||
updateMousePosition((int)event.getHistoricalX(i), (int)event.getHistoricalY(i));
|
||||
}
|
||||
// Get relative axis values if we can
|
||||
if (NvMouseHelper.eventHasRelativeMouseAxes(event)) {
|
||||
// Send the deltas straight from the motion event
|
||||
conn.sendMouseMove((short)NvMouseHelper.getRelativeAxisX(event),
|
||||
(short)NvMouseHelper.getRelativeAxisY(event));
|
||||
|
||||
// Now process the current values
|
||||
updateMousePosition((int)event.getX(), (int)event.getY());
|
||||
// We have to also update the position Android thinks the cursor is at
|
||||
// in order to avoid jumping when we stop moving or click.
|
||||
lastMouseX = (int)event.getX();
|
||||
lastMouseY = (int)event.getY();
|
||||
}
|
||||
else {
|
||||
// First process the history
|
||||
for (int i = 0; i < event.getHistorySize(); i++) {
|
||||
updateMousePosition((int)event.getHistoricalX(i), (int)event.getHistoricalY(i));
|
||||
}
|
||||
|
||||
// Now process the current values
|
||||
updateMousePosition((int)event.getX(), (int)event.getY());
|
||||
}
|
||||
|
||||
lastButtonState = event.getButtonState();
|
||||
}
|
||||
// This case is for touch-based input devices
|
||||
else
|
||||
{
|
||||
if (virtualController != null &&
|
||||
virtualController.getControllerMode() == VirtualController.ControllerMode.Configuration) {
|
||||
// Ignore presses when the virtual controller is in configuration mode
|
||||
return true;
|
||||
}
|
||||
|
||||
int actionIndex = event.getActionIndex();
|
||||
|
||||
int eventX = (int)event.getX(actionIndex);
|
||||
@@ -756,8 +784,8 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
||||
|
||||
// Scale the deltas if the device resolution is different
|
||||
// than the stream resolution
|
||||
deltaX = (int)Math.round((double)deltaX * (REFERENCE_HORIZ_RES / (double)screenSize.x));
|
||||
deltaY = (int)Math.round((double)deltaY * (REFERENCE_VERT_RES / (double)screenSize.y));
|
||||
deltaX = (int)Math.round((double)deltaX * (REFERENCE_HORIZ_RES / (double)streamView.getWidth()));
|
||||
deltaY = (int)Math.round((double)deltaY * (REFERENCE_VERT_RES / (double)streamView.getHeight()));
|
||||
|
||||
conn.sendMouseMove((short)deltaX, (short)deltaY);
|
||||
}
|
||||
@@ -800,6 +828,9 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
||||
evdevHandler.stop();
|
||||
evdevHandler = null;
|
||||
}
|
||||
|
||||
// Enable cursor visibility again
|
||||
NvMouseHelper.setCursorVisibility(this, true);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -839,6 +870,16 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
||||
connecting = false;
|
||||
connected = true;
|
||||
|
||||
runOnUiThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
// Hide the mouse cursor now. Doing it before
|
||||
// dismissing the spinner seems to be undone
|
||||
// when the spinner gets displayed.
|
||||
NvMouseHelper.setCursorVisibility(Game.this, false);
|
||||
}
|
||||
});
|
||||
|
||||
hideSystemUi(1000);
|
||||
}
|
||||
|
||||
@@ -873,13 +914,6 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
||||
if (!connected && !connecting) {
|
||||
connecting = true;
|
||||
|
||||
// Resize the surface to match the aspect ratio of the video
|
||||
// This must be done after the surface is created.
|
||||
if (deferredSurfaceResize) {
|
||||
resizeSurfaceWithAspectRatio((SurfaceView) findViewById(R.id.surfaceView),
|
||||
prefConfig.width, prefConfig.height);
|
||||
}
|
||||
|
||||
conn.start(PlatformBinding.getDeviceName(), holder, drFlags,
|
||||
PlatformBinding.getAudioRenderer(), decoderRenderer);
|
||||
}
|
||||
|
||||
@@ -53,7 +53,7 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
|
||||
private RelativeLayout noPcFoundLayout;
|
||||
private PcGridAdapter pcGridAdapter;
|
||||
private ComputerManagerService.ComputerManagerBinder managerBinder;
|
||||
private boolean freezeUpdates, runningPolling, hasResumed;
|
||||
private boolean freezeUpdates, runningPolling, inForeground;
|
||||
private final ServiceConnection serviceConnection = new ServiceConnection() {
|
||||
public void onServiceConnected(ComponentName className, IBinder binder) {
|
||||
final ComputerManagerService.ComputerManagerBinder localBinder =
|
||||
@@ -161,11 +161,9 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
|
||||
}
|
||||
|
||||
private void startComputerUpdates() {
|
||||
if (managerBinder != null) {
|
||||
if (runningPolling) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Only allow polling to start if we're bound to CMS, polling is not already running,
|
||||
// and our activity is in the foreground.
|
||||
if (managerBinder != null && !runningPolling && inForeground) {
|
||||
freezeUpdates = false;
|
||||
managerBinder.startPolling(new ComputerManagerListener() {
|
||||
@Override
|
||||
@@ -215,7 +213,7 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
|
||||
protected void onResume() {
|
||||
super.onResume();
|
||||
|
||||
hasResumed = true;
|
||||
inForeground = true;
|
||||
startComputerUpdates();
|
||||
}
|
||||
|
||||
@@ -223,7 +221,7 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
|
||||
protected void onPause() {
|
||||
super.onPause();
|
||||
|
||||
hasResumed = false;
|
||||
inForeground = false;
|
||||
stopComputerUpdates(false);
|
||||
}
|
||||
|
||||
@@ -271,10 +269,9 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
|
||||
@Override
|
||||
public void onContextMenuClosed(Menu menu) {
|
||||
// For some reason, this gets called again _after_ onPause() is called on this activity.
|
||||
// We don't want to start computer updates again, so we need to keep track of whether we're paused.
|
||||
if (hasResumed) {
|
||||
startComputerUpdates();
|
||||
}
|
||||
// startComputerUpdates() manages this and won't actual start polling until the activity
|
||||
// returns to the foreground.
|
||||
startComputerUpdates();
|
||||
}
|
||||
|
||||
private void doPair(final ComputerDetails computer) {
|
||||
@@ -330,7 +327,7 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
|
||||
Dialog.displayDialog(PcView.this, getResources().getString(R.string.pair_pairing_title),
|
||||
getResources().getString(R.string.pair_pairing_msg)+" "+pinStr, false);
|
||||
|
||||
PairingManager.PairState pairState = httpConn.pair(pinStr);
|
||||
PairingManager.PairState pairState = httpConn.pair(httpConn.getServerInfo(), pinStr);
|
||||
if (pairState == PairingManager.PairState.PIN_WRONG) {
|
||||
message = getResources().getString(R.string.pair_incorrect_pin);
|
||||
}
|
||||
@@ -368,14 +365,15 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
|
||||
}
|
||||
|
||||
if (toastSuccess) {
|
||||
// Open the app list after a successful pairing attemp
|
||||
// Open the app list after a successful pairing attempt
|
||||
doAppList(computer);
|
||||
}
|
||||
else {
|
||||
// Start polling again if we're still in the foreground
|
||||
startComputerUpdates();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Start polling again
|
||||
startComputerUpdates();
|
||||
}
|
||||
}).start();
|
||||
}
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
package com.limelight;
|
||||
|
||||
/* This is a dummy class to allow for a separate icon
|
||||
* and launcher for TV.
|
||||
*/
|
||||
public class PcViewTv extends PcView {}
|
||||
@@ -82,6 +82,7 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
|
||||
defaultContext.leftTriggerAxis = MotionEvent.AXIS_BRAKE;
|
||||
defaultContext.rightTriggerAxis = MotionEvent.AXIS_GAS;
|
||||
defaultContext.controllerNumber = (short) 0;
|
||||
defaultContext.assignedControllerNumber = true;
|
||||
}
|
||||
|
||||
private static InputDevice.MotionRange getMotionRangeForJoystickAxis(InputDevice dev, int axis) {
|
||||
@@ -146,8 +147,9 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
|
||||
InputDeviceContext devContext = (InputDeviceContext) context;
|
||||
|
||||
LimeLog.info(devContext.name+" ("+context.id+") needs a controller number assigned");
|
||||
if (devContext.name != null && devContext.name.contains("gpio-keys")) {
|
||||
// This is the back button on Shield portable consoles
|
||||
if (devContext.name != null &&
|
||||
(devContext.name.contains("gpio-keys") || // This is the back button on Shield portable consoles
|
||||
devContext.name.contains("joy_key"))) { // These are the gamepad buttons on the Archos Gamepad 2
|
||||
LimeLog.info("Built-in buttons hardcoded as controller 0");
|
||||
context.controllerNumber = 0;
|
||||
}
|
||||
@@ -212,6 +214,7 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
|
||||
String devName = dev.getName();
|
||||
|
||||
LimeLog.info("Creating controller context for device: "+devName);
|
||||
LimeLog.info(dev.toString());
|
||||
|
||||
context.name = devName;
|
||||
context.id = dev.getId();
|
||||
@@ -229,6 +232,7 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
|
||||
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)
|
||||
@@ -241,6 +245,12 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
|
||||
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);
|
||||
@@ -324,8 +334,11 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
|
||||
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.KITKAT) {
|
||||
boolean[] hasSelectKey = dev.hasKeys(KeyEvent.KEYCODE_BUTTON_SELECT, KeyEvent.KEYCODE_BACK, 0);
|
||||
if (hasSelectKey[0] && hasSelectKey[1]) {
|
||||
LimeLog.info("Ignoring back button because select is present");
|
||||
context.ignoreBack = true;
|
||||
// Xiaomi gamepads claim to have both buttons then only send KEYCODE_BACK events
|
||||
if (dev.getVendorId() != 0x2717) {
|
||||
LimeLog.info("Ignoring back button because select is present");
|
||||
context.ignoreBack = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -362,7 +375,7 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
|
||||
}
|
||||
// Samsung's face buttons appear as a non-virtual button so we'll explicitly ignore
|
||||
// back presses on this device
|
||||
else if (devName.equals("sec_touchscreen")) {
|
||||
else if (devName.equals("sec_touchscreen") || devName.equals("sec_touchkey")) {
|
||||
context.ignoreBack = true;
|
||||
}
|
||||
// The Serval has a couple of unknown buttons that are start and select. It also has
|
||||
@@ -398,12 +411,82 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
|
||||
return context;
|
||||
}
|
||||
|
||||
private void sendControllerInputPacket(GenericControllerContext context) {
|
||||
assignControllerNumberIfNeeded(context);
|
||||
conn.sendControllerInput(context.controllerNumber, context.inputMap,
|
||||
context.leftTrigger, context.rightTrigger,
|
||||
context.leftStickX, context.leftStickY,
|
||||
context.rightStickX, context.rightStickY);
|
||||
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 void sendControllerInputPacket(GenericControllerContext originalContext) {
|
||||
assignControllerNumberIfNeeded(originalContext);
|
||||
|
||||
// Take the context's controller number and fuse all inputs with the same number
|
||||
short controllerNumber = originalContext.controllerNumber;
|
||||
short 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) {
|
||||
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) {
|
||||
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);
|
||||
}
|
||||
|
||||
conn.sendControllerInput(controllerNumber, inputMap,
|
||||
leftTrigger, rightTrigger,
|
||||
leftStickX, leftStickY,
|
||||
rightStickX, rightStickY);
|
||||
}
|
||||
|
||||
// Return a valid keycode, 0 to consume, or -1 to not consume the event
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
package com.limelight.binding.input;
|
||||
|
||||
|
||||
import android.content.Context;
|
||||
import android.hardware.input.InputManager;
|
||||
import android.view.MotionEvent;
|
||||
|
||||
import com.limelight.LimeLog;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
// NVIDIA extended the Android input APIs with support for using an attached mouse in relative
|
||||
// mode without having to grab the input device (which requires root). The data comes in the form
|
||||
// of new AXIS_RELATIVE_X and AXIS_RELATIVE_Y constants in the mouse's MotionEvent objects and
|
||||
// a new function, InputManager.setCursorVisibility(), that allows the cursor to be hidden.
|
||||
//
|
||||
// http://docs.nvidia.com/gameworks/index.html#technologies/mobile/game_controller_handling_mouse.htm
|
||||
|
||||
public class NvMouseHelper {
|
||||
private static boolean nvExtensionSupported;
|
||||
private static Method methodSetCursorVisibility;
|
||||
private static int AXIS_RELATIVE_X;
|
||||
private static int AXIS_RELATIVE_Y;
|
||||
|
||||
static {
|
||||
try {
|
||||
methodSetCursorVisibility = InputManager.class.getMethod("setCursorVisibility", boolean.class);
|
||||
|
||||
Field fieldRelX = MotionEvent.class.getField("AXIS_RELATIVE_X");
|
||||
Field fieldRelY = MotionEvent.class.getField("AXIS_RELATIVE_Y");
|
||||
|
||||
AXIS_RELATIVE_X = (Integer) fieldRelX.get(null);
|
||||
AXIS_RELATIVE_Y = (Integer) fieldRelY.get(null);
|
||||
|
||||
nvExtensionSupported = true;
|
||||
} catch (Exception e) {
|
||||
LimeLog.info("NvMouseHelper not supported");
|
||||
nvExtensionSupported = false;
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean setCursorVisibility(Context context, boolean visible) {
|
||||
if (!nvExtensionSupported) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
methodSetCursorVisibility.invoke(context.getSystemService(Context.INPUT_SERVICE), visible);
|
||||
return true;
|
||||
} catch (InvocationTargetException e) {
|
||||
e.printStackTrace();
|
||||
} catch (IllegalAccessException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public static boolean eventHasRelativeMouseAxes(MotionEvent event) {
|
||||
if (!nvExtensionSupported) {
|
||||
return false;
|
||||
}
|
||||
return event.getAxisValue(AXIS_RELATIVE_X) != 0 ||
|
||||
event.getAxisValue(AXIS_RELATIVE_Y) != 0;
|
||||
}
|
||||
|
||||
public static float getRelativeAxisX(MotionEvent event) {
|
||||
return event.getAxisValue(AXIS_RELATIVE_X);
|
||||
}
|
||||
|
||||
public static float getRelativeAxisY(MotionEvent event) {
|
||||
return event.getAxisValue(AXIS_RELATIVE_Y);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
package com.limelight.binding.input;
|
||||
|
||||
import android.view.View;
|
||||
|
||||
import com.limelight.nvstream.NvConnection;
|
||||
import com.limelight.nvstream.input.MouseButtonPacket;
|
||||
|
||||
@@ -17,23 +19,27 @@ public class TouchContext {
|
||||
private boolean confirmedDrag;
|
||||
private Timer dragTimer;
|
||||
private double distanceMoved;
|
||||
private double xFactor, yFactor;
|
||||
|
||||
private final NvConnection conn;
|
||||
private final int actionIndex;
|
||||
private final double xFactor;
|
||||
private final double yFactor;
|
||||
private final int referenceWidth;
|
||||
private final int referenceHeight;
|
||||
private final View targetView;
|
||||
|
||||
private static final int TAP_MOVEMENT_THRESHOLD = 20;
|
||||
private static final int TAP_DISTANCE_THRESHOLD = 25;
|
||||
private static final int TAP_TIME_THRESHOLD = 250;
|
||||
private static final int DRAG_TIME_THRESHOLD = 650;
|
||||
|
||||
public TouchContext(NvConnection conn, int actionIndex, double xFactor, double yFactor)
|
||||
public TouchContext(NvConnection conn, int actionIndex,
|
||||
int referenceWidth, int referenceHeight, View view)
|
||||
{
|
||||
this.conn = conn;
|
||||
this.actionIndex = actionIndex;
|
||||
this.xFactor = xFactor;
|
||||
this.yFactor = yFactor;
|
||||
this.referenceWidth = referenceWidth;
|
||||
this.referenceHeight = referenceHeight;
|
||||
this.targetView = view;
|
||||
}
|
||||
|
||||
public int getActionIndex()
|
||||
@@ -68,6 +74,10 @@ public class TouchContext {
|
||||
|
||||
public boolean touchDownEvent(int eventX, int eventY)
|
||||
{
|
||||
// Get the view dimensions to scale inputs on this touch
|
||||
xFactor = referenceWidth / (double)targetView.getWidth();
|
||||
yFactor = referenceHeight / (double)targetView.getHeight();
|
||||
|
||||
originalTouchX = lastTouchX = eventX;
|
||||
originalTouchY = lastTouchY = eventY;
|
||||
originalTouchTime = System.currentTimeMillis();
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
package com.limelight.binding.input.driver;
|
||||
|
||||
import android.hardware.usb.UsbEndpoint;
|
||||
|
||||
import com.limelight.LimeLog;
|
||||
import com.limelight.binding.video.MediaCodecHelper;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
|
||||
public abstract class AbstractController {
|
||||
|
||||
private final int deviceId;
|
||||
|
||||
private UsbDriverListener listener;
|
||||
|
||||
protected short buttonFlags;
|
||||
protected float leftTrigger, rightTrigger;
|
||||
protected float rightStickX, rightStickY;
|
||||
protected float leftStickX, leftStickY;
|
||||
|
||||
public int getControllerId() {
|
||||
return deviceId;
|
||||
}
|
||||
|
||||
protected void setButtonFlag(int buttonFlag, int data) {
|
||||
if (data != 0) {
|
||||
buttonFlags |= buttonFlag;
|
||||
}
|
||||
else {
|
||||
buttonFlags &= ~buttonFlag;
|
||||
}
|
||||
}
|
||||
|
||||
protected void reportInput() {
|
||||
listener.reportControllerState(deviceId, buttonFlags, leftStickX, leftStickY,
|
||||
rightStickX, rightStickY, leftTrigger, rightTrigger);
|
||||
}
|
||||
|
||||
public abstract boolean start();
|
||||
public abstract void stop();
|
||||
|
||||
public AbstractController(int deviceId, UsbDriverListener listener) {
|
||||
this.deviceId = deviceId;
|
||||
this.listener = listener;
|
||||
}
|
||||
|
||||
protected void notifyDeviceRemoved() {
|
||||
listener.deviceRemoved(deviceId);
|
||||
}
|
||||
|
||||
protected void notifyDeviceAdded() {
|
||||
listener.deviceAdded(deviceId);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
package com.limelight.binding.input.driver;
|
||||
|
||||
import android.hardware.usb.UsbConstants;
|
||||
import android.hardware.usb.UsbDevice;
|
||||
import android.hardware.usb.UsbDeviceConnection;
|
||||
import android.hardware.usb.UsbEndpoint;
|
||||
import android.hardware.usb.UsbInterface;
|
||||
|
||||
import com.limelight.LimeLog;
|
||||
import com.limelight.binding.video.MediaCodecHelper;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
|
||||
public abstract class AbstractXboxController extends AbstractController {
|
||||
protected final UsbDevice device;
|
||||
protected final UsbDeviceConnection connection;
|
||||
|
||||
private Thread inputThread;
|
||||
private boolean stopped;
|
||||
|
||||
protected UsbEndpoint inEndpt, outEndpt;
|
||||
|
||||
public AbstractXboxController(UsbDevice device, UsbDeviceConnection connection, int deviceId, UsbDriverListener listener) {
|
||||
super(deviceId, listener);
|
||||
this.device = device;
|
||||
this.connection = connection;
|
||||
}
|
||||
|
||||
private Thread createInputThread() {
|
||||
return new Thread() {
|
||||
public void run() {
|
||||
while (!isInterrupted() && !stopped) {
|
||||
byte[] buffer = new byte[64];
|
||||
|
||||
int res;
|
||||
|
||||
//
|
||||
// There's no way that I can tell to determine if a device has failed
|
||||
// or if the timeout has simply expired. We'll check how long the transfer
|
||||
// took to fail and assume the device failed if it happened before the timeout
|
||||
// expired.
|
||||
//
|
||||
|
||||
do {
|
||||
// Read the next input state packet
|
||||
long lastMillis = MediaCodecHelper.getMonotonicMillis();
|
||||
res = connection.bulkTransfer(inEndpt, buffer, buffer.length, 3000);
|
||||
|
||||
// If we get a zero length response, treat it as an error
|
||||
if (res == 0) {
|
||||
res = -1;
|
||||
}
|
||||
|
||||
if (res == -1 && MediaCodecHelper.getMonotonicMillis() - lastMillis < 1000) {
|
||||
LimeLog.warning("Detected device I/O error");
|
||||
AbstractXboxController.this.stop();
|
||||
break;
|
||||
}
|
||||
} while (res == -1 && !isInterrupted() && !stopped);
|
||||
|
||||
if (res == -1 || stopped) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (handleRead(ByteBuffer.wrap(buffer, 0, res).order(ByteOrder.LITTLE_ENDIAN))) {
|
||||
// Report input if handleRead() returns true
|
||||
reportInput();
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public boolean start() {
|
||||
// Force claim all interfaces
|
||||
for (int i = 0; i < device.getInterfaceCount(); i++) {
|
||||
UsbInterface iface = device.getInterface(i);
|
||||
|
||||
if (!connection.claimInterface(iface, true)) {
|
||||
LimeLog.warning("Failed to claim interfaces");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Find the endpoints
|
||||
UsbInterface iface = device.getInterface(0);
|
||||
for (int i = 0; i < iface.getEndpointCount(); i++) {
|
||||
UsbEndpoint endpt = iface.getEndpoint(i);
|
||||
if (endpt.getDirection() == UsbConstants.USB_DIR_IN) {
|
||||
if (inEndpt != null) {
|
||||
LimeLog.warning("Found duplicate IN endpoint");
|
||||
return false;
|
||||
}
|
||||
inEndpt = endpt;
|
||||
}
|
||||
else if (endpt.getDirection() == UsbConstants.USB_DIR_OUT) {
|
||||
if (outEndpt != null) {
|
||||
LimeLog.warning("Found duplicate OUT endpoint");
|
||||
return false;
|
||||
}
|
||||
outEndpt = endpt;
|
||||
}
|
||||
}
|
||||
|
||||
// Make sure the required endpoints were present
|
||||
if (inEndpt == null || outEndpt == null) {
|
||||
LimeLog.warning("Missing required endpoint");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Run the init function
|
||||
if (!doInit()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Start listening for controller input
|
||||
inputThread = createInputThread();
|
||||
inputThread.start();
|
||||
|
||||
// Now report we're added
|
||||
notifyDeviceAdded();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public void stop() {
|
||||
if (stopped) {
|
||||
return;
|
||||
}
|
||||
|
||||
stopped = true;
|
||||
|
||||
// Stop the input thread
|
||||
if (inputThread != null) {
|
||||
inputThread.interrupt();
|
||||
inputThread = null;
|
||||
}
|
||||
|
||||
// Report the device removed
|
||||
notifyDeviceRemoved();
|
||||
|
||||
// Close the USB connection
|
||||
connection.close();
|
||||
}
|
||||
|
||||
protected abstract boolean handleRead(ByteBuffer buffer);
|
||||
protected abstract boolean doInit();
|
||||
}
|
||||
@@ -10,7 +10,11 @@ import android.hardware.usb.UsbDevice;
|
||||
import android.hardware.usb.UsbDeviceConnection;
|
||||
import android.hardware.usb.UsbManager;
|
||||
import android.os.Binder;
|
||||
import android.os.Build;
|
||||
import android.os.IBinder;
|
||||
import android.view.InputDevice;
|
||||
|
||||
import com.limelight.LimeLog;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
||||
@@ -24,10 +28,10 @@ public class UsbDriverService extends Service implements UsbDriverListener {
|
||||
private final UsbEventReceiver receiver = new UsbEventReceiver();
|
||||
private final UsbDriverBinder binder = new UsbDriverBinder();
|
||||
|
||||
private final ArrayList<XboxOneController> controllers = new ArrayList<>();
|
||||
private final ArrayList<AbstractController> controllers = new ArrayList<>();
|
||||
|
||||
private UsbDriverListener listener;
|
||||
private static int nextDeviceId;
|
||||
private int nextDeviceId;
|
||||
|
||||
@Override
|
||||
public void reportControllerState(int controllerId, short buttonFlags, float leftStickX, float leftStickY, float rightStickX, float rightStickY, float leftTrigger, float rightTrigger) {
|
||||
@@ -40,7 +44,7 @@ public class UsbDriverService extends Service implements UsbDriverListener {
|
||||
@Override
|
||||
public void deviceRemoved(int controllerId) {
|
||||
// Remove the the controller from our list (if not removed already)
|
||||
for (XboxOneController controller : controllers) {
|
||||
for (AbstractController controller : controllers) {
|
||||
if (controller.getControllerId() == controllerId) {
|
||||
controllers.remove(controller);
|
||||
break;
|
||||
@@ -91,7 +95,7 @@ public class UsbDriverService extends Service implements UsbDriverListener {
|
||||
|
||||
// Report all controllerMap that already exist
|
||||
if (listener != null) {
|
||||
for (XboxOneController controller : controllers) {
|
||||
for (AbstractController controller : controllers) {
|
||||
listener.deviceAdded(controller.getControllerId());
|
||||
}
|
||||
}
|
||||
@@ -100,7 +104,7 @@ public class UsbDriverService extends Service implements UsbDriverListener {
|
||||
|
||||
private void handleUsbDeviceState(UsbDevice device) {
|
||||
// Are we able to operate it?
|
||||
if (XboxOneController.canClaimDevice(device)) {
|
||||
if (shouldClaimDevice(device)) {
|
||||
// Do we have permission yet?
|
||||
if (!usbManager.hasPermission(device)) {
|
||||
// Let's ask for permission
|
||||
@@ -110,9 +114,26 @@ public class UsbDriverService extends Service implements UsbDriverListener {
|
||||
|
||||
// Open the device
|
||||
UsbDeviceConnection connection = usbManager.openDevice(device);
|
||||
if (connection == null) {
|
||||
LimeLog.warning("Unable to open USB device: "+device.getDeviceName());
|
||||
return;
|
||||
}
|
||||
|
||||
// Try to initialize it
|
||||
XboxOneController controller = new XboxOneController(device, connection, nextDeviceId++, this);
|
||||
|
||||
AbstractController controller;
|
||||
|
||||
if (XboxOneController.canClaimDevice(device)) {
|
||||
controller = new XboxOneController(device, connection, nextDeviceId++, this);
|
||||
}
|
||||
else if (Xbox360Controller.canClaimDevice(device)) {
|
||||
controller = new Xbox360Controller(device, connection, nextDeviceId++, this);
|
||||
}
|
||||
else {
|
||||
// Unreachable
|
||||
return;
|
||||
}
|
||||
|
||||
// Start the controller
|
||||
if (!controller.start()) {
|
||||
connection.close();
|
||||
return;
|
||||
@@ -123,6 +144,34 @@ public class UsbDriverService extends Service implements UsbDriverListener {
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isRecognizedInputDevice(UsbDevice device) {
|
||||
// On KitKat and later, we can determine if this VID and PID combo
|
||||
// matches an existing input device and defer to the built-in controller
|
||||
// support in that case. Prior to KitKat, we'll always return true to be safe.
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
|
||||
for (int id : InputDevice.getDeviceIds()) {
|
||||
InputDevice inputDev = InputDevice.getDevice(id);
|
||||
|
||||
if (inputDev.getVendorId() == device.getVendorId() &&
|
||||
inputDev.getProductId() == device.getProductId()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
else {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
private boolean shouldClaimDevice(UsbDevice device) {
|
||||
// We always bind to XB1 controllers but only bind to XB360 controllers
|
||||
// if we know the kernel isn't already driving this device.
|
||||
return XboxOneController.canClaimDevice(device) ||
|
||||
(!isRecognizedInputDevice(device) && Xbox360Controller.canClaimDevice(device));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
this.usbManager = (UsbManager) getSystemService(Context.USB_SERVICE);
|
||||
@@ -135,7 +184,7 @@ public class UsbDriverService extends Service implements UsbDriverListener {
|
||||
|
||||
// Enumerate existing devices
|
||||
for (UsbDevice dev : usbManager.getDeviceList().values()) {
|
||||
if (XboxOneController.canClaimDevice(dev)) {
|
||||
if (shouldClaimDevice(dev)) {
|
||||
// Start the process of claiming this device
|
||||
handleUsbDeviceState(dev);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,166 @@
|
||||
package com.limelight.binding.input.driver;
|
||||
|
||||
import android.hardware.usb.UsbConstants;
|
||||
import android.hardware.usb.UsbDevice;
|
||||
import android.hardware.usb.UsbDeviceConnection;
|
||||
|
||||
import com.limelight.LimeLog;
|
||||
import com.limelight.nvstream.input.ControllerPacket;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
public class Xbox360Controller extends AbstractXboxController {
|
||||
|
||||
// This list is taken from the Xpad driver in the Linux kernel.
|
||||
// I've excluded the devices that aren't "controllers" in the traditional sense, but
|
||||
// if people really want to use their dancepads or fight sticks with Moonlight, I can
|
||||
// put them in.
|
||||
private static final DeviceIdTuple[] supportedDeviceTuples = {
|
||||
new DeviceIdTuple(0x045e, 0x028e, "Microsoft X-Box 360 pad"),
|
||||
new DeviceIdTuple(0x044f, 0xb326, "Thrustmaster Gamepad GP XID"),
|
||||
new DeviceIdTuple(0x046d, 0xc21d, "Logitech Gamepad F310"),
|
||||
new DeviceIdTuple(0x046d, 0xc21e, "Logitech Gamepad F510"),
|
||||
new DeviceIdTuple(0x046d, 0xc21f, "Logitech Gamepad F710"),
|
||||
new DeviceIdTuple(0x046d, 0xc242, "Logitech Chillstream Controller"),
|
||||
new DeviceIdTuple(0x0738, 0x4716, "Mad Catz Wired Xbox 360 Controller"),
|
||||
new DeviceIdTuple(0x0738, 0x4726, "Mad Catz Xbox 360 Controller"),
|
||||
new DeviceIdTuple(0x0738, 0xb726, "Mad Catz Xbox controller - MW2"),
|
||||
new DeviceIdTuple(0x0738, 0xbeef, "Mad Catz JOYTECH NEO SE Advanced GamePad"),
|
||||
new DeviceIdTuple(0x0738, 0xcb02, "Saitek Cyborg Rumble Pad - PC/Xbox 360"),
|
||||
new DeviceIdTuple(0x0738, 0xcb03, "Saitek P3200 Rumble Pad - PC/Xbox 360"),
|
||||
new DeviceIdTuple(0x0e6f, 0x0113, "Afterglow AX.1 Gamepad for Xbox 360"),
|
||||
new DeviceIdTuple(0x0e6f, 0x0201, "Pelican PL-3601 'TSZ' Wired Xbox 360 Controller"),
|
||||
new DeviceIdTuple(0x0e6f, 0x0213, "Afterglow Gamepad for Xbox 360"),
|
||||
new DeviceIdTuple(0x0e6f, 0x021f, "Rock Candy Gamepad for Xbox 360"),
|
||||
new DeviceIdTuple(0x0e6f, 0x0301, "Logic3 Controller"),
|
||||
new DeviceIdTuple(0x0e6f, 0x0401, "Logic3 Controller"),
|
||||
new DeviceIdTuple(0x12ab, 0x0301, "PDP AFTERGLOW AX.1"),
|
||||
new DeviceIdTuple(0x146b, 0x0601, "BigBen Interactive XBOX 360 Controller"),
|
||||
new DeviceIdTuple(0x1532, 0x0037, "Razer Sabertooth"),
|
||||
new DeviceIdTuple(0x15e4, 0x3f00, "Power A Mini Pro Elite"),
|
||||
new DeviceIdTuple(0x15e4, 0x3f0a, "Xbox Airflo wired controller"),
|
||||
new DeviceIdTuple(0x15e4, 0x3f10, "Batarang Xbox 360 controller"),
|
||||
new DeviceIdTuple(0x162e, 0xbeef, "Joytech Neo-Se Take2"),
|
||||
new DeviceIdTuple(0x1689, 0xfd00, "Razer Onza Tournament Edition"),
|
||||
new DeviceIdTuple(0x1689, 0xfd01, "Razer Onza Classic Edition"),
|
||||
new DeviceIdTuple(0x24c6, 0x5d04, "Razer Sabertooth"),
|
||||
new DeviceIdTuple(0x1bad, 0xf016, "Mad Catz Xbox 360 Controller"),
|
||||
new DeviceIdTuple(0x1bad, 0xf023, "MLG Pro Circuit Controller (Xbox)"),
|
||||
new DeviceIdTuple(0x1bad, 0xf900, "Harmonix Xbox 360 Controller"),
|
||||
new DeviceIdTuple(0x1bad, 0xf901, "Gamestop Xbox 360 Controller"),
|
||||
new DeviceIdTuple(0x1bad, 0xf903, "Tron Xbox 360 controller"),
|
||||
new DeviceIdTuple(0x24c6, 0x5300, "PowerA MINI PROEX Controller"),
|
||||
new DeviceIdTuple(0x24c6, 0x5303, "Xbox Airflo wired controller"),
|
||||
};
|
||||
|
||||
public static boolean canClaimDevice(UsbDevice device) {
|
||||
for (DeviceIdTuple tuple : supportedDeviceTuples) {
|
||||
if (device.getVendorId() == tuple.vid && device.getProductId() == tuple.pid) {
|
||||
LimeLog.info("XB360 can claim device: " + tuple.name);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public Xbox360Controller(UsbDevice device, UsbDeviceConnection connection, int deviceId, UsbDriverListener listener) {
|
||||
super(device, connection, deviceId, listener);
|
||||
}
|
||||
|
||||
private int unsignByte(byte b) {
|
||||
if (b < 0) {
|
||||
return b + 256;
|
||||
}
|
||||
else {
|
||||
return b;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean handleRead(ByteBuffer buffer) {
|
||||
if (buffer.limit() < 14) {
|
||||
LimeLog.severe("Read too small: "+buffer.limit());
|
||||
return false;
|
||||
}
|
||||
|
||||
// Skip first short
|
||||
buffer.position(buffer.position() + 2);
|
||||
|
||||
// DPAD
|
||||
byte b = buffer.get();
|
||||
setButtonFlag(ControllerPacket.LEFT_FLAG, b & 0x04);
|
||||
setButtonFlag(ControllerPacket.RIGHT_FLAG, b & 0x08);
|
||||
setButtonFlag(ControllerPacket.UP_FLAG, b & 0x01);
|
||||
setButtonFlag(ControllerPacket.DOWN_FLAG, b & 0x02);
|
||||
|
||||
// Start/Select
|
||||
setButtonFlag(ControllerPacket.PLAY_FLAG, b & 0x10);
|
||||
setButtonFlag(ControllerPacket.BACK_FLAG, b & 0x20);
|
||||
|
||||
// LS/RS
|
||||
setButtonFlag(ControllerPacket.LS_CLK_FLAG, b & 0x40);
|
||||
setButtonFlag(ControllerPacket.RS_CLK_FLAG, b & 0x80);
|
||||
|
||||
// ABXY buttons
|
||||
b = buffer.get();
|
||||
setButtonFlag(ControllerPacket.A_FLAG, b & 0x10);
|
||||
setButtonFlag(ControllerPacket.B_FLAG, b & 0x20);
|
||||
setButtonFlag(ControllerPacket.X_FLAG, b & 0x40);
|
||||
setButtonFlag(ControllerPacket.Y_FLAG, b & 0x80);
|
||||
|
||||
// LB/RB
|
||||
setButtonFlag(ControllerPacket.LB_FLAG, b & 0x01);
|
||||
setButtonFlag(ControllerPacket.RB_FLAG, b & 0x02);
|
||||
|
||||
// Xbox button
|
||||
setButtonFlag(ControllerPacket.SPECIAL_BUTTON_FLAG, b & 0x04);
|
||||
|
||||
// Triggers
|
||||
leftTrigger = unsignByte(buffer.get()) / 255.0f;
|
||||
rightTrigger = unsignByte(buffer.get()) / 255.0f;
|
||||
|
||||
// Left stick
|
||||
leftStickX = buffer.getShort() / 32767.0f;
|
||||
leftStickY = ~buffer.getShort() / 32767.0f;
|
||||
|
||||
// Right stick
|
||||
rightStickX = buffer.getShort() / 32767.0f;
|
||||
rightStickY = ~buffer.getShort() / 32767.0f;
|
||||
|
||||
// Return true to send input
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean sendLedCommand(byte command) {
|
||||
byte[] commandBuffer = {0x01, 0x03, command};
|
||||
|
||||
int res = connection.bulkTransfer(outEndpt, commandBuffer, commandBuffer.length, 3000);
|
||||
if (res != commandBuffer.length) {
|
||||
LimeLog.warning("LED set transfer failed: "+res);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean doInit() {
|
||||
// Turn the LED on corresponding to our device ID
|
||||
sendLedCommand((byte)(2 + (getControllerId() % 4)));
|
||||
|
||||
// No need to fail init if the LED command fails
|
||||
return true;
|
||||
}
|
||||
|
||||
private static class DeviceIdTuple {
|
||||
public final int vid;
|
||||
public final int pid;
|
||||
public final String name;
|
||||
|
||||
public DeviceIdTuple(int vid, int pid, String name) {
|
||||
this.vid = vid;
|
||||
this.pid = pid;
|
||||
this.name = name;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -13,19 +13,7 @@ import com.limelight.nvstream.input.ControllerPacket;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
|
||||
public class XboxOneController {
|
||||
private final UsbDevice device;
|
||||
private final UsbDeviceConnection connection;
|
||||
private final int deviceId;
|
||||
|
||||
private Thread inputThread;
|
||||
private UsbDriverListener listener;
|
||||
private boolean stopped;
|
||||
|
||||
private short buttonFlags;
|
||||
private float leftTrigger, rightTrigger;
|
||||
private float rightStickX, rightStickY;
|
||||
private float leftStickX, leftStickY;
|
||||
public class XboxOneController extends AbstractXboxController {
|
||||
|
||||
private static final int MICROSOFT_VID = 0x045e;
|
||||
private static final int XB1_IFACE_SUBCLASS = 71;
|
||||
@@ -35,28 +23,7 @@ public class XboxOneController {
|
||||
private static final byte[] XB1_INIT_DATA = {0x05, 0x20, 0x00, 0x01, 0x00};
|
||||
|
||||
public XboxOneController(UsbDevice device, UsbDeviceConnection connection, int deviceId, UsbDriverListener listener) {
|
||||
this.device = device;
|
||||
this.connection = connection;
|
||||
this.deviceId = deviceId;
|
||||
this.listener = listener;
|
||||
}
|
||||
|
||||
public int getControllerId() {
|
||||
return this.deviceId;
|
||||
}
|
||||
|
||||
private void setButtonFlag(int buttonFlag, int data) {
|
||||
if (data != 0) {
|
||||
buttonFlags |= buttonFlag;
|
||||
}
|
||||
else {
|
||||
buttonFlags &= ~buttonFlag;
|
||||
}
|
||||
}
|
||||
|
||||
private void reportInput() {
|
||||
listener.reportControllerState(deviceId, buttonFlags, leftStickX, leftStickY,
|
||||
rightStickX, rightStickY, leftTrigger, rightTrigger);
|
||||
super(device, connection, deviceId, listener);
|
||||
}
|
||||
|
||||
private void processButtons(ByteBuffer buffer) {
|
||||
@@ -90,137 +57,24 @@ public class XboxOneController {
|
||||
|
||||
rightStickX = buffer.getShort() / 32767.0f;
|
||||
rightStickY = ~buffer.getShort() / 32767.0f;
|
||||
|
||||
reportInput();
|
||||
}
|
||||
|
||||
private void processPacket(ByteBuffer buffer) {
|
||||
@Override
|
||||
protected boolean handleRead(ByteBuffer buffer) {
|
||||
switch (buffer.get())
|
||||
{
|
||||
case 0x20:
|
||||
buffer.position(buffer.position()+3);
|
||||
processButtons(buffer);
|
||||
break;
|
||||
return true;
|
||||
|
||||
case 0x07:
|
||||
buffer.position(buffer.position() + 3);
|
||||
setButtonFlag(ControllerPacket.SPECIAL_BUTTON_FLAG, buffer.get() & 0x01);
|
||||
reportInput();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void startInputThread(final UsbEndpoint inEndpt) {
|
||||
inputThread = new Thread() {
|
||||
public void run() {
|
||||
while (!isInterrupted() && !stopped) {
|
||||
byte[] buffer = new byte[64];
|
||||
|
||||
int res;
|
||||
|
||||
//
|
||||
// There's no way that I can tell to determine if a device has failed
|
||||
// or if the timeout has simply expired. We'll check how long the transfer
|
||||
// took to fail and assume the device failed if it happened before the timeout
|
||||
// expired.
|
||||
//
|
||||
|
||||
do {
|
||||
// Read the next input state packet
|
||||
long lastMillis = MediaCodecHelper.getMonotonicMillis();
|
||||
res = connection.bulkTransfer(inEndpt, buffer, buffer.length, 3000);
|
||||
if (res == -1 && MediaCodecHelper.getMonotonicMillis() - lastMillis < 1000) {
|
||||
LimeLog.warning("Detected device I/O error");
|
||||
XboxOneController.this.stop();
|
||||
break;
|
||||
}
|
||||
} while (res == -1 && !isInterrupted() && !stopped);
|
||||
|
||||
if (res == -1 || stopped) {
|
||||
break;
|
||||
}
|
||||
|
||||
processPacket(ByteBuffer.wrap(buffer, 0, res).order(ByteOrder.LITTLE_ENDIAN));
|
||||
}
|
||||
}
|
||||
};
|
||||
inputThread.setName("Xbox One Controller - Input Thread");
|
||||
inputThread.start();
|
||||
}
|
||||
|
||||
public boolean start() {
|
||||
// Force claim all interfaces
|
||||
for (int i = 0; i < device.getInterfaceCount(); i++) {
|
||||
UsbInterface iface = device.getInterface(i);
|
||||
|
||||
if (!connection.claimInterface(iface, true)) {
|
||||
LimeLog.warning("Failed to claim interfaces");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// Find the endpoints
|
||||
UsbEndpoint outEndpt = null;
|
||||
UsbEndpoint inEndpt = null;
|
||||
UsbInterface iface = device.getInterface(0);
|
||||
for (int i = 0; i < iface.getEndpointCount(); i++) {
|
||||
UsbEndpoint endpt = iface.getEndpoint(i);
|
||||
if (endpt.getDirection() == UsbConstants.USB_DIR_IN) {
|
||||
if (inEndpt != null) {
|
||||
LimeLog.warning("Found duplicate IN endpoint");
|
||||
return false;
|
||||
}
|
||||
inEndpt = endpt;
|
||||
}
|
||||
else if (endpt.getDirection() == UsbConstants.USB_DIR_OUT) {
|
||||
if (outEndpt != null) {
|
||||
LimeLog.warning("Found duplicate OUT endpoint");
|
||||
return false;
|
||||
}
|
||||
outEndpt = endpt;
|
||||
}
|
||||
}
|
||||
|
||||
// Make sure the required endpoints were present
|
||||
if (inEndpt == null || outEndpt == null) {
|
||||
LimeLog.warning("Missing required endpoint");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Send the initialization packet
|
||||
int res = connection.bulkTransfer(outEndpt, XB1_INIT_DATA, XB1_INIT_DATA.length, 3000);
|
||||
if (res != XB1_INIT_DATA.length) {
|
||||
LimeLog.warning("Initialization transfer failed: "+res);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Start listening for controller input
|
||||
startInputThread(inEndpt);
|
||||
|
||||
// Report this device added via the listener
|
||||
listener.deviceAdded(deviceId);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public void stop() {
|
||||
if (stopped) {
|
||||
return;
|
||||
}
|
||||
|
||||
stopped = true;
|
||||
|
||||
// Stop the input thread
|
||||
if (inputThread != null) {
|
||||
inputThread.interrupt();
|
||||
inputThread = null;
|
||||
}
|
||||
|
||||
// Report the device removed
|
||||
listener.deviceRemoved(deviceId);
|
||||
|
||||
// Close the USB connection
|
||||
connection.close();
|
||||
return false;
|
||||
}
|
||||
|
||||
public static boolean canClaimDevice(UsbDevice device) {
|
||||
@@ -230,4 +84,16 @@ public class XboxOneController {
|
||||
device.getInterface(0).getInterfaceSubclass() == XB1_IFACE_SUBCLASS &&
|
||||
device.getInterface(0).getInterfaceProtocol() == XB1_IFACE_PROTOCOL;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean doInit() {
|
||||
// Send the initialization packet
|
||||
int res = connection.bulkTransfer(outEndpt, XB1_INIT_DATA, XB1_INIT_DATA.length, 3000);
|
||||
if (res != XB1_INIT_DATA.length) {
|
||||
LimeLog.warning("Initialization transfer failed: "+res);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,17 @@
|
||||
package com.limelight.binding.input.evdev;
|
||||
|
||||
import android.content.Context;
|
||||
import android.app.Activity;
|
||||
import android.widget.Toast;
|
||||
|
||||
import com.limelight.LimeLog;
|
||||
|
||||
import java.io.DataOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.net.ServerSocket;
|
||||
import java.net.Socket;
|
||||
|
||||
public class EvdevHandler {
|
||||
|
||||
@@ -15,7 +21,10 @@ public class EvdevHandler {
|
||||
private boolean shutdown = false;
|
||||
private InputStream evdevIn;
|
||||
private OutputStream evdevOut;
|
||||
private Process reader;
|
||||
private Process su;
|
||||
private ServerSocket servSock;
|
||||
private Socket evdevSock;
|
||||
private Activity activity;
|
||||
|
||||
private static final byte UNGRAB_REQUEST = 1;
|
||||
private static final byte REGRAB_REQUEST = 2;
|
||||
@@ -27,19 +36,51 @@ public class EvdevHandler {
|
||||
int deltaY = 0;
|
||||
byte deltaScroll = 0;
|
||||
|
||||
// Launch the evdev reader shell
|
||||
ProcessBuilder builder = new ProcessBuilder("su", "-c", libraryPath+File.separatorChar+"libevdev_reader.so");
|
||||
builder.redirectErrorStream(false);
|
||||
|
||||
// Bind a local listening socket for evdevreader to connect to
|
||||
try {
|
||||
reader = builder.start();
|
||||
servSock = new ServerSocket(0, 1);
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
return;
|
||||
}
|
||||
|
||||
evdevIn = reader.getInputStream();
|
||||
evdevOut = reader.getOutputStream();
|
||||
// Launch a su shell
|
||||
ProcessBuilder builder = new ProcessBuilder("su");
|
||||
builder.redirectErrorStream(true);
|
||||
|
||||
try {
|
||||
su = builder.start();
|
||||
} catch (IOException e) {
|
||||
activity.runOnUiThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
Toast.makeText(activity, "This device is not rooted - Mouse capture is unavailable", Toast.LENGTH_LONG).show();
|
||||
}
|
||||
});
|
||||
e.printStackTrace();
|
||||
return;
|
||||
}
|
||||
|
||||
// Start evdevreader
|
||||
DataOutputStream suOut = new DataOutputStream(su.getOutputStream());
|
||||
try {
|
||||
suOut.writeChars(libraryPath+File.separatorChar+"libevdev_reader.so "+servSock.getLocalPort()+"\n");
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
return;
|
||||
}
|
||||
|
||||
// Wait for evdevreader's connection
|
||||
LimeLog.info("Waiting for EvdevReader connection to port "+servSock.getLocalPort());
|
||||
try {
|
||||
evdevSock = servSock.accept();
|
||||
evdevIn = evdevSock.getInputStream();
|
||||
evdevOut = evdevSock.getOutputStream();
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
return;
|
||||
}
|
||||
LimeLog.info("EvdevReader connected from port "+evdevSock.getPort());
|
||||
|
||||
while (!isInterrupted() && !shutdown) {
|
||||
EvdevEvent event;
|
||||
@@ -122,9 +163,10 @@ public class EvdevHandler {
|
||||
}
|
||||
};
|
||||
|
||||
public EvdevHandler(Context context, EvdevListener listener) {
|
||||
public EvdevHandler(Activity activity, EvdevListener listener) {
|
||||
this.listener = listener;
|
||||
this.libraryPath = context.getApplicationInfo().nativeLibraryDir;
|
||||
this.activity = activity;
|
||||
this.libraryPath = activity.getApplicationInfo().nativeLibraryDir;
|
||||
}
|
||||
|
||||
public void regrabAll() {
|
||||
@@ -159,6 +201,22 @@ public class EvdevHandler {
|
||||
shutdown = true;
|
||||
handlerThread.interrupt();
|
||||
|
||||
if (servSock != null) {
|
||||
try {
|
||||
servSock.close();
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
if (evdevSock != null) {
|
||||
try {
|
||||
evdevSock.close();
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
if (evdevIn != null) {
|
||||
try {
|
||||
evdevIn.close();
|
||||
@@ -175,8 +233,8 @@ public class EvdevHandler {
|
||||
}
|
||||
}
|
||||
|
||||
if (reader != null) {
|
||||
reader.destroy();
|
||||
if (su != null) {
|
||||
su.destroy();
|
||||
}
|
||||
|
||||
try {
|
||||
|
||||
@@ -0,0 +1,349 @@
|
||||
/**
|
||||
* Created by Karim Mreisi.
|
||||
*/
|
||||
|
||||
package com.limelight.binding.input.virtual_controller;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.Paint;
|
||||
import android.view.MotionEvent;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* This is a analog stick on screen element. It is used to get 2-Axis user input.
|
||||
*/
|
||||
public class AnalogStick extends VirtualControllerElement {
|
||||
|
||||
/**
|
||||
* outer radius size in percent of the ui element
|
||||
*/
|
||||
public static final int SIZE_RADIUS_COMPLETE = 90;
|
||||
/**
|
||||
* analog stick size in percent of the ui element
|
||||
*/
|
||||
public static final int SIZE_RADIUS_ANALOG_STICK = 90;
|
||||
/**
|
||||
* dead zone size in percent of the ui element
|
||||
*/
|
||||
public static final int SIZE_RADIUS_DEADZONE = 90;
|
||||
/**
|
||||
* time frame for a double click
|
||||
*/
|
||||
public final static long timeoutDoubleClick = 350;
|
||||
|
||||
/**
|
||||
* touch down time until the deadzone is lifted to allow precise movements with the analog sticks
|
||||
*/
|
||||
public final static long timeoutDeadzone = 150;
|
||||
|
||||
/**
|
||||
* Listener interface to update registered observers.
|
||||
*/
|
||||
public interface AnalogStickListener {
|
||||
|
||||
/**
|
||||
* onMovement event will be fired on real analog stick movement (outside of the deadzone).
|
||||
*
|
||||
* @param x horizontal position, value from -1.0 ... 0 .. 1.0
|
||||
* @param y vertical position, value from -1.0 ... 0 .. 1.0
|
||||
*/
|
||||
void onMovement(float x, float y);
|
||||
|
||||
/**
|
||||
* onClick event will be fired on click on the analog stick
|
||||
*/
|
||||
void onClick();
|
||||
|
||||
/**
|
||||
* onDoubleClick event will be fired on a double click in a short time frame on the analog
|
||||
* stick.
|
||||
*/
|
||||
void onDoubleClick();
|
||||
|
||||
/**
|
||||
* onRevoke event will be fired on unpress of the analog stick.
|
||||
*/
|
||||
void onRevoke();
|
||||
}
|
||||
|
||||
/**
|
||||
* Movement states of the analog sick.
|
||||
*/
|
||||
private enum STICK_STATE {
|
||||
NO_MOVEMENT,
|
||||
MOVED_IN_DEAD_ZONE,
|
||||
MOVED_ACTIVE
|
||||
}
|
||||
|
||||
/**
|
||||
* Click type states.
|
||||
*/
|
||||
private enum CLICK_STATE {
|
||||
SINGLE,
|
||||
DOUBLE
|
||||
}
|
||||
|
||||
/**
|
||||
* configuration if the analog stick should be displayed as circle or square
|
||||
*/
|
||||
private boolean circle_stick = true; // TODO: implement square sick for simulations
|
||||
|
||||
/**
|
||||
* outer radius, this size will be automatically updated on resize
|
||||
*/
|
||||
private float radius_complete = 0;
|
||||
/**
|
||||
* analog stick radius, this size will be automatically updated on resize
|
||||
*/
|
||||
private float radius_analog_stick = 0;
|
||||
/**
|
||||
* dead zone radius, this size will be automatically updated on resize
|
||||
*/
|
||||
private float radius_dead_zone = 0;
|
||||
|
||||
/**
|
||||
* horizontal position in relation to the center of the element
|
||||
*/
|
||||
private float relative_x = 0;
|
||||
/**
|
||||
* vertical position in relation to the center of the element
|
||||
*/
|
||||
private float relative_y = 0;
|
||||
|
||||
|
||||
private double movement_radius = 0;
|
||||
private double movement_angle = 0;
|
||||
|
||||
private float position_stick_x = 0;
|
||||
private float position_stick_y = 0;
|
||||
|
||||
private final Paint paint = new Paint();
|
||||
|
||||
private STICK_STATE stick_state = STICK_STATE.NO_MOVEMENT;
|
||||
private CLICK_STATE click_state = CLICK_STATE.SINGLE;
|
||||
|
||||
private List<AnalogStickListener> listeners = new ArrayList<>();
|
||||
private long timeLastClick = 0;
|
||||
|
||||
private static double getMovementRadius(float x, float y) {
|
||||
return Math.sqrt(x * x + y * y);
|
||||
}
|
||||
|
||||
private static double getAngle(float way_x, float way_y) {
|
||||
// prevent divisions by zero for corner cases
|
||||
if (way_x == 0) {
|
||||
return way_y < 0 ? Math.PI : 0;
|
||||
} else if (way_y == 0) {
|
||||
if (way_x > 0) {
|
||||
return Math.PI * 3 / 2;
|
||||
} else if (way_x < 0) {
|
||||
return Math.PI * 1 / 2;
|
||||
}
|
||||
}
|
||||
// return correct calculated angle for each quadrant
|
||||
if (way_x > 0) {
|
||||
if (way_y < 0) {
|
||||
// first quadrant
|
||||
return 3 * Math.PI / 2 + Math.atan((double) (-way_y / way_x));
|
||||
} else {
|
||||
// second quadrant
|
||||
return Math.PI + Math.atan((double) (way_x / way_y));
|
||||
}
|
||||
} else {
|
||||
if (way_y > 0) {
|
||||
// third quadrant
|
||||
return Math.PI / 2 + Math.atan((double) (way_y / -way_x));
|
||||
} else {
|
||||
// fourth quadrant
|
||||
return 0 + Math.atan((double) (-way_x / -way_y));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public AnalogStick(VirtualController controller, Context context) {
|
||||
super(controller, context);
|
||||
// reset stick position
|
||||
position_stick_x = getWidth() / 2;
|
||||
position_stick_y = getHeight() / 2;
|
||||
}
|
||||
|
||||
public void addAnalogStickListener(AnalogStickListener listener) {
|
||||
listeners.add(listener);
|
||||
}
|
||||
|
||||
private void notifyOnMovement(float x, float y) {
|
||||
_DBG("movement x: " + x + " movement y: " + y);
|
||||
// notify listeners
|
||||
for (AnalogStickListener listener : listeners) {
|
||||
listener.onMovement(x, y);
|
||||
}
|
||||
}
|
||||
|
||||
private void notifyOnClick() {
|
||||
_DBG("click");
|
||||
// notify listeners
|
||||
for (AnalogStickListener listener : listeners) {
|
||||
listener.onClick();
|
||||
}
|
||||
}
|
||||
|
||||
private void notifyOnDoubleClick() {
|
||||
_DBG("double click");
|
||||
// notify listeners
|
||||
for (AnalogStickListener listener : listeners) {
|
||||
listener.onDoubleClick();
|
||||
}
|
||||
}
|
||||
|
||||
private void notifyOnRevoke() {
|
||||
_DBG("revoke");
|
||||
// notify listeners
|
||||
for (AnalogStickListener listener : listeners) {
|
||||
listener.onRevoke();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
|
||||
// calculate new radius sizes depending
|
||||
radius_complete = getPercent(getCorrectWidth() / 2, 90);
|
||||
radius_dead_zone = getPercent(getCorrectWidth() / 2, 30);
|
||||
radius_analog_stick = getPercent(getCorrectWidth() / 2, 20);
|
||||
|
||||
super.onSizeChanged(w, h, oldw, oldh);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onElementDraw(Canvas canvas) {
|
||||
// set transparent background
|
||||
canvas.drawColor(Color.TRANSPARENT);
|
||||
|
||||
paint.setStyle(Paint.Style.STROKE);
|
||||
paint.setStrokeWidth(getDefaultStrokeWidth());
|
||||
|
||||
// draw outer circle
|
||||
if (!isPressed() || click_state == CLICK_STATE.SINGLE) {
|
||||
paint.setColor(getDefaultColor());
|
||||
} else {
|
||||
paint.setColor(pressedColor);
|
||||
}
|
||||
canvas.drawCircle(getWidth() / 2, getHeight() / 2, radius_complete, paint);
|
||||
|
||||
paint.setColor(getDefaultColor());
|
||||
// draw dead zone
|
||||
canvas.drawCircle(getWidth() / 2, getHeight() / 2, radius_dead_zone, paint);
|
||||
|
||||
// draw stick depending on state
|
||||
switch (stick_state) {
|
||||
case NO_MOVEMENT: {
|
||||
paint.setColor(getDefaultColor());
|
||||
canvas.drawCircle(getWidth() / 2, getHeight() / 2, radius_analog_stick, paint);
|
||||
break;
|
||||
}
|
||||
case MOVED_IN_DEAD_ZONE:
|
||||
case MOVED_ACTIVE: {
|
||||
paint.setColor(pressedColor);
|
||||
canvas.drawCircle(position_stick_x, position_stick_y, radius_analog_stick, paint);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void updatePosition() {
|
||||
// get 100% way
|
||||
float complete = radius_complete - radius_analog_stick;
|
||||
|
||||
// calculate relative way
|
||||
float correlated_y = (float) (Math.sin(Math.PI / 2 - movement_angle) * (movement_radius));
|
||||
float correlated_x = (float) (Math.cos(Math.PI / 2 - movement_angle) * (movement_radius));
|
||||
|
||||
// update positions
|
||||
position_stick_x = getWidth() / 2 - correlated_x;
|
||||
position_stick_y = getHeight() / 2 - correlated_y;
|
||||
|
||||
// Stay active even if we're back in the deadzone because we know the user is actively
|
||||
// giving analog stick input and we don't want to snap back into the deadzone.
|
||||
// We also release the deadzone if the user keeps the stick pressed for a bit to allow
|
||||
// them to make precise movements.
|
||||
stick_state = (stick_state == STICK_STATE.MOVED_ACTIVE ||
|
||||
System.currentTimeMillis() - timeLastClick > timeoutDeadzone ||
|
||||
movement_radius > radius_dead_zone) ?
|
||||
STICK_STATE.MOVED_ACTIVE : STICK_STATE.MOVED_IN_DEAD_ZONE;
|
||||
|
||||
// trigger move event if state active
|
||||
if (stick_state == STICK_STATE.MOVED_ACTIVE) {
|
||||
notifyOnMovement(-correlated_x / complete, correlated_y / complete);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onElementTouchEvent(MotionEvent event) {
|
||||
// save last click state
|
||||
CLICK_STATE lastClickState = click_state;
|
||||
|
||||
// get absolute way for each axis
|
||||
relative_x = -(getWidth() / 2 - event.getX());
|
||||
relative_y = -(getHeight() / 2 - event.getY());
|
||||
|
||||
// get radius and angel of movement from center
|
||||
movement_radius = getMovementRadius(relative_x, relative_y);
|
||||
movement_angle = getAngle(relative_x, relative_y);
|
||||
|
||||
// chop radius if out of outer circle and already pressed
|
||||
if (movement_radius > (radius_complete - radius_analog_stick)) {
|
||||
// not pressed already, so ignore event from outer circle
|
||||
if (!isPressed()) {
|
||||
return false;
|
||||
}
|
||||
movement_radius = radius_complete - radius_analog_stick;
|
||||
}
|
||||
|
||||
// handle event depending on action
|
||||
switch (event.getActionMasked()) {
|
||||
// down event (touch event)
|
||||
case MotionEvent.ACTION_DOWN:
|
||||
case MotionEvent.ACTION_POINTER_DOWN: {
|
||||
// set to dead zoned, will be corrected in update position if necessary
|
||||
stick_state = STICK_STATE.MOVED_IN_DEAD_ZONE;
|
||||
// check for double click
|
||||
if (lastClickState == CLICK_STATE.SINGLE &&
|
||||
timeLastClick + timeoutDoubleClick > System.currentTimeMillis()) {
|
||||
click_state = CLICK_STATE.DOUBLE;
|
||||
notifyOnDoubleClick();
|
||||
} else {
|
||||
click_state = CLICK_STATE.SINGLE;
|
||||
notifyOnClick();
|
||||
}
|
||||
// reset last click timestamp
|
||||
timeLastClick = System.currentTimeMillis();
|
||||
// set item pressed and update
|
||||
setPressed(true);
|
||||
break;
|
||||
}
|
||||
// up event (revoke touch)
|
||||
case MotionEvent.ACTION_UP:
|
||||
case MotionEvent.ACTION_POINTER_UP: {
|
||||
setPressed(false);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (isPressed()) {
|
||||
// when is pressed calculate new positions (will trigger movement if necessary)
|
||||
updatePosition();
|
||||
} else {
|
||||
stick_state = STICK_STATE.NO_MOVEMENT;
|
||||
notifyOnRevoke();
|
||||
// not longer pressed reset analog stick
|
||||
notifyOnMovement(0, 0);
|
||||
}
|
||||
// refresh view
|
||||
invalidate();
|
||||
// accept the touch event
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,237 @@
|
||||
/**
|
||||
* Created by Karim Mreisi.
|
||||
*/
|
||||
|
||||
package com.limelight.binding.input.virtual_controller;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.view.MotionEvent;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Timer;
|
||||
import java.util.TimerTask;
|
||||
|
||||
/**
|
||||
* This is a digital button on screen element. It is used to get click and double click user input.
|
||||
*/
|
||||
public class DigitalButton extends VirtualControllerElement {
|
||||
|
||||
/**
|
||||
* Listener interface to update registered observers.
|
||||
*/
|
||||
public interface DigitalButtonListener {
|
||||
|
||||
/**
|
||||
* onClick event will be fired on button click.
|
||||
*/
|
||||
void onClick();
|
||||
|
||||
/**
|
||||
* onLongClick event will be fired on button long click.
|
||||
*/
|
||||
void onLongClick();
|
||||
|
||||
/**
|
||||
* onRelease event will be fired on button unpress.
|
||||
*/
|
||||
void onRelease();
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
private class TimerLongClickTimerTask extends TimerTask {
|
||||
@Override
|
||||
public void run() {
|
||||
onLongClickCallback();
|
||||
}
|
||||
}
|
||||
|
||||
private List<DigitalButtonListener> listeners = new ArrayList<DigitalButtonListener>();
|
||||
private String text = "";
|
||||
private int icon = -1;
|
||||
private long timerLongClickTimeout = 3000;
|
||||
private Timer timerLongClick = null;
|
||||
private TimerLongClickTimerTask longClickTimerTask = null;
|
||||
|
||||
private final Paint paint = new Paint();
|
||||
|
||||
private int layer;
|
||||
private DigitalButton movingButton = null;
|
||||
|
||||
boolean inRange(float x, float y) {
|
||||
return (this.getX() < x && this.getX() + this.getWidth() > x) &&
|
||||
(this.getY() < y && this.getY() + this.getHeight() > y);
|
||||
}
|
||||
|
||||
public boolean checkMovement(float x, float y, DigitalButton movingButton) {
|
||||
// check if the movement happened in the same layer
|
||||
if (movingButton.layer != this.layer) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// save current pressed state
|
||||
boolean wasPressed = isPressed();
|
||||
|
||||
// check if the movement directly happened on the button
|
||||
if ((this.movingButton == null || movingButton == this.movingButton)
|
||||
&& this.inRange(x, y)) {
|
||||
// set button pressed state depending on moving button pressed state
|
||||
if (this.isPressed() != movingButton.isPressed()) {
|
||||
this.setPressed(movingButton.isPressed());
|
||||
}
|
||||
}
|
||||
// check if the movement is outside of the range and the movement button
|
||||
// is the saved moving button
|
||||
else if (movingButton == this.movingButton) {
|
||||
this.setPressed(false);
|
||||
}
|
||||
|
||||
// check if a change occurred
|
||||
if (wasPressed != isPressed()) {
|
||||
if (isPressed()) {
|
||||
// is pressed set moving button and emit click event
|
||||
this.movingButton = movingButton;
|
||||
onClickCallback();
|
||||
} else {
|
||||
// no longer pressed reset moving button and emit release event
|
||||
this.movingButton = null;
|
||||
onReleaseCallback();
|
||||
}
|
||||
|
||||
invalidate();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private void checkMovementForAllButtons(float x, float y) {
|
||||
for (VirtualControllerElement element : virtualController.getElements()) {
|
||||
if (element != this && element instanceof DigitalButton) {
|
||||
((DigitalButton) element).checkMovement(x, y, this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public DigitalButton(VirtualController controller, int layer, Context context) {
|
||||
super(controller, context);
|
||||
this.layer = layer;
|
||||
}
|
||||
|
||||
public void addDigitalButtonListener(DigitalButtonListener listener) {
|
||||
listeners.add(listener);
|
||||
}
|
||||
|
||||
public void setText(String text) {
|
||||
this.text = text;
|
||||
invalidate();
|
||||
}
|
||||
|
||||
public void setIcon(int id) {
|
||||
this.icon = id;
|
||||
invalidate();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onElementDraw(Canvas canvas) {
|
||||
// set transparent background
|
||||
canvas.drawColor(Color.TRANSPARENT);
|
||||
|
||||
paint.setTextSize(getPercent(getWidth(), 30));
|
||||
paint.setTextAlign(Paint.Align.CENTER);
|
||||
paint.setStrokeWidth(getDefaultStrokeWidth());
|
||||
|
||||
paint.setColor(isPressed() ? pressedColor : getDefaultColor());
|
||||
paint.setStyle(Paint.Style.STROKE);
|
||||
canvas.drawRect(paint.getStrokeWidth(), paint.getStrokeWidth(),
|
||||
getWidth() - paint.getStrokeWidth(), getHeight() - paint.getStrokeWidth(), paint);
|
||||
|
||||
if (icon != -1) {
|
||||
Drawable d = getResources().getDrawable(icon);
|
||||
d.setBounds(5, 5, getWidth() - 5, getHeight() - 5);
|
||||
d.draw(canvas);
|
||||
} else {
|
||||
paint.setStyle(Paint.Style.FILL_AND_STROKE);
|
||||
paint.setStrokeWidth(getDefaultStrokeWidth()/2);
|
||||
canvas.drawText(text, getPercent(getWidth(), 50), getPercent(getHeight(), 63), paint);
|
||||
}
|
||||
}
|
||||
|
||||
private void onClickCallback() {
|
||||
_DBG("clicked");
|
||||
// notify listeners
|
||||
for (DigitalButtonListener listener : listeners) {
|
||||
listener.onClick();
|
||||
}
|
||||
|
||||
timerLongClick = new Timer();
|
||||
longClickTimerTask = new TimerLongClickTimerTask();
|
||||
timerLongClick.schedule(longClickTimerTask, timerLongClickTimeout);
|
||||
}
|
||||
|
||||
private void onLongClickCallback() {
|
||||
_DBG("long click");
|
||||
// notify listeners
|
||||
for (DigitalButtonListener listener : listeners) {
|
||||
listener.onLongClick();
|
||||
}
|
||||
}
|
||||
|
||||
private void onReleaseCallback() {
|
||||
_DBG("released");
|
||||
// notify listeners
|
||||
for (DigitalButtonListener listener : listeners) {
|
||||
listener.onRelease();
|
||||
}
|
||||
timerLongClick.cancel();
|
||||
longClickTimerTask.cancel();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onElementTouchEvent(MotionEvent event) {
|
||||
// get masked (not specific to a pointer) action
|
||||
float x = getX() + event.getX();
|
||||
float y = getY() + event.getY();
|
||||
int action = event.getActionMasked();
|
||||
|
||||
switch (action) {
|
||||
case MotionEvent.ACTION_DOWN:
|
||||
case MotionEvent.ACTION_POINTER_DOWN: {
|
||||
movingButton = null;
|
||||
setPressed(true);
|
||||
onClickCallback();
|
||||
|
||||
invalidate();
|
||||
|
||||
return true;
|
||||
}
|
||||
case MotionEvent.ACTION_MOVE: {
|
||||
checkMovementForAllButtons(x, y);
|
||||
|
||||
return true;
|
||||
}
|
||||
case MotionEvent.ACTION_CANCEL:
|
||||
case MotionEvent.ACTION_UP:
|
||||
case MotionEvent.ACTION_POINTER_UP: {
|
||||
setPressed(false);
|
||||
onReleaseCallback();
|
||||
|
||||
checkMovementForAllButtons(x, y);
|
||||
|
||||
invalidate();
|
||||
|
||||
return true;
|
||||
}
|
||||
default: {
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,205 @@
|
||||
/**
|
||||
* Created by Karim Mreisi.
|
||||
*/
|
||||
|
||||
package com.limelight.binding.input.virtual_controller;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.Paint;
|
||||
import android.view.MotionEvent;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class DigitalPad extends VirtualControllerElement {
|
||||
public final static int DIGITAL_PAD_DIRECTION_NO_DIRECTION = 0;
|
||||
int direction = DIGITAL_PAD_DIRECTION_NO_DIRECTION;
|
||||
public final static int DIGITAL_PAD_DIRECTION_LEFT = 1;
|
||||
public final static int DIGITAL_PAD_DIRECTION_UP = 2;
|
||||
public final static int DIGITAL_PAD_DIRECTION_RIGHT = 4;
|
||||
public final static int DIGITAL_PAD_DIRECTION_DOWN = 8;
|
||||
List<DigitalPadListener> listeners = new ArrayList<DigitalPadListener>();
|
||||
|
||||
private static final int DPAD_MARGIN = 5;
|
||||
|
||||
private final Paint paint = new Paint();
|
||||
|
||||
public DigitalPad(VirtualController controller, Context context) {
|
||||
super(controller, context);
|
||||
}
|
||||
|
||||
public void addDigitalPadListener(DigitalPadListener listener) {
|
||||
listeners.add(listener);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onElementDraw(Canvas canvas) {
|
||||
// set transparent background
|
||||
canvas.drawColor(Color.TRANSPARENT);
|
||||
|
||||
paint.setTextSize(getPercent(getCorrectWidth(), 20));
|
||||
paint.setTextAlign(Paint.Align.CENTER);
|
||||
paint.setStrokeWidth(getDefaultStrokeWidth());
|
||||
|
||||
if (direction == DIGITAL_PAD_DIRECTION_NO_DIRECTION) {
|
||||
// draw no direction rect
|
||||
paint.setStyle(Paint.Style.STROKE);
|
||||
paint.setColor(getDefaultColor());
|
||||
canvas.drawRect(
|
||||
getPercent(getWidth(), 36), getPercent(getHeight(), 36),
|
||||
getPercent(getWidth(), 63), getPercent(getHeight(), 63),
|
||||
paint
|
||||
);
|
||||
}
|
||||
|
||||
// draw left rect
|
||||
paint.setColor(
|
||||
(direction & DIGITAL_PAD_DIRECTION_LEFT) > 0 ? pressedColor : getDefaultColor());
|
||||
paint.setStyle(Paint.Style.STROKE);
|
||||
canvas.drawRect(
|
||||
paint.getStrokeWidth()+DPAD_MARGIN, getPercent(getHeight(), 33),
|
||||
getPercent(getWidth(), 33), getPercent(getHeight(), 66),
|
||||
paint
|
||||
);
|
||||
|
||||
|
||||
// draw up rect
|
||||
paint.setColor(
|
||||
(direction & DIGITAL_PAD_DIRECTION_UP) > 0 ? pressedColor : getDefaultColor());
|
||||
paint.setStyle(Paint.Style.STROKE);
|
||||
canvas.drawRect(
|
||||
getPercent(getWidth(), 33), paint.getStrokeWidth()+DPAD_MARGIN,
|
||||
getPercent(getWidth(), 66), getPercent(getHeight(), 33),
|
||||
paint
|
||||
);
|
||||
|
||||
// draw right rect
|
||||
paint.setColor(
|
||||
(direction & DIGITAL_PAD_DIRECTION_RIGHT) > 0 ? pressedColor : getDefaultColor());
|
||||
paint.setStyle(Paint.Style.STROKE);
|
||||
canvas.drawRect(
|
||||
getPercent(getWidth(), 66), getPercent(getHeight(), 33),
|
||||
getWidth() - (paint.getStrokeWidth()+DPAD_MARGIN), getPercent(getHeight(), 66),
|
||||
paint
|
||||
);
|
||||
|
||||
// draw down rect
|
||||
paint.setColor(
|
||||
(direction & DIGITAL_PAD_DIRECTION_DOWN) > 0 ? pressedColor : getDefaultColor());
|
||||
paint.setStyle(Paint.Style.STROKE);
|
||||
canvas.drawRect(
|
||||
getPercent(getWidth(), 33), getPercent(getHeight(), 66),
|
||||
getPercent(getWidth(), 66), getHeight() - (paint.getStrokeWidth()+DPAD_MARGIN),
|
||||
paint
|
||||
);
|
||||
|
||||
// draw left up line
|
||||
paint.setColor((
|
||||
(direction & DIGITAL_PAD_DIRECTION_LEFT) > 0 &&
|
||||
(direction & DIGITAL_PAD_DIRECTION_UP) > 0
|
||||
) ? pressedColor : getDefaultColor()
|
||||
);
|
||||
paint.setStyle(Paint.Style.STROKE);
|
||||
canvas.drawLine(
|
||||
paint.getStrokeWidth()+DPAD_MARGIN, getPercent(getHeight(), 33),
|
||||
getPercent(getWidth(), 33), paint.getStrokeWidth()+DPAD_MARGIN,
|
||||
paint
|
||||
);
|
||||
|
||||
// draw up right line
|
||||
paint.setColor((
|
||||
(direction & DIGITAL_PAD_DIRECTION_UP) > 0 &&
|
||||
(direction & DIGITAL_PAD_DIRECTION_RIGHT) > 0
|
||||
) ? pressedColor : getDefaultColor()
|
||||
);
|
||||
paint.setStyle(Paint.Style.STROKE);
|
||||
canvas.drawLine(
|
||||
getPercent(getWidth(), 66), paint.getStrokeWidth()+DPAD_MARGIN,
|
||||
getWidth() - (paint.getStrokeWidth()+DPAD_MARGIN), getPercent(getHeight(), 33),
|
||||
paint
|
||||
);
|
||||
|
||||
// draw right down line
|
||||
paint.setColor((
|
||||
(direction & DIGITAL_PAD_DIRECTION_RIGHT) > 0 &&
|
||||
(direction & DIGITAL_PAD_DIRECTION_DOWN) > 0
|
||||
) ? pressedColor : getDefaultColor()
|
||||
);
|
||||
paint.setStyle(Paint.Style.STROKE);
|
||||
canvas.drawLine(
|
||||
getWidth()-paint.getStrokeWidth(), getPercent(getHeight(), 66),
|
||||
getPercent(getWidth(), 66), getHeight()-(paint.getStrokeWidth()+DPAD_MARGIN),
|
||||
paint
|
||||
);
|
||||
|
||||
// draw down left line
|
||||
paint.setColor((
|
||||
(direction & DIGITAL_PAD_DIRECTION_DOWN) > 0 &&
|
||||
(direction & DIGITAL_PAD_DIRECTION_LEFT) > 0
|
||||
) ? pressedColor : getDefaultColor()
|
||||
);
|
||||
paint.setStyle(Paint.Style.STROKE);
|
||||
canvas.drawLine(
|
||||
getPercent(getWidth(), 33), getHeight()-(paint.getStrokeWidth()+DPAD_MARGIN),
|
||||
paint.getStrokeWidth()+DPAD_MARGIN, getPercent(getHeight(), 66),
|
||||
paint
|
||||
);
|
||||
}
|
||||
|
||||
private void newDirectionCallback(int direction) {
|
||||
_DBG("direction: " + direction);
|
||||
|
||||
// notify listeners
|
||||
for (DigitalPadListener listener : listeners) {
|
||||
listener.onDirectionChange(direction);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onElementTouchEvent(MotionEvent event) {
|
||||
// get masked (not specific to a pointer) action
|
||||
switch (event.getActionMasked()) {
|
||||
case MotionEvent.ACTION_DOWN:
|
||||
case MotionEvent.ACTION_POINTER_DOWN:
|
||||
case MotionEvent.ACTION_MOVE: {
|
||||
direction = 0;
|
||||
|
||||
if (event.getX() < getPercent(getWidth(), 33)) {
|
||||
direction |= DIGITAL_PAD_DIRECTION_LEFT;
|
||||
}
|
||||
if (event.getX() > getPercent(getWidth(), 66)) {
|
||||
direction |= DIGITAL_PAD_DIRECTION_RIGHT;
|
||||
}
|
||||
if (event.getY() > getPercent(getHeight(), 66)) {
|
||||
direction |= DIGITAL_PAD_DIRECTION_DOWN;
|
||||
}
|
||||
if (event.getY() < getPercent(getHeight(), 33)) {
|
||||
direction |= DIGITAL_PAD_DIRECTION_UP;
|
||||
}
|
||||
newDirectionCallback(direction);
|
||||
invalidate();
|
||||
|
||||
return true;
|
||||
}
|
||||
case MotionEvent.ACTION_CANCEL:
|
||||
case MotionEvent.ACTION_UP:
|
||||
case MotionEvent.ACTION_POINTER_UP: {
|
||||
direction = 0;
|
||||
newDirectionCallback(direction);
|
||||
invalidate();
|
||||
|
||||
return true;
|
||||
}
|
||||
default: {
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public interface DigitalPadListener {
|
||||
void onDirectionChange(int direction);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
/**
|
||||
* Created by Karim Mreisi.
|
||||
*/
|
||||
|
||||
package com.limelight.binding.input.virtual_controller;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import com.limelight.nvstream.input.ControllerPacket;
|
||||
|
||||
public class LeftAnalogStick extends AnalogStick {
|
||||
public LeftAnalogStick(final VirtualController controller, final Context context) {
|
||||
super(controller, context);
|
||||
|
||||
addAnalogStickListener(new AnalogStick.AnalogStickListener() {
|
||||
@Override
|
||||
public void onMovement(float x, float y) {
|
||||
VirtualController.ControllerInputContext inputContext =
|
||||
controller.getControllerInputContext();
|
||||
inputContext.leftStickX = (short) (x * 0x7FFE);
|
||||
inputContext.leftStickY = (short) (y * 0x7FFE);
|
||||
|
||||
controller.sendControllerInputContext();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClick() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDoubleClick() {
|
||||
VirtualController.ControllerInputContext inputContext =
|
||||
controller.getControllerInputContext();
|
||||
inputContext.inputMap |= ControllerPacket.LS_CLK_FLAG;
|
||||
|
||||
controller.sendControllerInputContext();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRevoke() {
|
||||
VirtualController.ControllerInputContext inputContext =
|
||||
controller.getControllerInputContext();
|
||||
inputContext.inputMap &= ~ControllerPacket.LS_CLK_FLAG;
|
||||
|
||||
controller.sendControllerInputContext();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
/**
|
||||
* Created by Karim Mreisi.
|
||||
*/
|
||||
|
||||
package com.limelight.binding.input.virtual_controller;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
public class LeftTrigger extends DigitalButton {
|
||||
public LeftTrigger(final VirtualController controller, final int layer, final Context context) {
|
||||
super(controller, layer, context);
|
||||
addDigitalButtonListener(new DigitalButton.DigitalButtonListener() {
|
||||
@Override
|
||||
public void onClick() {
|
||||
VirtualController.ControllerInputContext inputContext =
|
||||
controller.getControllerInputContext();
|
||||
inputContext.leftTrigger = (byte) 0xFF;
|
||||
|
||||
controller.sendControllerInputContext();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLongClick() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRelease() {
|
||||
VirtualController.ControllerInputContext inputContext =
|
||||
controller.getControllerInputContext();
|
||||
inputContext.leftTrigger = (byte) 0x00;
|
||||
|
||||
controller.sendControllerInputContext();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
/**
|
||||
* Created by Karim Mreisi.
|
||||
*/
|
||||
|
||||
package com.limelight.binding.input.virtual_controller;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import com.limelight.nvstream.input.ControllerPacket;
|
||||
|
||||
public class RightAnalogStick extends AnalogStick {
|
||||
public RightAnalogStick(final VirtualController controller, final Context context) {
|
||||
super(controller, context);
|
||||
|
||||
addAnalogStickListener(new AnalogStick.AnalogStickListener() {
|
||||
@Override
|
||||
public void onMovement(float x, float y) {
|
||||
VirtualController.ControllerInputContext inputContext =
|
||||
controller.getControllerInputContext();
|
||||
inputContext.rightStickX = (short) (x * 0x7FFE);
|
||||
inputContext.rightStickY = (short) (y * 0x7FFE);
|
||||
|
||||
controller.sendControllerInputContext();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClick() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDoubleClick() {
|
||||
VirtualController.ControllerInputContext inputContext =
|
||||
controller.getControllerInputContext();
|
||||
inputContext.inputMap |= ControllerPacket.RS_CLK_FLAG;
|
||||
|
||||
controller.sendControllerInputContext();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRevoke() {
|
||||
VirtualController.ControllerInputContext inputContext =
|
||||
controller.getControllerInputContext();
|
||||
inputContext.inputMap &= ~ControllerPacket.RS_CLK_FLAG;
|
||||
|
||||
controller.sendControllerInputContext();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
/**
|
||||
* Created by Karim Mreisi.
|
||||
*/
|
||||
|
||||
package com.limelight.binding.input.virtual_controller;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
public class RightTrigger extends DigitalButton {
|
||||
public RightTrigger(final VirtualController controller, final int layer, final Context context) {
|
||||
super(controller, layer, context);
|
||||
addDigitalButtonListener(new DigitalButton.DigitalButtonListener() {
|
||||
@Override
|
||||
public void onClick() {
|
||||
VirtualController.ControllerInputContext inputContext =
|
||||
controller.getControllerInputContext();
|
||||
inputContext.rightTrigger = (byte) 0xFF;
|
||||
|
||||
controller.sendControllerInputContext();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLongClick() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRelease() {
|
||||
VirtualController.ControllerInputContext inputContext =
|
||||
controller.getControllerInputContext();
|
||||
inputContext.rightTrigger = (byte) 0x00;
|
||||
|
||||
controller.sendControllerInputContext();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
+162
@@ -0,0 +1,162 @@
|
||||
/**
|
||||
* Created by Karim Mreisi.
|
||||
*/
|
||||
|
||||
package com.limelight.binding.input.virtual_controller;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.DisplayMetrics;
|
||||
import android.view.View;
|
||||
import android.widget.Button;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.RelativeLayout;
|
||||
import android.widget.Toast;
|
||||
|
||||
import com.limelight.R;
|
||||
import com.limelight.nvstream.NvConnection;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class VirtualController {
|
||||
public class ControllerInputContext {
|
||||
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 enum ControllerMode {
|
||||
Active,
|
||||
Configuration
|
||||
}
|
||||
|
||||
private static final boolean _PRINT_DEBUG_INFORMATION = false;
|
||||
|
||||
private NvConnection connection = null;
|
||||
private Context context = null;
|
||||
|
||||
private FrameLayout frame_layout = null;
|
||||
private RelativeLayout relative_layout = null;
|
||||
|
||||
ControllerMode currentMode = ControllerMode.Active;
|
||||
ControllerInputContext inputContext = new ControllerInputContext();
|
||||
|
||||
private RelativeLayout.LayoutParams layoutParamsButtonConfigure = null;
|
||||
private Button buttonConfigure = null;
|
||||
|
||||
private List<VirtualControllerElement> elements = new ArrayList<VirtualControllerElement>();
|
||||
|
||||
public VirtualController(final NvConnection conn, FrameLayout layout, final Context context) {
|
||||
this.connection = conn;
|
||||
this.frame_layout = layout;
|
||||
this.context = context;
|
||||
|
||||
relative_layout = new RelativeLayout(context);
|
||||
|
||||
frame_layout.addView(relative_layout);
|
||||
|
||||
buttonConfigure = new Button(context);
|
||||
buttonConfigure.setBackgroundResource(R.drawable.settings);
|
||||
buttonConfigure.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
String message;
|
||||
|
||||
if (currentMode == ControllerMode.Configuration) {
|
||||
currentMode = ControllerMode.Active;
|
||||
message = "Exiting configuration mode";
|
||||
} else {
|
||||
currentMode = ControllerMode.Configuration;
|
||||
message = "Entering configuration mode";
|
||||
}
|
||||
|
||||
Toast.makeText(context, message, Toast.LENGTH_SHORT).show();
|
||||
|
||||
relative_layout.invalidate();
|
||||
|
||||
for (VirtualControllerElement element : elements) {
|
||||
element.invalidate();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void removeElements() {
|
||||
for (VirtualControllerElement element : elements) {
|
||||
relative_layout.removeView(element);
|
||||
}
|
||||
elements.clear();
|
||||
}
|
||||
|
||||
public void addElement(VirtualControllerElement element, int x, int y, int width, int height) {
|
||||
elements.add(element);
|
||||
RelativeLayout.LayoutParams layoutParams = new RelativeLayout.LayoutParams(width, height);
|
||||
layoutParams.setMargins(x, y, 0, 0);
|
||||
|
||||
relative_layout.addView(element, layoutParams);
|
||||
}
|
||||
|
||||
public List<VirtualControllerElement> getElements() {
|
||||
return elements;
|
||||
}
|
||||
|
||||
private static final void _DBG(String text) {
|
||||
if (_PRINT_DEBUG_INFORMATION) {
|
||||
System.out.println("VirtualController: " + text);
|
||||
}
|
||||
}
|
||||
|
||||
public void refreshLayout() {
|
||||
relative_layout.removeAllViews();
|
||||
removeElements();
|
||||
|
||||
DisplayMetrics screen = context.getResources().getDisplayMetrics();
|
||||
|
||||
int buttonSize = (int)(screen.heightPixels*0.05f);
|
||||
layoutParamsButtonConfigure = new RelativeLayout.LayoutParams(buttonSize, buttonSize);
|
||||
relative_layout.addView(buttonConfigure, layoutParamsButtonConfigure);
|
||||
|
||||
VirtualControllerConfigurationLoader.createDefaultLayout(this, context);
|
||||
}
|
||||
|
||||
public ControllerMode getControllerMode() {
|
||||
return currentMode;
|
||||
}
|
||||
|
||||
public ControllerInputContext getControllerInputContext() {
|
||||
return inputContext;
|
||||
}
|
||||
|
||||
public void sendControllerInputContext() {
|
||||
sendControllerInputPacket();
|
||||
}
|
||||
|
||||
private void sendControllerInputPacket() {
|
||||
try {
|
||||
_DBG("INPUT_MAP + " + inputContext.inputMap);
|
||||
_DBG("LEFT_TRIGGER " + inputContext.leftTrigger);
|
||||
_DBG("RIGHT_TRIGGER " + inputContext.rightTrigger);
|
||||
_DBG("LEFT STICK X: " + inputContext.leftStickX + " Y: " + inputContext.leftStickY);
|
||||
_DBG("RIGHT STICK X: " + inputContext.rightStickX + " Y: " + inputContext.rightStickY);
|
||||
_DBG("RIGHT STICK X: " + inputContext.rightStickX + " Y: " + inputContext.rightStickY);
|
||||
|
||||
if (connection != null) {
|
||||
connection.sendControllerInput(
|
||||
inputContext.inputMap,
|
||||
inputContext.leftTrigger,
|
||||
inputContext.rightTrigger,
|
||||
inputContext.leftStickX,
|
||||
inputContext.leftStickY,
|
||||
inputContext.rightStickX,
|
||||
inputContext.rightStickY
|
||||
);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
+285
@@ -0,0 +1,285 @@
|
||||
/**
|
||||
* Created by Karim Mreisi.
|
||||
*/
|
||||
|
||||
package com.limelight.binding.input.virtual_controller;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.DisplayMetrics;
|
||||
|
||||
import com.limelight.nvstream.input.ControllerPacket;
|
||||
|
||||
public class VirtualControllerConfigurationLoader {
|
||||
private static final String PROFILE_PATH = "profiles";
|
||||
|
||||
private static int getPercent(
|
||||
int percent,
|
||||
int total) {
|
||||
return (int) (((float) total / (float) 100) * (float) percent);
|
||||
}
|
||||
|
||||
private static DigitalPad createDigitalPad(
|
||||
final VirtualController controller,
|
||||
final Context context) {
|
||||
|
||||
DigitalPad digitalPad = new DigitalPad(controller, context);
|
||||
digitalPad.addDigitalPadListener(new DigitalPad.DigitalPadListener() {
|
||||
@Override
|
||||
public void onDirectionChange(int direction) {
|
||||
VirtualController.ControllerInputContext inputContext =
|
||||
controller.getControllerInputContext();
|
||||
|
||||
if (direction == DigitalPad.DIGITAL_PAD_DIRECTION_NO_DIRECTION) {
|
||||
inputContext.inputMap &= ~ControllerPacket.LEFT_FLAG;
|
||||
inputContext.inputMap &= ~ControllerPacket.RIGHT_FLAG;
|
||||
inputContext.inputMap &= ~ControllerPacket.UP_FLAG;
|
||||
inputContext.inputMap &= ~ControllerPacket.DOWN_FLAG;
|
||||
|
||||
controller.sendControllerInputContext();
|
||||
return;
|
||||
}
|
||||
if ((direction & DigitalPad.DIGITAL_PAD_DIRECTION_LEFT) > 0) {
|
||||
inputContext.inputMap |= ControllerPacket.LEFT_FLAG;
|
||||
}
|
||||
if ((direction & DigitalPad.DIGITAL_PAD_DIRECTION_RIGHT) > 0) {
|
||||
inputContext.inputMap |= ControllerPacket.RIGHT_FLAG;
|
||||
}
|
||||
if ((direction & DigitalPad.DIGITAL_PAD_DIRECTION_UP) > 0) {
|
||||
inputContext.inputMap |= ControllerPacket.UP_FLAG;
|
||||
}
|
||||
if ((direction & DigitalPad.DIGITAL_PAD_DIRECTION_DOWN) > 0) {
|
||||
inputContext.inputMap |= ControllerPacket.DOWN_FLAG;
|
||||
}
|
||||
controller.sendControllerInputContext();
|
||||
}
|
||||
});
|
||||
|
||||
return digitalPad;
|
||||
}
|
||||
|
||||
private static DigitalButton createDigitalButton(
|
||||
final int keyShort,
|
||||
final int keyLong,
|
||||
final int layer,
|
||||
final String text,
|
||||
final int icon,
|
||||
final VirtualController controller,
|
||||
final Context context) {
|
||||
DigitalButton button = new DigitalButton(controller, layer, context);
|
||||
button.setText(text);
|
||||
button.setIcon(icon);
|
||||
|
||||
button.addDigitalButtonListener(new DigitalButton.DigitalButtonListener() {
|
||||
@Override
|
||||
public void onClick() {
|
||||
VirtualController.ControllerInputContext inputContext =
|
||||
controller.getControllerInputContext();
|
||||
inputContext.inputMap |= keyShort;
|
||||
|
||||
controller.sendControllerInputContext();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLongClick() {
|
||||
VirtualController.ControllerInputContext inputContext =
|
||||
controller.getControllerInputContext();
|
||||
inputContext.inputMap |= keyLong;
|
||||
|
||||
controller.sendControllerInputContext();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRelease() {
|
||||
VirtualController.ControllerInputContext inputContext =
|
||||
controller.getControllerInputContext();
|
||||
inputContext.inputMap &= ~keyShort;
|
||||
inputContext.inputMap &= ~keyLong;
|
||||
|
||||
controller.sendControllerInputContext();
|
||||
}
|
||||
});
|
||||
|
||||
return button;
|
||||
}
|
||||
|
||||
private static DigitalButton createLeftTrigger(
|
||||
final int layer,
|
||||
final String text,
|
||||
final int icon,
|
||||
final VirtualController controller,
|
||||
final Context context) {
|
||||
LeftTrigger button = new LeftTrigger(controller, layer, context);
|
||||
button.setText(text);
|
||||
button.setIcon(icon);
|
||||
return button;
|
||||
}
|
||||
|
||||
private static DigitalButton createRightTrigger(
|
||||
final int layer,
|
||||
final String text,
|
||||
final int icon,
|
||||
final VirtualController controller,
|
||||
final Context context) {
|
||||
RightTrigger button = new RightTrigger(controller, layer, context);
|
||||
button.setText(text);
|
||||
button.setIcon(icon);
|
||||
return button;
|
||||
}
|
||||
|
||||
private static AnalogStick createLeftStick(
|
||||
final VirtualController controller,
|
||||
final Context context) {
|
||||
return new LeftAnalogStick(controller, context);
|
||||
}
|
||||
|
||||
private static AnalogStick createRightStick(
|
||||
final VirtualController controller,
|
||||
final Context context) {
|
||||
return new RightAnalogStick(controller, context);
|
||||
}
|
||||
|
||||
private static final int BUTTON_BASE_X = 65;
|
||||
private static final int BUTTON_BASE_Y = 5;
|
||||
private static final int BUTTON_WIDTH = getPercent(30, 33);
|
||||
private static final int BUTTON_HEIGHT = getPercent(40, 33);
|
||||
|
||||
public static void createDefaultLayout(final VirtualController controller, final Context context) {
|
||||
|
||||
DisplayMetrics screen = context.getResources().getDisplayMetrics();
|
||||
|
||||
// NOTE: Some of these getPercent() expressions seem like they can be combined
|
||||
// into a single call. Due to floating point rounding, this isn't actually possible.
|
||||
|
||||
controller.addElement(createDigitalPad(controller, context),
|
||||
getPercent(5, screen.widthPixels),
|
||||
getPercent(BUTTON_BASE_Y, screen.heightPixels),
|
||||
getPercent(30, screen.widthPixels),
|
||||
getPercent(40, screen.heightPixels)
|
||||
);
|
||||
|
||||
controller.addElement(createDigitalButton(
|
||||
ControllerPacket.A_FLAG, 0, 1, "A", -1, controller, context),
|
||||
getPercent(BUTTON_BASE_X, screen.widthPixels)+getPercent(BUTTON_WIDTH, screen.widthPixels),
|
||||
getPercent(BUTTON_BASE_Y, screen.heightPixels)+2*getPercent(BUTTON_HEIGHT, screen.heightPixels),
|
||||
getPercent(BUTTON_WIDTH, screen.widthPixels),
|
||||
getPercent(BUTTON_HEIGHT, screen.heightPixels)
|
||||
);
|
||||
|
||||
controller.addElement(createDigitalButton(
|
||||
ControllerPacket.B_FLAG, 0, 1, "B", -1, controller, context),
|
||||
getPercent(BUTTON_BASE_X, screen.widthPixels)+2*getPercent(BUTTON_WIDTH, screen.widthPixels),
|
||||
getPercent(BUTTON_BASE_Y, screen.heightPixels)+getPercent(BUTTON_HEIGHT, screen.heightPixels),
|
||||
getPercent(BUTTON_WIDTH, screen.widthPixels),
|
||||
getPercent(BUTTON_HEIGHT, screen.heightPixels)
|
||||
);
|
||||
|
||||
controller.addElement(createDigitalButton(
|
||||
ControllerPacket.X_FLAG, 0, 1, "X", -1, controller, context),
|
||||
getPercent(BUTTON_BASE_X, screen.widthPixels),
|
||||
getPercent(BUTTON_BASE_Y, screen.heightPixels)+getPercent(BUTTON_HEIGHT, screen.heightPixels),
|
||||
getPercent(BUTTON_WIDTH, screen.widthPixels),
|
||||
getPercent(BUTTON_HEIGHT, screen.heightPixels)
|
||||
);
|
||||
|
||||
controller.addElement(createDigitalButton(
|
||||
ControllerPacket.Y_FLAG, 0, 1, "Y", -1, controller, context),
|
||||
getPercent(BUTTON_BASE_X, screen.widthPixels)+getPercent(BUTTON_WIDTH, screen.widthPixels),
|
||||
getPercent(BUTTON_BASE_Y, screen.heightPixels),
|
||||
getPercent(BUTTON_WIDTH, screen.widthPixels),
|
||||
getPercent(BUTTON_HEIGHT, screen.heightPixels)
|
||||
);
|
||||
|
||||
controller.addElement(createLeftTrigger(
|
||||
0, "LT", -1, controller, context),
|
||||
getPercent(BUTTON_BASE_X, screen.widthPixels),
|
||||
getPercent(BUTTON_BASE_Y, screen.heightPixels),
|
||||
getPercent(BUTTON_WIDTH, screen.widthPixels),
|
||||
getPercent(BUTTON_HEIGHT, screen.heightPixels)
|
||||
);
|
||||
|
||||
controller.addElement(createRightTrigger(
|
||||
0, "RT", -1, controller, context),
|
||||
getPercent(BUTTON_BASE_X, screen.widthPixels)+2*getPercent(BUTTON_WIDTH, screen.widthPixels),
|
||||
getPercent(BUTTON_BASE_Y, screen.heightPixels),
|
||||
getPercent(BUTTON_WIDTH, screen.widthPixels),
|
||||
getPercent(BUTTON_HEIGHT, screen.heightPixels)
|
||||
);
|
||||
|
||||
controller.addElement(createDigitalButton(
|
||||
ControllerPacket.LB_FLAG, 0, 1, "LB", -1, controller, context),
|
||||
getPercent(BUTTON_BASE_X, screen.widthPixels),
|
||||
getPercent(BUTTON_BASE_Y, screen.heightPixels)+2*getPercent(BUTTON_HEIGHT, screen.heightPixels),
|
||||
getPercent(BUTTON_WIDTH, screen.widthPixels),
|
||||
getPercent(BUTTON_HEIGHT, screen.heightPixels)
|
||||
);
|
||||
|
||||
controller.addElement(createDigitalButton(
|
||||
ControllerPacket.RB_FLAG, 0, 1, "RB", -1, controller, context),
|
||||
getPercent(BUTTON_BASE_X, screen.widthPixels)+2*getPercent(BUTTON_WIDTH, screen.widthPixels),
|
||||
getPercent(BUTTON_BASE_Y, screen.heightPixels)+2*getPercent(BUTTON_HEIGHT, screen.heightPixels),
|
||||
getPercent(BUTTON_WIDTH, screen.widthPixels),
|
||||
getPercent(BUTTON_HEIGHT, screen.heightPixels)
|
||||
);
|
||||
|
||||
controller.addElement(createLeftStick(controller, context),
|
||||
getPercent(5, screen.widthPixels),
|
||||
getPercent(50, screen.heightPixels),
|
||||
getPercent(40, screen.widthPixels),
|
||||
getPercent(50, screen.heightPixels)
|
||||
);
|
||||
|
||||
controller.addElement(createRightStick(controller, context),
|
||||
getPercent(55, screen.widthPixels),
|
||||
getPercent(50, screen.heightPixels),
|
||||
getPercent(40, screen.widthPixels),
|
||||
getPercent(50, screen.heightPixels)
|
||||
);
|
||||
|
||||
controller.addElement(createDigitalButton(
|
||||
ControllerPacket.BACK_FLAG, 0, 2, "BACK", -1, controller, context),
|
||||
getPercent(40, screen.widthPixels),
|
||||
getPercent(90, screen.heightPixels),
|
||||
getPercent(10, screen.widthPixels),
|
||||
getPercent(10, screen.heightPixels)
|
||||
);
|
||||
|
||||
controller.addElement(createDigitalButton(
|
||||
ControllerPacket.PLAY_FLAG, 0, 3, "START", -1, controller, context),
|
||||
getPercent(40, screen.widthPixels)+getPercent(10, screen.widthPixels),
|
||||
getPercent(90, screen.heightPixels),
|
||||
getPercent(10, screen.widthPixels),
|
||||
getPercent(10, screen.heightPixels)
|
||||
);
|
||||
}
|
||||
|
||||
/*
|
||||
NOT IMPLEMENTED YET,
|
||||
this should later be used to store and load a profile for the virtual controller
|
||||
public static void saveProfile(final String name,
|
||||
final VirtualController controller,
|
||||
final Context context) {
|
||||
|
||||
SharedPreferences preferences = context.getSharedPreferences(PROFILE_PATH + "/" +
|
||||
name, Activity.MODE_PRIVATE);
|
||||
|
||||
JSONArray elementConfigurations = new JSONArray();
|
||||
for (VirtualControllerElement element : controller.getElements()) {
|
||||
JSONObject elementConfiguration = new JSONObject();
|
||||
try {
|
||||
elementConfiguration.put("TYPE", element.getClass().getName());
|
||||
elementConfiguration.put("CONFIGURATION", element.getConfiguration());
|
||||
elementConfigurations.put(elementConfiguration);
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
SharedPreferences.Editor editor= preferences.edit();
|
||||
editor.putString("ELEMENTS", elementConfigurations.toString());
|
||||
}
|
||||
|
||||
public static void loadFromPreferences(final VirtualController controller) {
|
||||
|
||||
}
|
||||
*/
|
||||
}
|
||||
+290
@@ -0,0 +1,290 @@
|
||||
/**
|
||||
* Created by Karim Mreisi.
|
||||
*/
|
||||
|
||||
package com.limelight.binding.input.virtual_controller;
|
||||
|
||||
import android.app.AlertDialog;
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Paint;
|
||||
import android.util.DisplayMetrics;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
import android.widget.RelativeLayout;
|
||||
|
||||
public abstract class VirtualControllerElement extends View {
|
||||
protected static boolean _PRINT_DEBUG_INFORMATION = false;
|
||||
|
||||
protected VirtualController virtualController;
|
||||
|
||||
private final Paint paint = new Paint();
|
||||
|
||||
private int normalColor = 0xF0888888;
|
||||
protected int pressedColor = 0xF00000FF;
|
||||
private int configNormalColor = 0xF0FF0000;
|
||||
private int configSelectedColor = 0xF000FF00;
|
||||
|
||||
protected int startSize_x;
|
||||
protected int startSize_y;
|
||||
|
||||
float position_pressed_x = 0;
|
||||
float position_pressed_y = 0;
|
||||
|
||||
private enum Mode {
|
||||
Normal,
|
||||
Resize,
|
||||
Move
|
||||
}
|
||||
|
||||
private Mode currentMode = Mode.Normal;
|
||||
|
||||
protected VirtualControllerElement(VirtualController controller, Context context) {
|
||||
super(context);
|
||||
|
||||
this.virtualController = controller;
|
||||
}
|
||||
|
||||
protected void moveElement(int pressed_x, int pressed_y, int x, int y) {
|
||||
int newPos_x = (int) getX() + x - pressed_x;
|
||||
int newPos_y = (int) getY() + y - pressed_y;
|
||||
|
||||
RelativeLayout.LayoutParams layoutParams = (RelativeLayout.LayoutParams) getLayoutParams();
|
||||
|
||||
layoutParams.leftMargin = newPos_x > 0 ? newPos_x : 0;
|
||||
layoutParams.topMargin = newPos_y > 0 ? newPos_y : 0;
|
||||
layoutParams.rightMargin = 0;
|
||||
layoutParams.bottomMargin = 0;
|
||||
|
||||
requestLayout();
|
||||
}
|
||||
|
||||
protected void resizeElement(int pressed_x, int pressed_y, int width, int height) {
|
||||
RelativeLayout.LayoutParams layoutParams = (RelativeLayout.LayoutParams) getLayoutParams();
|
||||
|
||||
int newHeight = height + (startSize_y - pressed_y);
|
||||
int newWidth = width + (startSize_x - pressed_x);
|
||||
|
||||
layoutParams.height = newHeight > 20 ? newHeight : 20;
|
||||
layoutParams.width = newWidth > 20 ? newWidth : 20;
|
||||
|
||||
requestLayout();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDraw(Canvas canvas) {
|
||||
onElementDraw(canvas);
|
||||
|
||||
if (currentMode != Mode.Normal) {
|
||||
paint.setColor(configSelectedColor);
|
||||
paint.setStrokeWidth(getDefaultStrokeWidth());
|
||||
paint.setStyle(Paint.Style.STROKE);
|
||||
|
||||
canvas.drawRect(paint.getStrokeWidth(), paint.getStrokeWidth(),
|
||||
getWidth()-paint.getStrokeWidth(), getHeight()-paint.getStrokeWidth(),
|
||||
paint);
|
||||
}
|
||||
|
||||
super.onDraw(canvas);
|
||||
}
|
||||
|
||||
/*
|
||||
protected void actionShowNormalColorChooser() {
|
||||
AmbilWarnaDialog colorDialog = new AmbilWarnaDialog(getContext(), normalColor, true, new AmbilWarnaDialog.OnAmbilWarnaListener() {
|
||||
@Override
|
||||
public void onCancel(AmbilWarnaDialog dialog)
|
||||
{}
|
||||
|
||||
@Override
|
||||
public void onOk(AmbilWarnaDialog dialog, int color) {
|
||||
normalColor = color;
|
||||
invalidate();
|
||||
}
|
||||
});
|
||||
colorDialog.show();
|
||||
}
|
||||
|
||||
protected void actionShowPressedColorChooser() {
|
||||
AmbilWarnaDialog colorDialog = new AmbilWarnaDialog(getContext(), normalColor, true, new AmbilWarnaDialog.OnAmbilWarnaListener() {
|
||||
@Override
|
||||
public void onCancel(AmbilWarnaDialog dialog) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onOk(AmbilWarnaDialog dialog, int color) {
|
||||
pressedColor = color;
|
||||
invalidate();
|
||||
}
|
||||
});
|
||||
colorDialog.show();
|
||||
}
|
||||
*/
|
||||
|
||||
protected void actionEnableMove() {
|
||||
currentMode = Mode.Move;
|
||||
}
|
||||
|
||||
protected void actionEnableResize() {
|
||||
currentMode = Mode.Resize;
|
||||
}
|
||||
|
||||
protected void actionCancel() {
|
||||
currentMode = Mode.Normal;
|
||||
invalidate();
|
||||
}
|
||||
|
||||
protected int getDefaultColor() {
|
||||
return (virtualController.getControllerMode() == VirtualController.ControllerMode.Configuration) ?
|
||||
configNormalColor : normalColor;
|
||||
}
|
||||
|
||||
protected int getDefaultStrokeWidth() {
|
||||
DisplayMetrics screen = getResources().getDisplayMetrics();
|
||||
return (int)(screen.heightPixels*0.004f);
|
||||
}
|
||||
|
||||
protected void showConfigurationDialog() {
|
||||
try {
|
||||
AlertDialog.Builder alertBuilder = new AlertDialog.Builder(getContext());
|
||||
|
||||
alertBuilder.setTitle("Configuration");
|
||||
|
||||
CharSequence functions[] = new CharSequence[]{
|
||||
"Move",
|
||||
"Resize",
|
||||
/*election
|
||||
"Set n
|
||||
Disable color sormal color",
|
||||
"Set pressed color",
|
||||
*/
|
||||
"Cancel"
|
||||
};
|
||||
|
||||
alertBuilder.setItems(functions, new DialogInterface.OnClickListener() {
|
||||
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
switch (which) {
|
||||
case 0: { // move
|
||||
actionEnableMove();
|
||||
break;
|
||||
}
|
||||
case 1: { // resize
|
||||
actionEnableResize();
|
||||
break;
|
||||
}
|
||||
/*
|
||||
case 2: { // set default color
|
||||
actionShowNormalColorChooser();
|
||||
break;
|
||||
}
|
||||
case 3: { // set pressed color
|
||||
actionShowPressedColorChooser();
|
||||
break;
|
||||
}
|
||||
*/
|
||||
default: { // cancel
|
||||
actionCancel();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
AlertDialog alert = alertBuilder.create();
|
||||
// show menu
|
||||
alert.show();
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onTouchEvent(MotionEvent event) {
|
||||
if (virtualController.getControllerMode() == VirtualController.ControllerMode.Active) {
|
||||
return onElementTouchEvent(event);
|
||||
}
|
||||
|
||||
switch (event.getActionMasked()) {
|
||||
case MotionEvent.ACTION_DOWN:
|
||||
case MotionEvent.ACTION_POINTER_DOWN: {
|
||||
position_pressed_x = event.getX();
|
||||
position_pressed_y = event.getY();
|
||||
startSize_x = getWidth();
|
||||
startSize_y = getHeight();
|
||||
|
||||
actionEnableMove();
|
||||
|
||||
return true;
|
||||
}
|
||||
case MotionEvent.ACTION_MOVE: {
|
||||
switch (currentMode) {
|
||||
case Move: {
|
||||
moveElement(
|
||||
(int) position_pressed_x,
|
||||
(int) position_pressed_y,
|
||||
(int) event.getX(),
|
||||
(int) event.getY());
|
||||
break;
|
||||
}
|
||||
case Resize: {
|
||||
resizeElement(
|
||||
(int) position_pressed_x,
|
||||
(int) position_pressed_y,
|
||||
(int) event.getX(),
|
||||
(int) event.getY());
|
||||
break;
|
||||
}
|
||||
case Normal: {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
case MotionEvent.ACTION_CANCEL:
|
||||
case MotionEvent.ACTION_UP:
|
||||
case MotionEvent.ACTION_POINTER_UP: {
|
||||
actionCancel();
|
||||
return true;
|
||||
}
|
||||
default: {
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
abstract protected void onElementDraw(Canvas canvas);
|
||||
|
||||
abstract public boolean onElementTouchEvent(MotionEvent event);
|
||||
|
||||
protected static final void _DBG(String text) {
|
||||
if (_PRINT_DEBUG_INFORMATION) {
|
||||
System.out.println(text);
|
||||
}
|
||||
}
|
||||
|
||||
public void setColors(int normalColor, int pressedColor) {
|
||||
this.normalColor = normalColor;
|
||||
this.pressedColor = pressedColor;
|
||||
|
||||
invalidate();
|
||||
}
|
||||
|
||||
protected final float getPercent(float value, float percent) {
|
||||
return value / 100 * percent;
|
||||
}
|
||||
|
||||
protected final int getCorrectWidth() {
|
||||
return getWidth() > getHeight() ? getHeight() : getWidth();
|
||||
}
|
||||
|
||||
/**
|
||||
public JSONObject getConfiguration () {
|
||||
JSONObject configuration = new JSONObject();
|
||||
return configuration;
|
||||
}
|
||||
|
||||
public void loadConfiguration (JSONObject configuration) {
|
||||
}
|
||||
*/
|
||||
}
|
||||
@@ -109,6 +109,20 @@ public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer {
|
||||
else {
|
||||
LimeLog.info("No HEVC decoder found");
|
||||
}
|
||||
|
||||
// Set attributes that are queried in getCapabilities(). This must be done here
|
||||
// because getCapabilities() may be called before setup() in current versions of the common
|
||||
// library. The limitation of this is that we don't know whether we're using HEVC or AVC, so
|
||||
// we just assume AVC. This isn't really a problem because the capabilities are usually
|
||||
// shared between AVC and HEVC decoders on the same device.
|
||||
if (avcDecoderName != null) {
|
||||
directSubmit = MediaCodecHelper.decoderCanDirectSubmit(avcDecoderName);
|
||||
adaptivePlayback = MediaCodecHelper.decoderSupportsAdaptivePlayback(avcDecoderName);
|
||||
|
||||
if (directSubmit) {
|
||||
LimeLog.info("Decoder "+avcDecoderName+" will use direct submit");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -171,14 +185,6 @@ public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Set decoder-specific attributes
|
||||
directSubmit = MediaCodecHelper.decoderCanDirectSubmit(selectedDecoderName);
|
||||
adaptivePlayback = MediaCodecHelper.decoderSupportsAdaptivePlayback(selectedDecoderName);
|
||||
|
||||
if (directSubmit) {
|
||||
LimeLog.info("Decoder "+selectedDecoderName+" will use direct submit");
|
||||
}
|
||||
|
||||
// Codecs have been known to throw all sorts of crazy runtime exceptions
|
||||
// due to implementation problems
|
||||
try {
|
||||
|
||||
@@ -57,6 +57,11 @@ public class MediaCodecHelper {
|
||||
// Software decoders that don't support H264 high profile
|
||||
blacklistedDecoderPrefixes.add("omx.google");
|
||||
blacklistedDecoderPrefixes.add("AVCDecoder");
|
||||
|
||||
// Without bitstream fixups, we perform horribly on NVIDIA's HEVC
|
||||
// decoder. While not strictly necessary, I'm going to fully blacklist this
|
||||
// one to avoid users getting inaccurate impressions of Tegra X1/Moonlight performance.
|
||||
blacklistedDecoderPrefixes.add("OMX.Nvidia.h265.decode");
|
||||
}
|
||||
|
||||
static {
|
||||
@@ -76,14 +81,31 @@ public class MediaCodecHelper {
|
||||
|
||||
constrainedHighProfilePrefixes = new LinkedList<String>();
|
||||
constrainedHighProfilePrefixes.add("omx.intel");
|
||||
}
|
||||
|
||||
static {
|
||||
whitelistedHevcDecoders = new LinkedList<>();
|
||||
|
||||
// Exynos seems to be the only HEVC decoder that works reliably
|
||||
whitelistedHevcDecoders.add("omx.exynos");
|
||||
// whitelistedHevcDecoders.add("omx.nvidia"); TODO: This needs a similar fixup to the Tegra 3
|
||||
whitelistedHevcDecoders.add("omx.mtk");
|
||||
whitelistedHevcDecoders.add("omx.amlogic");
|
||||
whitelistedHevcDecoders.add("omx.rk");
|
||||
// omx.qcom added conditionally during initialization
|
||||
|
||||
// TODO: This needs a similar fixup to the Tegra 3 otherwise it buffers 16 frames
|
||||
//whitelistedHevcDecoders.add("omx.nvidia");
|
||||
|
||||
// Sony ATVs have broken MediaTek codecs (decoder hangs after rendering the first frame).
|
||||
// I know the Fire TV 2 works, so I'll just whitelist Amazon devices which seem
|
||||
// to actually be tested. Ugh...
|
||||
if (Build.MANUFACTURER.equalsIgnoreCase("Amazon")) {
|
||||
whitelistedHevcDecoders.add("omx.mtk");
|
||||
}
|
||||
|
||||
// These theoretically have good HEVC decoding capabilities (potentially better than
|
||||
// their AVC decoders), but haven't been tested enough
|
||||
//whitelistedHevcDecoders.add("omx.amlogic");
|
||||
//whitelistedHevcDecoders.add("omx.rk");
|
||||
|
||||
// Based on GPU attributes queried at runtime, the omx.qcom prefix will be added
|
||||
// during initialization to avoid SoCs with broken HEVC decoders.
|
||||
}
|
||||
|
||||
public static void initializeWithContext(Context context) {
|
||||
|
||||
@@ -53,7 +53,7 @@ public class ComputerDatabaseManager {
|
||||
}
|
||||
|
||||
public void deleteComputer(String name) {
|
||||
computerDb.delete(COMPUTER_TABLE_NAME, COMPUTER_NAME_COLUMN_NAME+"='"+name+"'", null);
|
||||
computerDb.delete(COMPUTER_TABLE_NAME, COMPUTER_NAME_COLUMN_NAME+"=?", new String[]{name});
|
||||
}
|
||||
|
||||
public boolean updateComputer(ComputerDetails details) {
|
||||
@@ -118,7 +118,7 @@ public class ComputerDatabaseManager {
|
||||
}
|
||||
|
||||
public ComputerDetails getComputerByName(String name) {
|
||||
Cursor c = computerDb.rawQuery("SELECT * FROM "+COMPUTER_TABLE_NAME+" WHERE "+COMPUTER_NAME_COLUMN_NAME+"='"+name+"'", null);
|
||||
Cursor c = computerDb.query(COMPUTER_TABLE_NAME, null, COMPUTER_NAME_COLUMN_NAME+"=?", new String[]{name}, null, null, null);
|
||||
ComputerDetails details = new ComputerDetails();
|
||||
if (!c.moveToFirst()) {
|
||||
// No matching computer
|
||||
|
||||
@@ -37,6 +37,7 @@ public class ComputerManagerService extends Service {
|
||||
private static final int MDNS_QUERY_PERIOD_MS = 1000;
|
||||
private static final int FAST_POLL_TIMEOUT = 500;
|
||||
private static final int OFFLINE_POLL_TRIES = 5;
|
||||
private static final int EMPTY_LIST_THRESHOLD = 3;
|
||||
|
||||
private final ComputerManagerBinder binder = new ComputerManagerBinder();
|
||||
|
||||
@@ -232,8 +233,10 @@ public class ComputerManagerService extends Service {
|
||||
|
||||
@Override
|
||||
public boolean onUnbind(Intent intent) {
|
||||
// Stop mDNS autodiscovery
|
||||
discoveryBinder.stopDiscovery();
|
||||
if (discoveryBinder != null) {
|
||||
// Stop mDNS autodiscovery
|
||||
discoveryBinder.stopDiscovery();
|
||||
}
|
||||
|
||||
// Stop polling
|
||||
pollingActive = false;
|
||||
@@ -661,6 +664,7 @@ public class ComputerManagerService extends Service {
|
||||
thread = new Thread() {
|
||||
@Override
|
||||
public void run() {
|
||||
int emptyAppListResponses = 0;
|
||||
do {
|
||||
InetAddress selectedAddr;
|
||||
|
||||
@@ -705,7 +709,15 @@ public class ComputerManagerService extends Service {
|
||||
}
|
||||
|
||||
List<NvApp> list = NvHTTP.getAppListByReader(new StringReader(appList));
|
||||
if (appList != null && !appList.isEmpty() && !list.isEmpty()) {
|
||||
if (list.isEmpty()) {
|
||||
LimeLog.warning("Empty app list received from "+computer.uuid);
|
||||
|
||||
// The app list might actually be empty, so if we get an empty response a few times
|
||||
// in a row, we'll go ahead and believe it.
|
||||
emptyAppListResponses++;
|
||||
}
|
||||
if (appList != null && !appList.isEmpty() &&
|
||||
(!list.isEmpty() || emptyAppListResponses >= EMPTY_LIST_THRESHOLD)) {
|
||||
// Open the cache file
|
||||
OutputStream cacheOut = null;
|
||||
try {
|
||||
@@ -721,6 +733,11 @@ public class ComputerManagerService extends Service {
|
||||
} catch (IOException ignored) {}
|
||||
}
|
||||
|
||||
// Reset empty count if it wasn't empty this time
|
||||
if (!list.isEmpty()) {
|
||||
emptyAppListResponses = 0;
|
||||
}
|
||||
|
||||
// Update the computer
|
||||
computer.rawAppList = appList;
|
||||
receivedAppList = true;
|
||||
@@ -731,8 +748,8 @@ public class ComputerManagerService extends Service {
|
||||
listener.notifyComputerUpdated(computer);
|
||||
}
|
||||
}
|
||||
else {
|
||||
LimeLog.warning("Empty app list received from "+computer.uuid);
|
||||
else if (appList == null || appList.isEmpty()) {
|
||||
LimeLog.warning("Null app list received from "+computer.uuid);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
|
||||
@@ -21,6 +21,7 @@ public class PreferenceConfiguration {
|
||||
private static final String ENABLE_51_SURROUND_PREF_STRING = "checkbox_51_surround";
|
||||
private static final String USB_DRIVER_PREF_SRING = "checkbox_usb_driver";
|
||||
private static final String VIDEO_FORMAT_PREF_STRING = "video_format";
|
||||
private static final String ONSCREEN_CONTROLLER_PREF_STRING = "checkbox_show_onscreen_controls";
|
||||
|
||||
private static final int BITRATE_DEFAULT_720_30 = 5;
|
||||
private static final int BITRATE_DEFAULT_720_60 = 10;
|
||||
@@ -42,6 +43,7 @@ public class PreferenceConfiguration {
|
||||
private static final boolean DEFAULT_ENABLE_51_SURROUND = false;
|
||||
private static final boolean DEFAULT_USB_DRIVER = true;
|
||||
private static final String DEFAULT_VIDEO_FORMAT = "auto";
|
||||
private static final boolean ONSCREEN_CONTROLLER_DEFAULT = false;
|
||||
|
||||
public static final int FORCE_H265_ON = -1;
|
||||
public static final int AUTOSELECT_H265 = 0;
|
||||
@@ -54,6 +56,7 @@ public class PreferenceConfiguration {
|
||||
public boolean stretchVideo, enableSops, playHostAudio, disableWarnings;
|
||||
public String language;
|
||||
public boolean listMode, smallIconMode, multiController, enable51Surround, usbDriver;
|
||||
public boolean onscreenController;
|
||||
|
||||
public static int getDefaultBitrate(String resFpsString) {
|
||||
if (resFpsString.equals("720p30")) {
|
||||
@@ -97,7 +100,7 @@ public class PreferenceConfiguration {
|
||||
}
|
||||
|
||||
// Use small mode on anything smaller than a 7" tablet
|
||||
return context.getResources().getConfiguration().smallestScreenWidthDp < 600;
|
||||
return context.getResources().getConfiguration().screenWidthDp < 600;
|
||||
}
|
||||
|
||||
public static int getDefaultBitrate(Context context) {
|
||||
@@ -183,6 +186,7 @@ public class PreferenceConfiguration {
|
||||
config.multiController = prefs.getBoolean(MULTI_CONTROLLER_PREF_STRING, DEFAULT_MULTI_CONTROLLER);
|
||||
config.enable51Surround = prefs.getBoolean(ENABLE_51_SURROUND_PREF_STRING, DEFAULT_ENABLE_51_SURROUND);
|
||||
config.usbDriver = prefs.getBoolean(USB_DRIVER_PREF_SRING, DEFAULT_USB_DRIVER);
|
||||
config.onscreenController = prefs.getBoolean(ONSCREEN_CONTROLLER_PREF_STRING, ONSCREEN_CONTROLLER_DEFAULT);
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
@@ -2,12 +2,15 @@ package com.limelight.preferences;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.content.res.Configuration;
|
||||
import android.os.Bundle;
|
||||
import android.app.Activity;
|
||||
import android.preference.Preference;
|
||||
import android.preference.PreferenceCategory;
|
||||
import android.preference.PreferenceFragment;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.preference.PreferenceScreen;
|
||||
|
||||
import com.limelight.PcView;
|
||||
import com.limelight.R;
|
||||
@@ -60,6 +63,15 @@ public class StreamSettings extends Activity {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
addPreferencesFromResource(R.xml.preferences);
|
||||
PreferenceScreen screen = getPreferenceScreen();
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
// Add a listener to the FPS and resolution preference
|
||||
// so the bitrate can be auto-adjusted
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
package com.limelight.ui;
|
||||
|
||||
import android.annotation.TargetApi;
|
||||
import android.content.Context;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.SurfaceView;
|
||||
|
||||
public class StreamView extends SurfaceView {
|
||||
private double desiredAspectRatio;
|
||||
|
||||
public void setDesiredAspectRatio(double aspectRatio) {
|
||||
this.desiredAspectRatio = aspectRatio;
|
||||
}
|
||||
|
||||
public StreamView(Context context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
public StreamView(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
}
|
||||
|
||||
public StreamView(Context context, AttributeSet attrs, int defStyleAttr) {
|
||||
super(context, attrs, defStyleAttr);
|
||||
}
|
||||
|
||||
@TargetApi(21)
|
||||
public StreamView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
|
||||
super(context, attrs, defStyleAttr, defStyleRes);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
|
||||
// If no fixed aspect ratio has been provided, simply use the default onMeasure() behavior
|
||||
if (desiredAspectRatio == 0) {
|
||||
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
|
||||
return;
|
||||
}
|
||||
|
||||
// Based on code from: https://www.buzzingandroid.com/2012/11/easy-measuring-of-custom-views-with-specific-aspect-ratio/
|
||||
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
|
||||
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
|
||||
|
||||
int measuredHeight, measuredWidth;
|
||||
if (widthSize > heightSize * desiredAspectRatio) {
|
||||
measuredHeight = heightSize;
|
||||
measuredWidth = (int)(measuredHeight * desiredAspectRatio);
|
||||
} else {
|
||||
measuredWidth = widthSize;
|
||||
measuredHeight = (int)(measuredWidth / desiredAspectRatio);
|
||||
}
|
||||
|
||||
setMeasuredDimension(measuredWidth, measuredHeight);
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@
|
||||
#include <string.h>
|
||||
|
||||
#include <sys/types.h>
|
||||
#include <sys/socket.h>
|
||||
#include <sys/stat.h>
|
||||
#include <fcntl.h>
|
||||
#include <linux/input.h>
|
||||
@@ -11,9 +12,13 @@
|
||||
#include <errno.h>
|
||||
#include <dirent.h>
|
||||
#include <pthread.h>
|
||||
#include <netinet/in.h>
|
||||
#include <netinet/tcp.h>
|
||||
|
||||
#include <android/log.h>
|
||||
|
||||
#define EVDEV_MAX_EVENT_SIZE 24
|
||||
|
||||
#define REL_X 0x00
|
||||
#define REL_Y 0x01
|
||||
#define KEY_Q 16
|
||||
@@ -30,9 +35,11 @@ struct DeviceEntry {
|
||||
static struct DeviceEntry *DeviceListHead;
|
||||
static int grabbing = 1;
|
||||
static pthread_mutex_t DeviceListLock = PTHREAD_MUTEX_INITIALIZER;
|
||||
static pthread_mutex_t SocketSendLock = PTHREAD_MUTEX_INITIALIZER;
|
||||
static int sock;
|
||||
|
||||
// This is a small executable that runs in a root shell. It reads input
|
||||
// devices and writes the evdev output packets to stdout. This allows
|
||||
// devices and writes the evdev output packets to a socket. This allows
|
||||
// Moonlight to read input devices without having to muck with changing
|
||||
// device permissions or modifying SELinux policy (which is prevented in
|
||||
// Marshmallow anyway).
|
||||
@@ -56,20 +63,23 @@ static int hasKey(int fd, short key) {
|
||||
}
|
||||
|
||||
static void outputEvdevData(char *data, int dataSize) {
|
||||
// We need to lock stdout before writing to prevent
|
||||
// interleaving of data between threads.
|
||||
flockfile(stdout);
|
||||
fwrite(&dataSize, sizeof(dataSize), 1, stdout);
|
||||
fwrite(data, dataSize, 1, stdout);
|
||||
fflush(stdout);
|
||||
funlockfile(stdout);
|
||||
char packetBuffer[EVDEV_MAX_EVENT_SIZE + sizeof(dataSize)];
|
||||
|
||||
// Copy the full packet into our buffer
|
||||
memcpy(packetBuffer, &dataSize, sizeof(dataSize));
|
||||
memcpy(&packetBuffer[sizeof(dataSize)], data, dataSize);
|
||||
|
||||
// Lock to prevent other threads from sending at the same time
|
||||
pthread_mutex_lock(&SocketSendLock);
|
||||
send(sock, packetBuffer, dataSize + sizeof(dataSize), 0);
|
||||
pthread_mutex_unlock(&SocketSendLock);
|
||||
}
|
||||
|
||||
void* pollThreadFunc(void* context) {
|
||||
struct DeviceEntry *device = context;
|
||||
struct pollfd pollinfo;
|
||||
int pollres, ret;
|
||||
char data[64];
|
||||
char data[EVDEV_MAX_EVENT_SIZE];
|
||||
|
||||
__android_log_print(ANDROID_LOG_INFO, "EvdevReader", "Polling /dev/input/%s", device->devName);
|
||||
|
||||
@@ -94,7 +104,7 @@ void* pollThreadFunc(void* context) {
|
||||
|
||||
if (pollres > 0 && (pollinfo.revents & POLLIN)) {
|
||||
// We'll have data available now
|
||||
ret = read(device->fd, data, sizeof(struct input_event));
|
||||
ret = read(device->fd, data, EVDEV_MAX_EVENT_SIZE);
|
||||
if (ret < 0) {
|
||||
__android_log_print(ANDROID_LOG_ERROR, "EvdevReader",
|
||||
"read() failed: %d", errno);
|
||||
@@ -132,6 +142,9 @@ cleanup:
|
||||
{
|
||||
struct DeviceEntry *lastEntry;
|
||||
|
||||
// Lock the device list
|
||||
pthread_mutex_lock(&DeviceListLock);
|
||||
|
||||
if (DeviceListHead == device) {
|
||||
DeviceListHead = device->next;
|
||||
}
|
||||
@@ -146,6 +159,9 @@ cleanup:
|
||||
lastEntry = lastEntry->next;
|
||||
}
|
||||
}
|
||||
|
||||
// Unlock device list
|
||||
pthread_mutex_unlock(&DeviceListLock);
|
||||
}
|
||||
|
||||
// Free the context
|
||||
@@ -254,6 +270,11 @@ static int enumerateDevices(void) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (strstr(dirEnt->d_name, "event") == NULL) {
|
||||
// Skip non-event devices
|
||||
continue;
|
||||
}
|
||||
|
||||
startPollForDevice(dirEnt->d_name);
|
||||
}
|
||||
|
||||
@@ -261,6 +282,39 @@ static int enumerateDevices(void) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int connectSocket(int port) {
|
||||
struct sockaddr_in saddr;
|
||||
int ret;
|
||||
int val;
|
||||
|
||||
sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
|
||||
if (sock < 0) {
|
||||
__android_log_print(ANDROID_LOG_ERROR, "EvdevReader", "socket() failed: %d", errno);
|
||||
return -1;
|
||||
}
|
||||
|
||||
memset(&saddr, 0, sizeof(saddr));
|
||||
saddr.sin_family = AF_INET;
|
||||
saddr.sin_port = htons(port);
|
||||
saddr.sin_addr.s_addr = inet_addr("127.0.0.1");
|
||||
ret = connect(sock, (struct sockaddr*)&saddr, sizeof(saddr));
|
||||
if (ret < 0) {
|
||||
__android_log_print(ANDROID_LOG_ERROR, "EvdevReader", "connect() failed: %d", errno);
|
||||
return -1;
|
||||
}
|
||||
|
||||
val = 1;
|
||||
ret = setsockopt(sock, IPPROTO_TCP, TCP_NODELAY, (char*)&val, sizeof(val));
|
||||
if (ret < 0) {
|
||||
__android_log_print(ANDROID_LOG_ERROR, "EvdevReader", "setsockopt() failed: %d", errno);
|
||||
// We can continue anyways
|
||||
}
|
||||
|
||||
__android_log_print(ANDROID_LOG_INFO, "EvdevReader", "Connection established to port %d", port);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
#define UNGRAB_REQ 1
|
||||
#define REGRAB_REQ 2
|
||||
|
||||
@@ -268,6 +322,18 @@ int main(int argc, char* argv[]) {
|
||||
int ret;
|
||||
int pollres;
|
||||
struct pollfd pollinfo;
|
||||
int port;
|
||||
|
||||
__android_log_print(ANDROID_LOG_INFO, "EvdevReader", "Entered main()");
|
||||
|
||||
port = atoi(argv[1]);
|
||||
__android_log_print(ANDROID_LOG_INFO, "EvdevReader", "Requested port number: %d", port);
|
||||
|
||||
// Connect to the app's socket
|
||||
ret = connectSocket(port);
|
||||
if (ret < 0) {
|
||||
return ret;
|
||||
}
|
||||
|
||||
// Perform initial enumeration
|
||||
ret = enumerateDevices();
|
||||
@@ -282,7 +348,7 @@ int main(int argc, char* argv[]) {
|
||||
do {
|
||||
// Every second we poll again for new devices if
|
||||
// we haven't received any new events
|
||||
pollinfo.fd = STDIN_FILENO;
|
||||
pollinfo.fd = sock;
|
||||
pollinfo.events = POLLIN;
|
||||
pollinfo.revents = 0;
|
||||
pollres = poll(&pollinfo, 1, 1000);
|
||||
@@ -293,33 +359,52 @@ int main(int argc, char* argv[]) {
|
||||
}
|
||||
while (pollres == 0);
|
||||
|
||||
ret = fread(&requestId, sizeof(requestId), 1, stdin);
|
||||
if (ret < sizeof(requestId)) {
|
||||
__android_log_print(ANDROID_LOG_ERROR, "EvdevReader", "Short read on input");
|
||||
return errno;
|
||||
}
|
||||
|
||||
if (requestId != UNGRAB_REQ && requestId != REGRAB_REQ) {
|
||||
__android_log_print(ANDROID_LOG_ERROR, "EvdevReader", "Unknown request");
|
||||
return requestId;
|
||||
}
|
||||
|
||||
{
|
||||
struct DeviceEntry *currentEntry;
|
||||
|
||||
pthread_mutex_lock(&DeviceListLock);
|
||||
|
||||
// Update state for future devices
|
||||
grabbing = (requestId == REGRAB_REQ);
|
||||
|
||||
// Carry out the requested action on each device
|
||||
currentEntry = DeviceListHead;
|
||||
while (currentEntry != NULL) {
|
||||
ioctl(currentEntry->fd, EVIOCGRAB, grabbing);
|
||||
currentEntry = currentEntry->next;
|
||||
if (pollres > 0 && (pollinfo.revents & POLLIN)) {
|
||||
// We'll have data available now
|
||||
ret = recv(sock, &requestId, sizeof(requestId), 0);
|
||||
if (ret < sizeof(requestId)) {
|
||||
__android_log_print(ANDROID_LOG_ERROR, "EvdevReader", "Short read on socket");
|
||||
return errno;
|
||||
}
|
||||
|
||||
pthread_mutex_unlock(&DeviceListLock);
|
||||
if (requestId != UNGRAB_REQ && requestId != REGRAB_REQ) {
|
||||
__android_log_print(ANDROID_LOG_ERROR, "EvdevReader", "Unknown request");
|
||||
return requestId;
|
||||
}
|
||||
|
||||
{
|
||||
struct DeviceEntry *currentEntry;
|
||||
|
||||
pthread_mutex_lock(&DeviceListLock);
|
||||
|
||||
// Update state for future devices
|
||||
grabbing = (requestId == REGRAB_REQ);
|
||||
|
||||
// Carry out the requested action on each device
|
||||
currentEntry = DeviceListHead;
|
||||
while (currentEntry != NULL) {
|
||||
ioctl(currentEntry->fd, EVIOCGRAB, grabbing);
|
||||
currentEntry = currentEntry->next;
|
||||
}
|
||||
|
||||
pthread_mutex_unlock(&DeviceListLock);
|
||||
|
||||
__android_log_print(ANDROID_LOG_INFO, "EvdevReader", "New grab status is: %s",
|
||||
grabbing ? "enabled" : "disabled");
|
||||
}
|
||||
}
|
||||
else {
|
||||
// Terminate this thread
|
||||
if (pollres < 0) {
|
||||
__android_log_print(ANDROID_LOG_ERROR, "EvdevReader",
|
||||
"Socket recv poll() failed: %d", errno);
|
||||
}
|
||||
else {
|
||||
__android_log_print(ANDROID_LOG_ERROR, "EvdevReader",
|
||||
"Socket poll unexpected revents: %d", pollinfo.revents);
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
# Android.mk for Moonlight's ENet JNI binding
|
||||
MY_LOCAL_PATH := $(call my-dir)
|
||||
|
||||
include $(call all-subdir-makefiles)
|
||||
|
||||
LOCAL_PATH := $(MY_LOCAL_PATH)
|
||||
|
||||
include $(CLEAR_VARS)
|
||||
LOCAL_MODULE := jnienet
|
||||
|
||||
LOCAL_SRC_FILES := jnienet.c \
|
||||
enet/callbacks.c \
|
||||
enet/compress.c \
|
||||
enet/host.c \
|
||||
enet/list.c \
|
||||
enet/packet.c \
|
||||
enet/peer.c \
|
||||
enet/protocol.c \
|
||||
enet/unix.c \
|
||||
enet/win32.c \
|
||||
|
||||
LOCAL_CFLAGS := -DHAS_SOCKLEN_T=1
|
||||
LOCAL_C_INCLUDES := $(LOCAL_PATH)/enet/include
|
||||
|
||||
include $(BUILD_SHARED_LIBRARY)
|
||||
Submodule
+1
Submodule app/src/main/jni/jnienet/enet added at 7546b505c1
@@ -0,0 +1,148 @@
|
||||
#include "enet/enet.h"
|
||||
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#include <jni.h>
|
||||
|
||||
#define CLIENT_TO_LONG(x) ((intptr_t)(x))
|
||||
#define LONG_TO_CLIENT(x) ((ENetHost*)(intptr_t)(x))
|
||||
|
||||
#define PEER_TO_LONG(x) ((intptr_t)(x))
|
||||
#define LONG_TO_PEER(x) ((ENetPeer*)(intptr_t)(x))
|
||||
|
||||
JNIEXPORT jint JNICALL
|
||||
Java_com_limelight_nvstream_enet_EnetConnection_initializeEnet(JNIEnv *env, jobject class) {
|
||||
return enet_initialize();
|
||||
}
|
||||
|
||||
JNIEXPORT jlong JNICALL
|
||||
Java_com_limelight_nvstream_enet_EnetConnection_createClient(JNIEnv *env, jobject class, jstring address) {
|
||||
ENetAddress enetAddress;
|
||||
const char *addrStr;
|
||||
int err;
|
||||
|
||||
// Perform a lookup on the address to determine the address family
|
||||
addrStr = (*env)->GetStringUTFChars(env, address, 0);
|
||||
err = enet_address_set_host(&enetAddress, addrStr);
|
||||
(*env)->ReleaseStringUTFChars(env, address, addrStr);
|
||||
if (err < 0) {
|
||||
return CLIENT_TO_LONG(NULL);
|
||||
}
|
||||
|
||||
// Create a client that can use 1 outgoing connection and 1 channel
|
||||
return CLIENT_TO_LONG(enet_host_create(enetAddress.address.ss_family, NULL, 1, 1, 0, 0));
|
||||
}
|
||||
|
||||
JNIEXPORT jlong JNICALL
|
||||
Java_com_limelight_nvstream_enet_EnetConnection_connectToPeer(JNIEnv *env, jobject class, jlong client, jstring address, jint port, jint timeout) {
|
||||
ENetPeer* peer;
|
||||
ENetAddress enetAddress;
|
||||
ENetEvent event;
|
||||
const char *addrStr;
|
||||
int err;
|
||||
|
||||
// Initialize the ENet address
|
||||
addrStr = (*env)->GetStringUTFChars(env, address, 0);
|
||||
err = enet_address_set_host(&enetAddress, addrStr);
|
||||
enet_address_set_port(&enetAddress, port);
|
||||
(*env)->ReleaseStringUTFChars(env, address, addrStr);
|
||||
if (err < 0) {
|
||||
return PEER_TO_LONG(NULL);
|
||||
}
|
||||
|
||||
// Start the connection
|
||||
peer = enet_host_connect(LONG_TO_CLIENT(client), &enetAddress, 1, 0);
|
||||
if (peer == NULL) {
|
||||
return PEER_TO_LONG(NULL);
|
||||
}
|
||||
|
||||
// Wait for the connect to complete
|
||||
if (enet_host_service(LONG_TO_CLIENT(client), &event, timeout) <= 0 || event.type != ENET_EVENT_TYPE_CONNECT) {
|
||||
enet_peer_reset(peer);
|
||||
return PEER_TO_LONG(NULL);
|
||||
}
|
||||
|
||||
// Ensure the connect verify ACK is sent immediately
|
||||
enet_host_flush(LONG_TO_CLIENT(client));
|
||||
|
||||
// Set the max peer timeout to 10 seconds
|
||||
enet_peer_timeout(peer, ENET_PEER_TIMEOUT_LIMIT, ENET_PEER_TIMEOUT_MINIMUM, 10000);
|
||||
|
||||
return PEER_TO_LONG(peer);
|
||||
}
|
||||
|
||||
JNIEXPORT jint JNICALL
|
||||
Java_com_limelight_nvstream_enet_EnetConnection_readPacket(JNIEnv *env, jobject class, jlong client, jbyteArray data, jint length, jint timeout) {
|
||||
jint err;
|
||||
jbyte* dataPtr;
|
||||
ENetEvent event;
|
||||
|
||||
// Wait for a receive event, timeout, or disconnect
|
||||
err = enet_host_service(LONG_TO_CLIENT(client), &event, timeout);
|
||||
if (err <= 0) {
|
||||
return err;
|
||||
}
|
||||
else if (event.type != ENET_EVENT_TYPE_RECEIVE) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Check that the packet isn't too large
|
||||
if (event.packet->dataLength > length) {
|
||||
enet_packet_destroy(event.packet);
|
||||
return event.packet->dataLength;
|
||||
}
|
||||
|
||||
// Copy the packet data into the caller's buffer
|
||||
dataPtr = (*env)->GetByteArrayElements(env, data, 0);
|
||||
memcpy(dataPtr, event.packet->data, event.packet->dataLength);
|
||||
err = event.packet->dataLength;
|
||||
(*env)->ReleaseByteArrayElements(env, data, dataPtr, 0);
|
||||
|
||||
// Free the packet
|
||||
enet_packet_destroy(event.packet);
|
||||
|
||||
return err;
|
||||
}
|
||||
|
||||
JNIEXPORT jboolean JNICALL
|
||||
Java_com_limelight_nvstream_enet_EnetConnection_writePacket(JNIEnv *env, jobject class, jlong client, jlong peer, jbyteArray data, jint length, jint packetFlags) {
|
||||
ENetPacket* packet;
|
||||
jboolean ret;
|
||||
jbyte* dataPtr;
|
||||
|
||||
dataPtr = (*env)->GetByteArrayElements(env, data, 0);
|
||||
|
||||
// Create the reliable packet that describes our outgoing message
|
||||
packet = enet_packet_create(dataPtr, length, packetFlags);
|
||||
if (packet != NULL) {
|
||||
// Send the message to the peer
|
||||
if (enet_peer_send(LONG_TO_PEER(peer), 0, packet) < 0) {
|
||||
// This can fail if the peer has been disconnected
|
||||
enet_packet_destroy(packet);
|
||||
ret = JNI_FALSE;
|
||||
}
|
||||
else {
|
||||
// Force the client to send the packet now
|
||||
enet_host_flush(LONG_TO_CLIENT(client));
|
||||
ret = JNI_TRUE;
|
||||
}
|
||||
}
|
||||
else {
|
||||
ret = JNI_FALSE;
|
||||
}
|
||||
|
||||
(*env)->ReleaseByteArrayElements(env, data, dataPtr, JNI_ABORT);
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
JNIEXPORT void JNICALL
|
||||
Java_com_limelight_nvstream_enet_EnetConnection_destroyClient(JNIEnv *env, jobject class, jlong client) {
|
||||
enet_host_destroy(LONG_TO_CLIENT(client));
|
||||
}
|
||||
|
||||
JNIEXPORT void JNICALL
|
||||
Java_com_limelight_nvstream_enet_EnetConnection_disconnectPeer(JNIEnv *env, jobject class, jlong peer) {
|
||||
enet_peer_disconnect_now(LONG_TO_PEER(peer), 0);
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:id="@+id/configure_virtual_controller_frameLayout"
|
||||
>
|
||||
|
||||
</FrameLayout>
|
||||
@@ -4,7 +4,7 @@
|
||||
android:layout_height="match_parent"
|
||||
tools:context=".Game" >
|
||||
|
||||
<SurfaceView
|
||||
<com.limelight.ui.StreamView
|
||||
android:id="@+id/surfaceView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
|
||||
@@ -103,8 +103,12 @@
|
||||
<string name="summary_checkbox_multi_controller">When unchecked, all controllers appear as one</string>
|
||||
<string name="title_seekbar_deadzone">Adjust analog stick deadzone</string>
|
||||
<string name="suffix_seekbar_deadzone">%</string>
|
||||
<string name="title_checkbox_xb1_driver">Xbox One controller driver</string>
|
||||
<string name="summary_checkbox_xb1_driver">Enables a built-in USB driver for devices without native Xbox One controller support.</string>
|
||||
<string name="title_checkbox_xb1_driver">Xbox 360/One controller driver</string>
|
||||
<string name="summary_checkbox_xb1_driver">Enables a built-in USB driver for devices without native Xbox controller support.</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>
|
||||
<string name="summary_checkbox_show_onscreen_controls">Show virtual controller overlay on touchscreen</string>
|
||||
|
||||
<string name="category_ui_settings">UI Settings</string>
|
||||
<string name="title_language_list">Language</string>
|
||||
|
||||
@@ -51,6 +51,14 @@
|
||||
android:summary="@string/summary_checkbox_xb1_driver"
|
||||
android:defaultValue="true" />
|
||||
</PreferenceCategory>
|
||||
<PreferenceCategory android:title="@string/category_on_screen_controls_settings"
|
||||
android:key="category_onscreen_controls">
|
||||
<CheckBoxPreference
|
||||
android:key="checkbox_show_onscreen_controls"
|
||||
android:title="@string/title_checkbox_show_onscreen_controls"
|
||||
android:summary="@string/summary_checkbox_show_onscreen_controls"
|
||||
android:defaultValue="false"/>
|
||||
</PreferenceCategory>
|
||||
<PreferenceCategory android:title="@string/category_host_settings">
|
||||
<CheckBoxPreference
|
||||
android:key="checkbox_enable_sops"
|
||||
@@ -90,5 +98,4 @@
|
||||
android:summary="@string/summary_video_format"
|
||||
android:defaultValue="auto" />
|
||||
</PreferenceCategory>
|
||||
|
||||
</PreferenceScreen>
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ buildscript {
|
||||
jcenter()
|
||||
}
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:1.3.0'
|
||||
classpath 'com.android.tools.build:gradle:2.1.0'
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
#Sun Dec 07 22:52:07 PST 2014
|
||||
#Sat Feb 06 16:21:20 EST 2016
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-2.2.1-all.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-2.10-all.zip
|
||||
|
||||
Reference in New Issue
Block a user