Compare commits

..

165 Commits

Author SHA1 Message Date
Cameron Gutman 32af2d0831 Increment version code 2016-06-26 14:05:38 -07:00
Cameron Gutman 242b03d4b5 Add gradle.properties for Dex In-Process 2016-06-20 21:33:39 -07:00
Cameron Gutman 87a62666ac Prefer Shield capture provider over Android N 2016-06-20 20:43:59 -07:00
Cameron Gutman 2dcf5486da Revert "Display the running app first on the app grid"
This reverts commit 36f8cc02cb.
2016-06-20 20:30:47 -07:00
Cameron Gutman 60d3d8b3ae Version to 4.6 2016-06-18 15:17:15 -07:00
Cameron Gutman e9141d65fe Improve reliability of missing root detection 2016-06-18 14:54:53 -07:00
Cameron Gutman aae591daec Improve multi-window experience on N 2016-06-18 14:52:20 -07:00
Cameron Gutman a5ca8a7472 Add a hack to avoid crashing when the app window divider is dragged off of the screen on N multi-window 2016-06-18 14:40:42 -07:00
Cameron Gutman 36f8cc02cb Display the running app first on the app grid 2016-06-18 13:38:37 -07:00
Cameron Gutman 55b9645651 Fix minor Lint issues 2016-06-18 12:38:43 -07:00
Cameron Gutman d30ecbed5b Update gradle 2016-06-18 11:40:55 -07:00
Cameron Gutman 0bbd27f04c Update common jar 2016-06-18 11:37:00 -07:00
Cameron Gutman 3c53fb7403 Update target SDK to 24 2016-06-18 11:19:04 -07:00
Cameron Gutman 7a81950819 Enable sustained performance mode on N+ when streaming 2016-06-18 11:17:34 -07:00
Cameron Gutman 74f212c702 Add Android N mouse capture support 2016-06-18 11:15:53 -07:00
Cameron Gutman 36be943854 Add support for more Xbox controller models 2016-06-13 22:28:48 -05:00
Cameron Gutman 26a4fc75a5 Add handling for the ADT-1 controller 2016-06-13 21:28:54 -05:00
Cameron Gutman a5ec5fc265 Select the optimal display mode before streaming 2016-06-13 21:23:00 -05:00
Cameron Gutman 541ac44be4 Add an unified input capture interface 2016-06-13 20:33:43 -05:00
Cameron Gutman 117b555fcd Fix wiki link 2016-05-30 12:22:54 -05:00
Cameron Gutman a10cd04441 Clarify wording in H.265 settings 2016-05-29 16:39:20 -05:00
Cameron Gutman 53dccbde2a Repeat key down events are needed for proper key repeating 2016-05-29 15:52:18 -05:00
Cameron Gutman 56625dfe4b Bump version to 4.5.10 2016-05-21 18:28:02 -05:00
Cameron Gutman 2eab5a3b7b Update the ENet submodule to include the MTU fix for LTE streaming 2016-05-21 18:09:40 -05:00
Cameron Gutman f9e811862a Bump version to 4.5.9 2016-05-19 22:39:50 -04:00
Cameron Gutman 25ccc3d0e1 Fix for Xiaomi gamepad mapping 2016-05-19 22:31:14 -04:00
Cameron Gutman 8853bf0670 Bump version to 4.5.8 2016-05-07 21:25:11 -04:00
Cameron Gutman 71fa3a824b Update gradle 2016-05-07 21:20:05 -04:00
Cameron Gutman 56fd50834c Update common jar with the RTP queue changes 2016-05-07 21:19:54 -04:00
Cameron Gutman 48ba812cf6 When combining analog inputs, use the one with the highest magnitude 2016-05-07 21:19:02 -04:00
Cameron Gutman 019dc6d45f Display a warning at stream start if root access is unavailable 2016-05-07 20:59:17 -04:00
Cameron Gutman cbcb784a79 Blacklist Tegra X1's HEVC decoder until the correct SPS fixups are in place 2016-05-07 20:53:45 -04:00
Cameron Gutman 39fa0258ad Force the Archos Gamepad 2's controller buttons as controller 0 2016-04-23 22:23:43 -04:00
Cameron Gutman d0dd5bfa8c Combine all controllers with the same controller number before sending controller input 2016-04-23 22:23:01 -04:00
Cameron Gutman b948c47618 Increment patch level again 2016-04-22 00:15:21 -04:00
Cameron Gutman 18cae8ac53 Use common jar from the android branch (da297b5a89c2b645573f231af3e47752f27fbc79) to fix API 19 issues 2016-04-21 13:33:08 -04:00
Cameron Gutman 0576231dfc Update patch level to 4.5.7.1 2016-04-20 13:52:53 -04:00
Cameron Gutman 6ad35a83dd Update common jar with fix for < API 19 2016-04-20 13:42:57 -04:00
Cameron Gutman 33d4dfc745 Revert "Prevent the small-mode default from changing between portrait and landscape orientations"
This reverts commit 7c1eb80d62.
2016-04-20 13:34:24 -04:00
Cameron Gutman f3bf63a668 Increment app version 2016-04-19 20:49:40 -04:00
Cameron Gutman 2dbb7395a4 Restart the app view activity when configuration changes are made that could require the grid to be resized. This is much simpler than handling all of the fine edge cases here. 2016-04-19 20:38:05 -04:00
Cameron Gutman 7c1eb80d62 Prevent the small-mode default from changing between portrait and landscape orientations 2016-04-19 20:36:04 -04:00
Cameron Gutman f2bf093691 Update Gradle 2016-04-19 19:51:00 -04:00
Cameron Gutman 2f002bfa4a Fix being stuck in small-icon mode after resizing to minimum size on Android N 2016-04-19 19:36:10 -04:00
Cameron Gutman 4a19038d54 Update common jar to fix crashes in jnienet 2016-04-19 19:18:13 -04:00
Cameron Gutman 15fb3dd92c Fix mouse scaling to scale by stream view size rather than screen size for better behavior on N and in general 2016-04-19 19:13:57 -04:00
Cameron Gutman e0982d3961 Fix video stream aspect ratio scaling in multi-window mode on Android N 2016-04-19 18:40:45 -04:00
Cameron Gutman 7fb2f15f54 Re-release of 4.5.6 with fixed Gen 4 streaming 2016-03-29 23:37:03 -04:00
Cameron Gutman f93dbb4116 Update common jar again to fix streaming on Gen 4 and earlier 2016-03-29 23:34:31 -04:00
Cameron Gutman bc34fe3a9f Increment version to 4.5.6 2016-03-29 20:35:54 -04:00
Cameron Gutman bbe49491c1 Update common jar to support GFE 2.11.2.46+ 2016-03-29 20:17:35 -04:00
Cameron Gutman d5ccb80f26 Update to new Gradle for Android Studio 2.1 2016-03-29 20:15:45 -04:00
Cameron Gutman 50fd15379a Fix JNI compilation warnings 2016-03-10 15:28:42 -08:00
Cameron Gutman ed479f1155 Increment version to 4.5.5 2016-03-08 13:10:04 -08:00
Cameron Gutman 04db9ba714 Update common to fix RTSP handshake timeouts with ENet 2016-03-08 13:07:33 -08:00
Cameron Gutman 6a973e3248 Update version code for 4.5.4 r2 2016-03-07 15:01:58 -08:00
Cameron Gutman 96d9e4977b Update to ENet API to support IPv6 2016-03-07 14:37:01 -08:00
Cameron Gutman 5a3897f22a Update common jar to fix some ENet crashes 2016-03-07 13:16:55 -08:00
Cameron Gutman ceef00b79a Fail writePacket if enet_peer_send returns -1 2016-03-07 12:54:06 -08:00
Cameron Gutman 94ee24ea11 Update to 4.5.4 2016-03-06 21:52:54 -08:00
Cameron Gutman 1a201f2e94 Update gradle to latest beta 2016-03-06 21:51:48 -08:00
Cameron Gutman e0c6d41d4b Update libs again to fix duplicate files 2016-03-06 21:51:01 -08:00
Cameron Gutman 44a0ae86d2 Working ENet with new common jar and modified ENet library 2016-03-06 15:55:33 -08:00
Cameron Gutman 06822ad385 Add JNI library for ENet 2016-03-05 17:48:10 -06:00
Cameron Gutman 3be52280ba Update common to disable dynamic resolution switching 2016-02-28 14:52:07 -05:00
Cameron Gutman 5142f978cf Fixed polling resuming in the background in some cases 2016-02-23 23:47:49 -05:00
Cameron Gutman 667ffd4dfd Bump to version 4.5.3.2 2016-02-23 16:33:57 -05:00
Cameron Gutman 17626f1853 Update common to crash in mDNS discovery agent 2016-02-23 16:33:38 -05:00
Cameron Gutman 5c79567a2c Bump version to 4.5.3.1 2016-02-20 20:11:26 -05:00
Cameron Gutman 0f5fd9af62 Update common to fix mDNS running passively in the background 2016-02-20 20:11:00 -05:00
Cameron Gutman 99643537d1 Only disable missing translation Lint errors rather than ignoring all Lint errors 2016-02-20 20:10:14 -05:00
Cameron Gutman 47650386e0 Bump version code and update common to fix video issue on H265 2016-02-19 11:35:52 -05:00
Cameron Gutman aa3fc34646 Update version code and lint options for building releases with Gradle 2.0 2016-02-19 04:11:03 -05:00
Cameron Gutman 92f5f1ac71 Bump to 4.5.3 with support for GFE 2.10.2 2016-02-19 03:58:38 -05:00
Cameron Gutman eb739f73c7 Update Gradle and Gradle Wrapper for Android Studio 2.0 2016-02-06 16:44:00 -05:00
Cameron Gutman 20a646106b Fix duplicate file exceptions with newer versions of Gradle 2016-02-06 16:43:11 -05:00
Cameron Gutman 0dc14517cd Bump version to 4.5.2 2016-01-30 05:16:29 -05:00
Cameron Gutman 04713c007b Remove some hacks for Android TV 2016-01-30 05:10:47 -05:00
Cameron Gutman 1cac7660b8 Fix a null pointer exception reported by a user 2016-01-30 04:55:17 -05:00
Cameron Gutman edb286f9af Hide the mouse on the main thread just to be safe 2016-01-30 04:27:14 -05:00
Cameron Gutman fb15ff99ca Add support for the NVIDIA relative mouse extensions for Shield devices 2016-01-30 04:21:20 -05:00
Cameron Gutman a455e75e37 Fix recognition of mouse events on Shield Portable 2016-01-30 04:15:09 -05:00
Cameron Gutman 2b452e51f9 Bump version to 4.5.1 2016-01-28 13:02:46 -05:00
Cameron Gutman 9d2b6f8854 Make nextDeviceId non-static since the lifetime of ControllerHandler is also just the life of the connection 2016-01-28 13:02:30 -05:00
Cameron Gutman 3be10a1b59 Update preference string to include Xbox 360 2016-01-28 12:55:19 -05:00
Cameron Gutman 01950c25a8 Only claim Xbox 360 controllers if the kernel hasn't already 2016-01-28 12:35:16 -05:00
Cameron Gutman 7ad1ebd0e8 Fix Xbox 360 driver 2016-01-28 12:07:11 -05:00
Cameron Gutman ee01a8b5a0 Turn the XB360 controller LED on at init 2016-01-27 14:00:14 -05:00
Cameron Gutman 23c54f6813 Add support for wired Xbox 360 controllers (pending testing) 2016-01-27 13:45:04 -05:00
Cameron Gutman ceef4510fb Fix infinite app list loading spinner if the app list is actually empty 2016-01-24 02:51:06 -05:00
Cameron Gutman 042a6b943e Bump version to 4.5 2016-01-20 02:18:22 -05:00
Cameron Gutman e114b73654 Revert "Fix margins around analog sticks"
This reverts commit 5d84f8af43.
2016-01-20 01:35:30 -05:00
Cameron Gutman da0a505978 Shrink the text size in the buttons so the start button text fits on the Nexus 9 2016-01-20 01:30:48 -05:00
Cameron Gutman cb6d4a385c Leave a margin around the d-pad so the selection rectangle doesn't draw over the control itself 2016-01-20 01:12:53 -05:00
Cameron Gutman 2806aee0fc Fix drawing and placement of face buttons 2016-01-20 01:04:06 -05:00
Cameron Gutman 52736f5162 Increase the time allowed for a double click to activate the stick button 2016-01-20 00:28:33 -05:00
Cameron Gutman 6d45ad7fe8 Improve precision of joystick inputs by lifting the deadzone after 150 ms. This way it prevents false inputs when activation the stick buttons but allows for precise movements after confirming that the touch is intended. 2016-01-20 00:28:11 -05:00
Cameron Gutman 2fc53644bc Use a uniform stroke width based on screen size in pixels 2016-01-19 20:26:46 -05:00
Cameron Gutman b33eaec493 Temporarily disable the config dialog and just map a tap of a controller element to move 2016-01-19 19:58:11 -05:00
Cameron Gutman 63d6f3ac78 Fix snapping into the deadzone when using analog sticks 2016-01-19 19:54:52 -05:00
Cameron Gutman fd4caac013 Fix erratic joystick movement 2016-01-19 19:44:33 -05:00
Cameron Gutman ada875cdb0 Highlight the controls red when in configuration mode 2016-01-19 18:52:51 -05:00
Cameron Gutman 49ddfa573d Ignore inputs when the on-screen controls are in configuration mode 2016-01-19 18:31:00 -05:00
Cameron Gutman b58ac367ee Increase the size of the virtual controller settings button 2016-01-19 18:24:10 -05:00
Cameron Gutman cf62b4ed95 Select is slightly too long for the button so rename it to Backc 2016-01-19 18:13:16 -05:00
Cameron Gutman b05c62e141 Fix outside of each d-pad button being cut off by the end of the canvas 2016-01-19 18:01:30 -05:00
Cameron Gutman 095556106c Fix highlighting of selected controller element during configuration 2016-01-19 17:45:14 -05:00
Cameron Gutman 5cdd72a45c Disable printing controller output 2016-01-19 17:35:17 -05:00
Cameron Gutman 5d84f8af43 Fix margins around analog sticks 2016-01-19 17:34:52 -05:00
Cameron Gutman d9483d9214 Show a nicer configuration toast 2016-01-19 17:30:49 -05:00
Cameron Gutman 250475830f Draw the highlight border after the element so it doesn't get drawn over 2016-01-19 17:08:00 -05:00
Cameron Gutman b8a0a823e0 Raise d-pad and buttons slightly further from the analog sticks 2016-01-19 16:33:00 -05:00
Cameron Gutman 6a54d669a3 Fix capitalization of preference group 2016-01-19 16:31:06 -05:00
Cameron Gutman 62559c4e66 Merge branch 'master' of https://github.com/hop3l3ss/limelight-android 2016-01-19 16:23:56 -05:00
Cameron Gutman e04ecaaf7a Rework the face buttons to match the d-pad 2016-01-19 16:23:40 -05:00
Karim fa4706c95f fix on screen controls category typo 2016-01-09 12:56:39 +01:00
Karim 7067c0e02e show onscreen controls settings only on touchscreen devices 2016-01-09 12:49:12 +01:00
Cameron Gutman cc71ce6180 Fix crash in XB1 controller driver on Fire HD 6 after controller removal 2016-01-07 22:52:17 -06:00
Cameron Gutman f409a3583c Fix direct submit behavior in decoders since the addition of HEVC 2016-01-07 18:51:02 -06:00
Cameron Gutman ac7504e017 Bump version to 4.0.4 2016-01-07 16:08:08 -06:00
Cameron Gutman 345bd3f7c1 Hide on-screen controls preference until bugs are resolved 2016-01-07 16:01:33 -06:00
Cameron Gutman 2e2960ec69 Disable on-screen controls by default 2016-01-07 12:57:59 -06:00
Cameron Gutman e93b103d1e Fix ConcurrentModificationException in virtual controller code 2016-01-07 12:57:37 -06:00
Cameron Gutman 22977a4c5b Use a socket for communication from EvdevReader to Moonlight rather than stdin/stdout. On some devices, fwrite(stdout) hangs for unknown reasons. 2016-01-07 12:49:30 -06:00
Cameron Gutman 7da5d5322b Cache Paint objects instead of allocation in draw method 2016-01-07 02:23:34 -06:00
Cameron Gutman 49e2c40ba4 Add LB and RB buttons to virtual controller 2016-01-07 01:06:22 -06:00
Cameron Gutman 8041a004c2 Remove text from d-pad as it tends to get in the way of visuals on screen 2016-01-07 01:00:15 -06:00
Cameron Gutman db62d78e04 On-screen controls: Fix functionality of Select button and rename Play to Start 2016-01-07 00:45:30 -06:00
Cameron Gutman bd79318b1e Cleanup new virtual controller code 2016-01-07 00:30:45 -06:00
Cameron Gutman 2736bd9165 Android Studio auto-reformat of new virtual controller code 2016-01-07 00:24:39 -06:00
Cameron Gutman b6bd48584f Refactor to match other preference conventions 2016-01-07 00:20:46 -06:00
Cameron Gutman 7b4f3c975a Fix on-screen controls not showing up on 16:9 devices 2016-01-07 00:15:33 -06:00
Cameron Gutman b165fadc55 Remove unused file 2016-01-07 00:14:16 -06:00
Cameron Gutman 274e0d0557 Merge branch 'master' into virtualcontroller_master
Conflicts:
	app/app.iml
	app/build.gradle
	app/libs/limelight-common.jar
	app/src/main/java/com/limelight/Game.java
	app/src/main/java/com/limelight/binding/input/ControllerHandler.java
	app/src/main/java/com/limelight/binding/video/MediaCodecHelper.java
	app/src/main/java/com/limelight/computers/ComputerDatabaseManager.java
	app/src/main/java/com/limelight/preferences/PreferenceConfiguration.java
	app/src/main/jni/evdev_reader/evdev_reader.c
	app/src/main/res/xml/preferences.xml
	limelight-android.iml
	limelight_vc.iml
	moonlight-android.iml
2016-01-07 00:01:03 -06:00
Cameron Gutman 7594e51a18 Fix SQL injection vulnerability and crashes when an apostrophe is present in a computer name 2016-01-06 15:17:30 -06:00
Cameron Gutman bf22819b53 Update common with timeouts for RTSP handshake 2016-01-06 13:08:18 -06:00
Cameron Gutman 3dea4b15e0 Fix support for kernels that output 24-byte input events 2016-01-06 13:05:51 -06:00
Cameron Gutman 5836b3292b Only grab event devices 2016-01-06 12:36:09 -06:00
Cameron Gutman a8fd49a234 Fix possible segmentation fault or memory corruption if EVIOCGRAB fails and the cleanup is executed before the device entry is inserted into the list 2016-01-06 12:35:45 -06:00
Cameron Gutman 006ad72eb2 Check the stdin poll() return value before reading 2016-01-05 19:53:23 -06:00
Cameron Gutman dc254e1ee5 Some S6s have back buttons on the device called sec_touchkey so also ignore back presses on those too 2016-01-05 00:27:19 -06:00
Cameron Gutman b0d31a4d35 Update version for 4.0.3 r2 2016-01-04 09:30:56 -06:00
Cameron Gutman 24155feea4 Update common with proper HEVC fix for r2 of 4.0.3 2016-01-04 09:29:22 -06:00
Cameron Gutman db0a4e35c6 Bump to 4.0.3 2016-01-03 16:35:21 -06:00
Cameron Gutman 68ef98d346 Update common to fix broken mobile 900-series GPU detection for H.265 2016-01-03 16:29:02 -06:00
Karim f23bb9fac1 improve virtual controller:
* add digital 8-Way pad
  * add on screen element size and position configuration
  * begin with cleanup
2016-01-03 11:12:43 +01:00
Cameron Gutman d20dde0b6d Print a message when the EvdevReader starts 2016-01-02 19:42:40 -06:00
Cameron Gutman f76b30d109 Fix exceptions in onStop when the connection is aborted due to lack of H.264 support 2016-01-02 18:28:01 -06:00
Cameron Gutman ee1a047cde Remove several decoders from the whitelist based on some user-reported issues 2016-01-02 18:16:12 -06:00
hop3l3ss 4c533fedfd Merge pull request #1 from ruqqq/master
Merge https://github.com/limelight-stream/limelight-android
2015-12-31 11:44:42 +01:00
Faruq Rasid f8ab7b8e13 Merge https://github.com/limelight-stream/limelight-android 2015-12-31 10:14:30 +08:00
Cameron Gutman 46c5eaf0e1 Fix a user-reported crash in USB code 2015-12-23 14:03:55 -06:00
Karim Mreisi 1d6b5a35bd Merge https://github.com/limelight-stream/limelight-android 2015-02-03 21:52:02 +01:00
Karim Mreisi 1ff6ee14ac fix analogstick, add minimum range and press deadzone, add movement touch to digital buttons depending on layers 2015-02-03 21:51:27 +01:00
Karim Mreisi d2e51e97c0 square analog stick for testing 2015-01-28 08:25:22 +01:00
Karim Mreisi 9f94465979 add virtual controller element abstraction class 2015-01-28 07:12:20 +01:00
Karim Mreisi d83526ff5c add analog stick double click event, add button long press event, add virtual controller settings draft 2015-01-26 09:38:52 +01:00
Karim Mreisi 1d6b7e1b2e fix digital button/pad mouse movement, add selct & start button 2015-01-25 09:21:37 +01:00
Karim Mreisi 1c9458d056 fix digital button revoke event, update colors 2015-01-24 11:46:31 +01:00
Karim Mreisi 4e29f2ae8b add real digital pad and new digital buttons 2015-01-24 10:26:28 +01:00
Karim Mreisi 69321636b5 add LB and RB 2015-01-23 07:30:08 +01:00
Karim Mreisi d190b254bd Merge https://github.com/limelight-stream/limelight-android 2015-01-23 06:57:51 +01:00
Karim Mreisi 005a96f3d3 fix not implemented toast message 2015-01-22 09:01:30 +01:00
Karim Mreisi e39e0910a1 add virtual controller configuration screen 2015-01-22 08:59:55 +01:00
Karim Mreisi 56a6cee8f2 add touch controls 2015-01-22 08:06:14 +01:00
59 changed files with 3326 additions and 459 deletions
+3
View File
@@ -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
+1 -1
View File
@@ -8,7 +8,7 @@ whether in your own home or over the internet.
[Moonlight-pc](https://github.com/moonlight-stream/moonlight-pc) is also currently in development for Windows, OS X and Linux. Versions for [iOS](https://github.com/moonlight-stream/moonlight-ios) and [Windows and Windows Phone](https://github.com/moonlight-stream/moonlight-windows) are also in development.
Check our [wiki](https://github.com/moonlight-stream/moonlight-android/wiki) for more detailed information or a troubleshooting guide.
Check our [wiki](https://github.com/moonlight-stream/moonlight-docs/wiki) for more detailed information or a troubleshooting guide.
##Features
+16 -5
View File
@@ -4,15 +4,15 @@ import org.apache.tools.ant.taskdefs.condition.Os
apply plugin: 'com.android.application'
android {
compileSdkVersion 23
buildToolsVersion "23.0.2"
compileSdkVersion 24
buildToolsVersion "24.0.0"
defaultConfig {
minSdkVersion 16
targetSdkVersion 23
targetSdkVersion 24
versionName "4.0.2"
versionCode = 79
versionName "4.6"
versionCode = 103
}
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.
+4 -14
View File
@@ -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,22 +31,10 @@
<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" >
@@ -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" />
+24 -4
View File
@@ -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();
}
+206 -115
View File
@@ -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.capture.InputCaptureManager;
import com.limelight.binding.input.capture.InputCaptureProvider;
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;
@@ -36,6 +39,7 @@ import android.hardware.input.InputManager;
import android.media.AudioManager;
import android.net.ConnectivityManager;
import android.net.wifi.WifiManager;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
@@ -45,14 +49,13 @@ import android.view.InputDevice;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
import android.view.View;
import android.view.View.OnGenericMotionListener;
import android.view.View.OnSystemUiVisibilityChangeListener;
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 +74,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 InputCaptureProvider inputCaptureProvider;
private int modifierFlags = 0;
private boolean grabbedInput = true;
private boolean grabComboDown = false;
private StreamView streamView;
private EnhancedDecoderRenderer decoderRenderer;
@@ -171,13 +174,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();
@@ -246,42 +246,30 @@ public class Game extends Activity implements SurfaceHolder.Callback,
InputManager inputManager = (InputManager) getSystemService(Context.INPUT_SERVICE);
inputManager.registerInputDeviceListener(controllerHandler, null);
boolean aspectRatioMatch = false;
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.KITKAT) {
// On KitKat and later (where we can use the whole screen via immersive mode), we'll
// calculate whether we need to scale by aspect ratio or not. If not, we'll use
// setFixedSize so we can handle 4K properly. The only known devices that have
// >= 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.
double screenAspectRatio = ((double)screenSize.y) / screenSize.x;
double streamAspectRatio = ((double)prefConfig.height) / prefConfig.width;
if (Math.abs(screenAspectRatio - streamAspectRatio) < 0.001) {
LimeLog.info("Stream has compatible aspect ratio with output display");
aspectRatioMatch = true;
}
}
SurfaceHolder sh = sv.getHolder();
if (prefConfig.stretchVideo || aspectRatioMatch) {
// Set the surface to the size of the video
sh.setFixedSize(prefConfig.width, prefConfig.height);
}
else {
deferredSurfaceResize = true;
}
// Set to the optimal mode for streaming
prepareDisplayForRendering();
// 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) {
// Start watching for raw input
evdevHandler = new EvdevHandler(this, this);
evdevHandler.start();
// Use sustained performance mode on N+ to ensure consistent
// CPU availability
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
getWindow().setSustainedPerformanceMode(true);
}
inputCaptureProvider = InputCaptureManager.getInputCaptureProvider(this, this);
if (prefConfig.onscreenController) {
// create virtual onscreen controller
virtualController = new VirtualController(conn,
(FrameLayout)findViewById(R.id.surfaceView).getParent(),
this);
virtualController.refreshLayout();
}
if (prefConfig.usbDriver) {
@@ -291,22 +279,96 @@ public class Game extends Activity implements SurfaceHolder.Callback,
}
// The connection will be started when the surface gets created
sh.addCallback(this);
streamView.getHolder().addCallback(this);
}
private void resizeSurfaceWithAspectRatio(SurfaceView sv, double vidWidth, double vidHeight)
{
// Get the visible width of the activity
double visibleWidth = getWindow().getDecorView().getWidth();
private void prepareDisplayForRendering() {
Display display = getWindowManager().getDefaultDisplay();
WindowManager.LayoutParams windowLayoutParams = getWindow().getAttributes();
ViewGroup.LayoutParams lp = sv.getLayoutParams();
// On M, we can explicitly set the optimal display mode
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
Display.Mode bestMode = display.getMode();
for (Display.Mode candidate : display.getSupportedModes()) {
boolean refreshRateOk = candidate.getRefreshRate() >= bestMode.getRefreshRate() &&
candidate.getRefreshRate() < 63;
boolean resolutionOk = candidate.getPhysicalWidth() >= bestMode.getPhysicalWidth() &&
candidate.getPhysicalHeight() >= bestMode.getPhysicalHeight() &&
candidate.getPhysicalWidth() <= 4096;
// Calculate the new size of the SurfaceView
lp.width = (int) visibleWidth;
lp.height = (int) ((vidHeight / vidWidth) * visibleWidth);
LimeLog.info("Examining display mode: "+candidate.getPhysicalWidth()+"x"+
candidate.getPhysicalHeight()+"x"+candidate.getRefreshRate());
// Apply the size change
sv.setLayoutParams(lp);
// On non-4K streams, we force the resolution to never change
if (prefConfig.width < 3840) {
if (display.getMode().getPhysicalWidth() != candidate.getPhysicalWidth() ||
display.getMode().getPhysicalHeight() != candidate.getPhysicalHeight()) {
continue;
}
}
// Make sure the refresh rate doesn't regress
if (!refreshRateOk) {
continue;
}
// Make sure the resolution doesn't regress
if (!resolutionOk) {
continue;
}
bestMode = candidate;
}
LimeLog.info("Selected display mode: "+bestMode.getPhysicalWidth()+"x"+
bestMode.getPhysicalHeight()+"x"+bestMode.getRefreshRate());
windowLayoutParams.preferredDisplayModeId = bestMode.getModeId();
}
// On L, we can at least tell the OS that we want 60 Hz
else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
float bestRefreshRate = display.getRefreshRate();
for (float candidate : display.getSupportedRefreshRates()) {
if (candidate > bestRefreshRate && candidate < 63) {
LimeLog.info("Examining refresh rate: "+candidate);
bestRefreshRate = candidate;
}
}
LimeLog.info("Selected refresh rate: "+bestRefreshRate);
windowLayoutParams.preferredRefreshRate = bestRefreshRate;
}
// Apply the display mode change
getWindow().setAttributes(windowLayoutParams);
// From 4.4 to 5.1 we can't ask for a 4K display mode, so we'll
// need to hint the OS to provide one.
boolean aspectRatioMatch = false;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT &&
Build.VERSION.SDK_INT <= Build.VERSION_CODES.LOLLIPOP_MR1) {
// On KitKat and later (where we can use the whole screen via immersive mode), we'll
// calculate whether we need to scale by aspect ratio or not. If not, we'll use
// setFixedSize so we can handle 4K properly. The only known devices that have
// >= 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.
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) {
LimeLog.info("Stream has compatible aspect ratio with output display");
aspectRatioMatch = true;
}
}
if (prefConfig.stretchVideo || aspectRatioMatch) {
// Set the surface to the size of the video
streamView.getHolder().setFixedSize(prefConfig.width, prefConfig.height);
}
else {
// Set the surface to scale based on the aspect ratio of the stream
streamView.setDesiredAspectRatio((double)prefConfig.width / (double)prefConfig.height);
}
}
private void checkDataConnection()
@@ -354,8 +416,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 +428,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,13 +468,11 @@ public class Game extends Activity implements SurfaceHolder.Callback,
private final Runnable toggleGrab = new Runnable() {
@Override
public void run() {
if (evdevHandler != null) {
if (grabbedInput) {
evdevHandler.ungrabAll();
}
else {
evdevHandler.regrabAll();
}
if (grabbedInput) {
inputCaptureProvider.disableCapture();
}
else {
inputCaptureProvider.enableCapture();
}
grabbedInput = !grabbedInput;
@@ -515,11 +579,6 @@ public class Game extends Activity implements SurfaceHolder.Callback,
return true;
}
// Eat repeat down events
if (event.getRepeatCount() > 0) {
return true;
}
// Pass through keyboard input if we're not grabbing
if (!grabbedInput) {
return super.onKeyDown(keyCode, event);
@@ -596,7 +655,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 +694,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 (inputCaptureProvider.eventHasRelativeMouseAxes(event)) {
// Send the deltas straight from the motion event
conn.sendMouseMove((short) inputCaptureProvider.getRelativeAxisX(event),
(short) inputCaptureProvider.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 +836,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);
}
@@ -795,11 +875,11 @@ public class Game extends Activity implements SurfaceHolder.Callback,
conn.stop();
}
// Close the Evdev reader to allow use of captured input devices
if (evdevHandler != null) {
evdevHandler.stop();
evdevHandler = null;
}
// Enable cursor visibility again
inputCaptureProvider.disableCapture();
// Destroy the capture provider
inputCaptureProvider.destroy();
}
@Override
@@ -839,6 +919,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.
inputCaptureProvider.enableCapture();
}
});
hideSystemUi(1000);
}
@@ -873,13 +963,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);
}
@@ -888,6 +971,14 @@ public class Game extends Activity implements SurfaceHolder.Callback,
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
if (connected) {
// HACK: Android is supposed to let you return from this function
// before throwing a fit if you access the surface again. Unfortunately,
// MediaCodec often tries to access the destroyed surface and triggers
// an IllegalStateException. To workaround this, we will invoke
// the DecoderRenderer's stop function ourselves, so it will hopefully
// happen early enough to not trigger the bug
decoderRenderer.stop();
stopConnection();
}
}
+17 -19
View File
@@ -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) {
@@ -302,7 +299,7 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
// Stop updates and wait while pairing
stopComputerUpdates(true);
InetAddress addr = null;
InetAddress addr;
if (computer.reachability == ComputerDetails.Reachability.LOCAL) {
addr = computer.localIp;
}
@@ -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();
}
@@ -431,7 +429,7 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
NvHTTP httpConn;
String message;
try {
InetAddress addr = null;
InetAddress addr;
if (computer.reachability == ComputerDetails.Reachability.LOCAL) {
addr = computer.localIp;
}
@@ -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,22 @@ 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;
}
}
}
// The ADT-1 controller needs a similar fixup to the ASUS Gamepad
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.KITKAT) {
// The device name provided is just "Gamepad" which is pretty useless, so we
// use VID/PID instead
if (dev.getVendorId() == 0x18d1 && dev.getProductId() == 0x2c40) {
context.backIsStart = true;
context.modeIsSelect = true;
context.triggerDeadzone = 0.30f;
}
}
@@ -362,7 +386,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 +422,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
@@ -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,59 @@
package com.limelight.binding.input.capture;
import android.annotation.TargetApi;
import android.app.Activity;
import android.content.Context;
import android.os.Build;
import android.view.MotionEvent;
import android.view.PointerIcon;
import android.view.View;
import android.view.ViewGroup;
@TargetApi(Build.VERSION_CODES.N)
public class AndroidCaptureProvider extends InputCaptureProvider {
private ViewGroup rootViewGroup;
private Context context;
public AndroidCaptureProvider(Activity activity) {
this.context = activity;
this.rootViewGroup = (ViewGroup) activity.getWindow().getDecorView();
}
public static boolean isCaptureProviderSupported() {
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.N;
}
private void setPointerIconOnAllViews(PointerIcon icon) {
for (int i = 0; i < rootViewGroup.getChildCount(); i++) {
View view = rootViewGroup.getChildAt(i);
view.setPointerIcon(icon);
}
rootViewGroup.setPointerIcon(icon);
}
@Override
public void enableCapture() {
setPointerIconOnAllViews(PointerIcon.getSystemIcon(context, PointerIcon.TYPE_NULL));
}
@Override
public void disableCapture() {
setPointerIconOnAllViews(null);
}
@Override
public boolean eventHasRelativeMouseAxes(MotionEvent event) {
return event.getAxisValue(MotionEvent.AXIS_RELATIVE_X) != 0 ||
event.getAxisValue(MotionEvent.AXIS_RELATIVE_Y) != 0;
}
@Override
public float getRelativeAxisX(MotionEvent event) {
return event.getAxisValue(MotionEvent.AXIS_RELATIVE_X);
}
@Override
public float getRelativeAxisY(MotionEvent event) {
return event.getAxisValue(MotionEvent.AXIS_RELATIVE_Y);
}
}
@@ -0,0 +1,31 @@
package com.limelight.binding.input.capture;
import android.app.Activity;
import com.limelight.LimeLog;
import com.limelight.binding.input.evdev.EvdevCaptureProvider;
import com.limelight.binding.input.evdev.EvdevListener;
public class InputCaptureManager {
public static InputCaptureProvider getInputCaptureProvider(Activity activity, EvdevListener rootListener) {
// Shield capture is preferred because it can capture when the cursor is over
// the system UI. Android N native capture can only capture over views owned
// by the application.
if (ShieldCaptureProvider.isCaptureProviderSupported()) {
LimeLog.info("Using NVIDIA mouse capture extension");
return new ShieldCaptureProvider(activity);
}
else if (AndroidCaptureProvider.isCaptureProviderSupported()) {
LimeLog.info("Using Android N+ native mouse capture");
return new AndroidCaptureProvider(activity);
}
else if (EvdevCaptureProvider.isCaptureProviderSupported()) {
LimeLog.info("Using Evdev mouse capture");
return new EvdevCaptureProvider(activity, rootListener);
}
else {
LimeLog.info("Mouse capture not available");
return new NullCaptureProvider();
}
}
}
@@ -0,0 +1,21 @@
package com.limelight.binding.input.capture;
import android.view.MotionEvent;
public abstract class InputCaptureProvider {
public abstract void enableCapture();
public abstract void disableCapture();
public void destroy() {}
public boolean eventHasRelativeMouseAxes(MotionEvent event) {
return false;
}
public float getRelativeAxisX(MotionEvent event) {
return 0;
}
public float getRelativeAxisY(MotionEvent event) {
return 0;
}
}
@@ -0,0 +1,10 @@
package com.limelight.binding.input.capture;
public class NullCaptureProvider extends InputCaptureProvider {
@Override
public void enableCapture() {}
@Override
public void disableCapture() {}
}
@@ -0,0 +1,89 @@
package com.limelight.binding.input.capture;
import android.content.Context;
import android.hardware.input.InputManager;
import android.view.MotionEvent;
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 ShieldCaptureProvider extends InputCaptureProvider {
private static boolean nvExtensionSupported;
private static Method methodSetCursorVisibility;
private static int AXIS_RELATIVE_X;
private static int AXIS_RELATIVE_Y;
private Context context;
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) {
nvExtensionSupported = false;
}
}
public ShieldCaptureProvider(Context context) {
this.context = context;
}
public static boolean isCaptureProviderSupported() {
return nvExtensionSupported;
}
private boolean setCursorVisibility(boolean visible) {
try {
methodSetCursorVisibility.invoke(context.getSystemService(Context.INPUT_SERVICE), visible);
return true;
} catch (InvocationTargetException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
return false;
}
@Override
public void enableCapture() {
setCursorVisibility(false);
}
@Override
public void disableCapture() {
setCursorVisibility(true);
}
@Override
public boolean eventHasRelativeMouseAxes(MotionEvent event) {
return event.getAxisValue(AXIS_RELATIVE_X) != 0 ||
event.getAxisValue(AXIS_RELATIVE_Y) != 0;
}
@Override
public float getRelativeAxisX(MotionEvent event) {
return event.getAxisValue(AXIS_RELATIVE_X);
}
@Override
public float getRelativeAxisY(MotionEvent event) {
return event.getAxisValue(AXIS_RELATIVE_Y);
}
}
@@ -0,0 +1,47 @@
package com.limelight.binding.input.driver;
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;
@@ -68,14 +72,14 @@ public class UsbDriverService extends Service implements UsbDriverListener {
// Initial attachment broadcast
if (action.equals(UsbManager.ACTION_USB_DEVICE_ATTACHED)) {
UsbDevice device = (UsbDevice) intent.getParcelableExtra(UsbManager.EXTRA_DEVICE);
UsbDevice device = intent.getParcelableExtra(UsbManager.EXTRA_DEVICE);
// Continue the state machine
handleUsbDeviceState(device);
}
// Subsequent permission dialog completion intent
else if (action.equals(ACTION_USB_PERMISSION)) {
UsbDevice device = (UsbDevice) intent.getParcelableExtra(UsbManager.EXTRA_DEVICE);
UsbDevice device = intent.getParcelableExtra(UsbManager.EXTRA_DEVICE);
// If we got this far, we've already found we're able to handle this device
if (intent.getBooleanExtra(UsbManager.EXTRA_PERMISSION_GRANTED, false)) {
@@ -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,136 @@
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 {
private static final int XB360_IFACE_SUBCLASS = 93;
private static final int XB360_IFACE_PROTOCOL = 1; // Wired only
private static final int[] SUPPORTED_VENDORS = {
0x044f, // Thrustmaster
0x045e, // Microsoft
0x046d, // Logitech
0x0738, // Mad Catz
0x0e6f, // Unknown
0x12ab, // Unknown
0x1430, // RedOctane
0x146b, // BigBen
0x1bad, // Harmonix
0x0f0d, // Hori
0x1689, // Razer Onza
0x24c6, // PowerA
0x1532, // Razer Sabertooth
0x15e4, // Numark
0x162e, // Joytech
};
public static boolean canClaimDevice(UsbDevice device) {
for (int supportedVid : SUPPORTED_VENDORS) {
if (device.getVendorId() == supportedVid &&
device.getInterfaceCount() >= 1 &&
device.getInterface(0).getInterfaceClass() == UsbConstants.USB_CLASS_VENDOR_SPEC &&
device.getInterface(0).getInterfaceSubclass() == XB360_IFACE_SUBCLASS &&
device.getInterface(0).getInterfaceProtocol() == XB360_IFACE_PROTOCOL) {
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;
}
}
@@ -3,60 +3,30 @@ 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 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;
public class XboxOneController extends AbstractXboxController {
private Thread inputThread;
private UsbDriverListener listener;
private boolean stopped;
private short buttonFlags;
private float leftTrigger, rightTrigger;
private float rightStickX, rightStickY;
private float leftStickX, leftStickY;
private static final int MICROSOFT_VID = 0x045e;
private static final int XB1_IFACE_SUBCLASS = 71;
private static final int XB1_IFACE_PROTOCOL = 208;
private static final int[] SUPPORTED_VENDORS = {
0x045e, // Microsoft
0x0738, // Mad Catz
0x0e6f, // Unknown
0x0f0d, // Hori
0x24c6, // PowerA
};
// FIXME: odata_serial
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,103 +60,42 @@ 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;
return true;
}
return false;
}
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));
}
public static boolean canClaimDevice(UsbDevice device) {
for (int supportedVid : SUPPORTED_VENDORS) {
if (device.getVendorId() == supportedVid &&
device.getInterfaceCount() >= 1 &&
device.getInterface(0).getInterfaceClass() == UsbConstants.USB_CLASS_VENDOR_SPEC &&
device.getInterface(0).getInterfaceSubclass() == XB1_IFACE_SUBCLASS &&
device.getInterface(0).getInterfaceProtocol() == XB1_IFACE_PROTOCOL) {
return true;
}
};
inputThread.setName("Xbox One Controller - Input Thread");
inputThread.start();
}
return false;
}
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
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;
}
@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) {
@@ -194,40 +103,6 @@ public class XboxOneController {
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();
}
public static boolean canClaimDevice(UsbDevice device) {
return device.getVendorId() == MICROSOFT_VID &&
device.getInterfaceCount() >= 1 &&
device.getInterface(0).getInterfaceClass() == UsbConstants.USB_CLASS_VENDOR_SPEC &&
device.getInterface(0).getInterfaceSubclass() == XB1_IFACE_SUBCLASS &&
device.getInterface(0).getInterfaceProtocol() == XB1_IFACE_PROTOCOL;
}
}
@@ -1,13 +1,21 @@
package com.limelight.binding.input.evdev;
import android.content.Context;
import android.app.Activity;
import android.widget.Toast;
import com.limelight.LimeLog;
import com.limelight.LimelightBuildProps;
import com.limelight.binding.input.capture.InputCaptureProvider;
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 {
public class EvdevCaptureProvider extends InputCaptureProvider {
private final EvdevListener listener;
private final String libraryPath;
@@ -15,7 +23,11 @@ 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 boolean started = false;
private static final byte UNGRAB_REQUEST = 1;
private static final byte REGRAB_REQUEST = 2;
@@ -27,19 +39,47 @@ 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) {
reportDeviceNotRooted();
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) {
reportDeviceNotRooted();
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,23 +162,48 @@ public class EvdevHandler {
}
};
public EvdevHandler(Context context, EvdevListener listener) {
public EvdevCaptureProvider(Activity activity, EvdevListener listener) {
this.listener = listener;
this.libraryPath = context.getApplicationInfo().nativeLibraryDir;
this.activity = activity;
this.libraryPath = activity.getApplicationInfo().nativeLibraryDir;
}
public void regrabAll() {
if (!shutdown && evdevOut != null) {
try {
evdevOut.write(REGRAB_REQUEST);
} catch (IOException e) {
e.printStackTrace();
public static boolean isCaptureProviderSupported() {
return LimelightBuildProps.ROOT_BUILD;
}
private void reportDeviceNotRooted() {
activity.runOnUiThread(new Runnable() {
@Override
public void run() {
Toast.makeText(activity, "This device is not rooted - Mouse capture is unavailable", Toast.LENGTH_LONG).show();
}
});
}
@Override
public void enableCapture() {
if (!started) {
// Start the handler thread if it's our first time
// capturing
handlerThread.start();
started = true;
}
else {
// Send a request to regrab if we're already capturing
if (!shutdown && evdevOut != null) {
try {
evdevOut.write(REGRAB_REQUEST);
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
public void ungrabAll() {
if (!shutdown && evdevOut != null) {
@Override
public void disableCapture() {
if (started && !shutdown && evdevOut != null) {
try {
evdevOut.write(UNGRAB_REQUEST);
} catch (IOException e) {
@@ -147,18 +212,35 @@ public class EvdevHandler {
}
}
public void start() {
handlerThread.start();
}
public void stop() {
@Override
public void destroy() {
// We need to stop the process in this context otherwise
// we could get stuck waiting on output from the process
// in order to terminate it.
if (!started) {
return;
}
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 +257,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<>();
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<>();
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();
}
});
}
}
@@ -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();
}
});
}
}
@@ -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<>();
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();
}
}
}
@@ -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) {
}
*/
}
@@ -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 {
@@ -469,8 +475,8 @@ public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer {
} catch (InterruptedException ignored) { }
}
// Stop the decoder
videoDecoder.stop();
// We could stop the decoder here, but it seems to cause some problems
// so we'll just let release take care of it.
}
@Override
@@ -34,7 +34,7 @@ public class MediaCodecHelper {
private static final List<String> whitelistedHevcDecoders;
static {
directSubmitPrefixes = new LinkedList<String>();
directSubmitPrefixes = new LinkedList<>();
// These decoders have low enough input buffer latency that they
// can be directly invoked from the receive thread
@@ -48,42 +48,64 @@ public class MediaCodecHelper {
}
static {
preferredDecoders = new LinkedList<String>();
preferredDecoders = new LinkedList<>();
}
static {
blacklistedDecoderPrefixes = new LinkedList<String>();
blacklistedDecoderPrefixes = new LinkedList<>();
// 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 {
spsFixupBitstreamFixupDecoderPrefixes = new LinkedList<String>();
spsFixupBitstreamFixupDecoderPrefixes = new LinkedList<>();
spsFixupBitstreamFixupDecoderPrefixes.add("omx.nvidia");
spsFixupBitstreamFixupDecoderPrefixes.add("omx.qcom");
spsFixupBitstreamFixupDecoderPrefixes.add("omx.brcm");
baselineProfileHackPrefixes = new LinkedList<String>();
baselineProfileHackPrefixes = new LinkedList<>();
baselineProfileHackPrefixes.add("omx.intel");
whitelistedAdaptiveResolutionPrefixes = new LinkedList<String>();
whitelistedAdaptiveResolutionPrefixes = new LinkedList<>();
whitelistedAdaptiveResolutionPrefixes.add("omx.nvidia");
whitelistedAdaptiveResolutionPrefixes.add("omx.qcom");
whitelistedAdaptiveResolutionPrefixes.add("omx.sec");
whitelistedAdaptiveResolutionPrefixes.add("omx.TI");
constrainedHighProfilePrefixes = new LinkedList<String>();
constrainedHighProfilePrefixes = new LinkedList<>();
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) {
@@ -196,7 +218,7 @@ public class MediaCodecHelper {
@SuppressWarnings("deprecation")
@SuppressLint("NewApi")
private static LinkedList<MediaCodecInfo> getMediaCodecList() {
LinkedList<MediaCodecInfo> infoList = new LinkedList<MediaCodecInfo>();
LinkedList<MediaCodecInfo> infoList = new LinkedList<>();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
MediaCodecList mcl = new MediaCodecList(MediaCodecList.REGULAR_CODECS);
@@ -354,7 +376,7 @@ public class MediaCodecHelper {
break;
cpuInfo.append((char)ch);
}
return cpuInfo.toString();
} finally {
br.close();
@@ -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) {
@@ -68,7 +68,7 @@ public class ComputerDatabaseManager {
public List<ComputerDetails> getAllComputers() {
Cursor c = computerDb.rawQuery("SELECT * FROM "+COMPUTER_TABLE_NAME, null);
LinkedList<ComputerDetails> computerList = new LinkedList<ComputerDetails>();
LinkedList<ComputerDetails> computerList = new LinkedList<>();
while (c.moveToNext()) {
ComputerDetails details = new ComputerDetails();
@@ -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();
@@ -44,7 +45,7 @@ public class ComputerManagerService extends Service {
private final AtomicInteger dbRefCount = new AtomicInteger(0);
private IdentityManager idManager;
private final LinkedList<PollingTuple> pollingTuples = new LinkedList<PollingTuple>();
private final LinkedList<PollingTuple> pollingTuples = new LinkedList<>();
private ComputerManagerListener listener = null;
private final AtomicInteger activePolls = new AtomicInteger(0);
private boolean pollingActive = false;
@@ -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();
@@ -14,8 +14,6 @@ import com.limelight.grid.assets.MemoryAssetLoader;
import com.limelight.grid.assets.NetworkAssetLoader;
import com.limelight.nvstream.http.ComputerDetails;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import java.util.Collections;
import java.util.Comparator;
@@ -16,7 +16,7 @@ public abstract class GenericGridAdapter<T> extends BaseAdapter {
protected final Context context;
protected final int defaultImageRes;
protected final int layoutId;
protected final ArrayList<T> itemList = new ArrayList<T>();
protected final ArrayList<T> itemList = new ArrayList<>();
protected final LayoutInflater inflater;
public GenericGridAdapter(Context context, int layoutId, int defaultImageRes) {
@@ -135,7 +135,7 @@ public class CachedAppAssetLoader {
private LoaderTuple tuple;
public LoaderTask(ImageView imageView, boolean diskOnly) {
this.imageViewRef = new WeakReference<ImageView>(imageView);
this.imageViewRef = new WeakReference<>(imageView);
this.diskOnly = diskOnly;
}
@@ -213,7 +213,7 @@ public class CachedAppAssetLoader {
public AsyncDrawable(Resources res, Bitmap bitmap,
LoaderTask loaderTask) {
super(res, bitmap);
loaderTaskReference = new WeakReference<LoaderTask>(loaderTask);
loaderTaskReference = new WeakReference<>(loaderTask);
}
public LoaderTask getLoaderTask() {
@@ -7,7 +7,6 @@ import com.limelight.LimeLog;
import com.limelight.utils.CacheHelper;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
@@ -29,7 +29,7 @@ import android.widget.Toast;
public class AddComputerManually extends Activity {
private TextView hostText;
private ComputerManagerService.ComputerManagerBinder managerBinder;
private final LinkedBlockingQueue<String> computersToAdd = new LinkedBlockingQueue<String>();
private final LinkedBlockingQueue<String> computersToAdd = new LinkedBlockingQueue<>();
private Thread addThread;
private final ServiceConnection serviceConnection = new ServiceConnection() {
public void onServiceConnected(ComponentName className, final IBinder binder) {
@@ -3,6 +3,7 @@ package com.limelight.preferences;
import android.content.Context;
import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.content.res.Configuration;
import android.os.Build;
import android.preference.PreferenceManager;
@@ -21,6 +22,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 +44,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 +57,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 +101,7 @@ public class PreferenceConfiguration {
}
// Use small mode on anything smaller than a 7" tablet
return context.getResources().getConfiguration().smallestScreenWidthDp < 600;
return context.getResources().getConfiguration().smallestScreenWidthDp < 500;
}
public static int getDefaultBitrate(Context context) {
@@ -183,6 +187,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;
}
@@ -6,8 +6,10 @@ 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 +62,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);
}
}
@@ -14,7 +14,7 @@ public class Dialog implements Runnable {
private AlertDialog alert;
private static final ArrayList<Dialog> rundownDialogs = new ArrayList<Dialog>();
private static final ArrayList<Dialog> rundownDialogs = new ArrayList<>();
private Dialog(Activity activity, String title, String message, boolean endAfterDismiss)
{
@@ -15,7 +15,7 @@ public class SpinnerDialog implements Runnable,OnCancelListener {
private ProgressDialog progress;
private final boolean finish;
private static final ArrayList<SpinnerDialog> rundownDialogs = new ArrayList<SpinnerDialog>();
private static final ArrayList<SpinnerDialog> rundownDialogs = new ArrayList<>();
private SpinnerDialog(Activity activity, String title, String message, boolean finish)
{
+121 -36
View File
@@ -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;
}
}
}
+25
View File
@@ -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)
+148
View File
@@ -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>
+1 -1
View File
@@ -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"
+2 -2
View File
@@ -40,8 +40,8 @@
</string-array>
<string-array name="video_format_names">
<item>Use H.265 only if safe</item>
<item>Always use H.265 if available</item>
<item>Use H.265 only if stable</item>
<item>Always use H.265 (may crash)</item>
<item>Never use H.265</item>
</string-array>
<string-array name="video_format_values" translatable="false">
+6 -2
View File
@@ -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>
+8 -1
View File
@@ -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
View File
@@ -4,7 +4,7 @@ buildscript {
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:1.3.0'
classpath 'com.android.tools.build:gradle:2.1.2'
}
}
+19
View File
@@ -0,0 +1,19 @@
# Project-wide Gradle settings.
# IDE (e.g. Android Studio) users:
# Settings specified in this file will override any Gradle settings
# configured through the IDE.
# For more details on how to configure your build environment visit
# http://www.gradle.org/docs/current/userguide/build_environment.html
# Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings.
# Default value: -Xmx10248m -XX:MaxPermSize=256m
# org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
org.gradle.jvmargs=-Xmx3072m
# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. More details, visit
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
# org.gradle.parallel=true
+2 -2
View File
@@ -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