Compare commits

...

186 Commits

Author SHA1 Message Date
Cameron Gutman 0fc61e52dd Fix Lint error in translation 2016-08-13 19:24:49 -07:00
Cameron Gutman 5e44c33bb6 Version 4.7 2016-08-13 19:22:29 -07:00
Cameron Gutman df3655e958 Add support for Xbox One S controller over USB 2016-08-13 19:21:49 -07:00
Cameron Gutman fe43e13145 Set larger dimensions for vector drawables so the generated PNGs are larger 2016-08-13 19:18:42 -07:00
Cameron Gutman acd3aad8d9 Add support for mouse emulation with a gamepad 2016-08-13 18:52:39 -07:00
Cameron Gutman 811b4b4f22 Better center overlays on PC view 2016-08-13 17:48:30 -07:00
Cameron Gutman 7db3b9f401 Use Material icons 2016-08-13 16:45:42 -07:00
Cameron Gutman a5a099cf43 Update common jar with 4K fix 2016-08-13 14:51:07 -07:00
Cameron Gutman ba605643bb Switch to indeterminate progress bars 2016-08-13 14:42:03 -07:00
Cameron Gutman 96e98c1abb Update translations 2016-08-13 12:32:49 -07:00
Cameron Gutman 5de6f6ae2b Fix build with Dutch translation 2016-08-13 12:29:08 -07:00
Cameron Gutman 0685722773 Merge branch 'master' of github.com:moonlight-stream/moonlight-android 2016-08-13 12:20:54 -07:00
Cameron Gutman 29df3b2859 Merge branch 'master' of https://github.com/halluci/moonlight-android 2016-08-13 12:14:37 -07:00
DragonSpirit fc6f859ced Russian Translation Added (#146) 2016-08-13 12:10:02 -07:00
Subject 6b21a5416f Dutch Translation Added (#118)
* Halfway through string translation

* Fixed up translation -- Ready for pull request
2016-08-13 12:09:39 -07:00
jeid64 74e7c8bbf1 Add basic building instructions to README (#227)
* Add basic building instructions to README

Added instructions to build APK for developers. Included getting submodules and installing NDK.

* Spelling fixes. Added ndk.dir
2016-08-13 12:07:20 -07:00
Cameron Gutman 757075b16a Add support for Xbox One S controller connected via Bluetooth 2016-08-13 12:01:52 -07:00
Cameron Gutman e8903c4d48 Update build tools to 24.0.1 2016-08-13 11:45:38 -07:00
halluci 98262d16ee Japanese translation added:updated 2016-07-17 22:01:49 +09:00
halluci 339506cf10 Japanese translation added 2016-07-17 17:22:30 +09:00
Cameron Gutman 63bd5df09b Prefer Evdev over N native capture since Evdev can capture over the system UI 2016-07-13 23:12:29 -07:00
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
83 changed files with 4025 additions and 529 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
+7 -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
@@ -46,6 +46,12 @@ This project is being actively developed at [XDA Developers](http://forum.xda-de
2. Write code
3. Send Pull Requests
##Building
* Install Android Studio and the Android NDK
* Run git submodule update --init --recursive from within moonlight-android/
* In moonlight-android/, create a file called local.properties. Add an ndk.dir= property to the local.properties file and set it equal to your NDK directory.
* Build the APK using Android Studio
##Authors
* [Cameron Gutman](https://github.com/cgutman)
+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.1"
defaultConfig {
minSdkVersion 16
targetSdkVersion 23
targetSdkVersion 24
versionName "4.0.2"
versionCode = 79
versionName "4.7"
versionCode = 104
}
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();
}
+207 -116
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();
@@ -241,47 +241,35 @@ public class Game extends Activity implements SurfaceHolder.Callback,
// Initialize the connection
conn = new NvConnection(host, uniqueId, Game.this, config, PlatformBinding.getCryptoProvider(this));
keybTranslator = new KeyboardTranslator(conn);
controllerHandler = new ControllerHandler(conn, this, prefConfig.multiController, prefConfig.deadzonePercentage);
controllerHandler = new ControllerHandler(this, conn, this, prefConfig.multiController, prefConfig.deadzonePercentage);
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 {}
@@ -1,24 +1,30 @@
package com.limelight.binding.input;
import android.content.Context;
import android.hardware.input.InputManager;
import android.os.SystemClock;
import android.util.SparseArray;
import android.view.InputDevice;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.widget.Toast;
import com.limelight.LimeLog;
import com.limelight.binding.input.driver.UsbDriverListener;
import com.limelight.nvstream.NvConnection;
import com.limelight.nvstream.input.ControllerPacket;
import com.limelight.nvstream.input.MouseButtonPacket;
import com.limelight.ui.GameGestures;
import com.limelight.utils.Vector2d;
import java.util.Timer;
import java.util.TimerTask;
public class ControllerHandler implements InputManager.InputDeviceListener, UsbDriverListener {
private static final int MAXIMUM_BUMPER_UP_DELAY_MS = 100;
private static final int START_DOWN_TIME_KEYB_MS = 750;
private static final int START_DOWN_TIME_MOUSE_MODE_MS = 750;
private static final int MINIMUM_BUTTON_DOWN_TIME_MS = 25;
@@ -34,6 +40,7 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
private final SparseArray<UsbDeviceContext> usbDeviceContexts = new SparseArray<>();
private final NvConnection conn;
private final Context activityContext;
private final double stickDeadzone;
private final InputDeviceContext defaultContext = new InputDeviceContext();
private final GameGestures gestures;
@@ -42,7 +49,8 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
private final boolean multiControllerEnabled;
private short currentControllers;
public ControllerHandler(NvConnection conn, GameGestures gestures, boolean multiControllerEnabled, int deadzonePercentage) {
public ControllerHandler(Context activityContext, NvConnection conn, GameGestures gestures, boolean multiControllerEnabled, int deadzonePercentage) {
this.activityContext = activityContext;
this.conn = conn;
this.gestures = gestures;
this.multiControllerEnabled = multiControllerEnabled;
@@ -82,6 +90,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 +155,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 +222,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 +240,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 +253,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 +342,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 +394,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
@@ -371,6 +403,10 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
context.isServal = true;
context.ignoreBack = true;
}
// The Xbox One S Bluetooth controller has some mappings that need fixing up
else if (devName.equals("Xbox Wireless Controller")) {
context.isXboxBtController = true;
}
}
LimeLog.info("Analog stick deadzone: "+context.leftStickDeadzoneRadius+" "+context.rightStickDeadzoneRadius);
@@ -398,12 +434,116 @@ 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 &&
context.mouseEmulationActive == originalContext.mouseEmulationActive) {
inputMap |= context.inputMap;
leftTrigger |= maxByMagnitude(leftTrigger, context.leftTrigger);
rightTrigger |= maxByMagnitude(rightTrigger, context.rightTrigger);
leftStickX |= maxByMagnitude(leftStickX, context.leftStickX);
leftStickY |= maxByMagnitude(leftStickY, context.leftStickY);
rightStickX |= maxByMagnitude(rightStickX, context.rightStickX);
rightStickY |= maxByMagnitude(rightStickY, context.rightStickY);
}
}
for (int i = 0; i < usbDeviceContexts.size(); i++) {
GenericControllerContext context = usbDeviceContexts.valueAt(i);
if (context.assignedControllerNumber &&
context.controllerNumber == controllerNumber &&
context.mouseEmulationActive == originalContext.mouseEmulationActive) {
inputMap |= context.inputMap;
leftTrigger |= maxByMagnitude(leftTrigger, context.leftTrigger);
rightTrigger |= maxByMagnitude(rightTrigger, context.rightTrigger);
leftStickX |= maxByMagnitude(leftStickX, context.leftStickX);
leftStickY |= maxByMagnitude(leftStickY, context.leftStickY);
rightStickX |= maxByMagnitude(rightStickX, context.rightStickX);
rightStickY |= maxByMagnitude(rightStickY, context.rightStickY);
}
}
if (defaultContext.controllerNumber == controllerNumber) {
inputMap |= defaultContext.inputMap;
leftTrigger |= maxByMagnitude(leftTrigger, defaultContext.leftTrigger);
rightTrigger |= maxByMagnitude(rightTrigger, defaultContext.rightTrigger);
leftStickX |= maxByMagnitude(leftStickX, defaultContext.leftStickX);
leftStickY |= maxByMagnitude(leftStickY, defaultContext.leftStickY);
rightStickX |= maxByMagnitude(rightStickX, defaultContext.rightStickX);
rightStickY |= maxByMagnitude(rightStickY, defaultContext.rightStickY);
}
if (originalContext.mouseEmulationActive) {
int changedMask = inputMap ^ originalContext.mouseEmulationLastInputMap;
boolean aDown = (inputMap & ControllerPacket.A_FLAG) != 0;
boolean bDown = (inputMap & ControllerPacket.B_FLAG) != 0;
originalContext.mouseEmulationLastInputMap = inputMap;
if ((changedMask & ControllerPacket.A_FLAG) != 0) {
if (aDown) {
conn.sendMouseButtonDown(MouseButtonPacket.BUTTON_LEFT);
}
else {
conn.sendMouseButtonUp(MouseButtonPacket.BUTTON_LEFT);
}
}
if ((changedMask & ControllerPacket.B_FLAG) != 0) {
if (bDown) {
conn.sendMouseButtonDown(MouseButtonPacket.BUTTON_RIGHT);
}
else {
conn.sendMouseButtonUp(MouseButtonPacket.BUTTON_RIGHT);
}
}
conn.sendControllerInput(controllerNumber,
(short)0, (byte)0, (byte)0, (short)0, (short)0, (short)0, (short)0);
}
else {
conn.sendControllerInput(controllerNumber, inputMap,
leftTrigger, rightTrigger,
leftStickX, leftStickY,
rightStickX, rightStickY);
}
}
// Return a valid keycode, 0 to consume, or -1 to not consume the event
@@ -464,6 +604,30 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
return KeyEvent.KEYCODE_BUTTON_START;
}
}
else if (context.isXboxBtController) {
switch (event.getScanCode()) {
case 306:
return KeyEvent.KEYCODE_BUTTON_X;
case 307:
return KeyEvent.KEYCODE_BUTTON_Y;
case 308:
return KeyEvent.KEYCODE_BUTTON_L1;
case 309:
return KeyEvent.KEYCODE_BUTTON_R1;
case 310:
return KeyEvent.KEYCODE_BUTTON_SELECT;
case 311:
return KeyEvent.KEYCODE_BUTTON_START;
case 312:
return KeyEvent.KEYCODE_BUTTON_THUMBL;
case 313:
return KeyEvent.KEYCODE_BUTTON_THUMBR;
case 139:
return KeyEvent.KEYCODE_BUTTON_MODE;
default:
// Other buttons are mapped correctly
}
}
if (context.hatXAxis != -1 && context.hatYAxis != -1) {
switch (event.getKeyCode()) {
@@ -637,6 +801,46 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
return true;
}
private short scaleRawStickAxis(float stickValue) {
return (short)Math.pow(stickValue, 3);
}
private void sendEmulatedMouseEvent(short x, short y) {
Vector2d vector = new Vector2d();
vector.initialize(x, y);
vector.scalarMultiply(1 / 32766.0f);
vector.scalarMultiply(4);
if (vector.getMagnitude() > 0) {
// Move faster as the stick is pressed further from center
vector.scalarMultiply(Math.pow(vector.getMagnitude(), 2));
if (vector.getMagnitude() >= 1) {
conn.sendMouseMove((short)vector.getX(), (short)-vector.getY());
}
}
}
private void toggleMouseEmulation(final GenericControllerContext context) {
if (context.mouseEmulationTimer != null) {
context.mouseEmulationTimer.cancel();
context.mouseEmulationTimer = null;
}
context.mouseEmulationActive = !context.mouseEmulationActive;
Toast.makeText(activityContext, "Mouse emulation is: " + (context.mouseEmulationActive ? "ON" : "OFF"), Toast.LENGTH_SHORT).show();
if (context.mouseEmulationActive) {
context.mouseEmulationTimer = new Timer();
context.mouseEmulationTimer.schedule(new TimerTask() {
@Override
public void run() {
// Send mouse movement events from analog sticks
sendEmulatedMouseEvent(context.leftStickX, context.leftStickY);
sendEmulatedMouseEvent(context.rightStickX, context.rightStickY);
}
}, 50, 50);
}
}
public boolean handleButtonUp(KeyEvent event) {
InputDeviceContext context = getContextForDevice(event.getDevice());
@@ -663,10 +867,8 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
break;
case KeyEvent.KEYCODE_BUTTON_START:
case KeyEvent.KEYCODE_MENU:
if (SystemClock.uptimeMillis() - context.startDownTime > ControllerHandler.START_DOWN_TIME_KEYB_MS) {
// FIXME: The stock keyboard doesn't have controller focus so isn't usable. I'm not enabling this shortcut
// until we have a custom keyboard or some other fix
//gestures.showKeyboard();
if (SystemClock.uptimeMillis() - context.startDownTime > ControllerHandler.START_DOWN_TIME_MOUSE_MODE_MS) {
toggleMouseEmulation(context);
}
context.inputMap &= ~ControllerPacket.PLAY_FLAG;
break;
@@ -933,6 +1135,10 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
public short rightStickY = 0x0000;
public short leftStickX = 0x0000;
public short leftStickY = 0x0000;
public boolean mouseEmulationActive;
public Timer mouseEmulationTimer;
public short mouseEmulationLastInputMap;
}
class InputDeviceContext extends GenericControllerContext {
@@ -953,6 +1159,7 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
public boolean isDualShock4;
public boolean isXboxController;
public boolean isXboxBtController;
public boolean isServal;
public boolean backIsStart;
public boolean modeIsSelect;
@@ -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 (EvdevCaptureProvider.isCaptureProviderSupported()) {
LimeLog.info("Using Evdev mouse capture");
return new EvdevCaptureProvider(activity, rootListener);
}
else if (AndroidCaptureProvider.isCaptureProviderSupported()) {
LimeLog.info("Using Android N+ native mouse capture");
return new AndroidCaptureProvider(activity);
}
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,56 @@ public class XboxOneController {
rightStickX = buffer.getShort() / 32767.0f;
rightStickY = ~buffer.getShort() / 32767.0f;
reportInput();
}
private void processPacket(ByteBuffer buffer) {
private void ackModeReport(byte seqNum) {
byte[] payload = {0x01, 0x20, seqNum, 0x09, 0x00, 0x07, 0x20, 0x02,
0x00, 0x00, 0x00, 0x00, 0x00};
connection.bulkTransfer(outEndpt, payload, payload.length, 3000);
}
@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);
// The Xbox One S controller needs acks for mode reports otherwise
// it retransmits them forever.
if (buffer.get() == 0x30) {
ackModeReport(buffer.get());
buffer.position(buffer.position() + 1);
}
else {
buffer.position(buffer.position() + 2);
}
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 +117,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.ic_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;
@@ -28,7 +26,7 @@ public class AppGridAdapter extends GenericGridAdapter<AppView.AppObject> {
private final CachedAppAssetLoader loader;
public AppGridAdapter(Activity activity, boolean listMode, boolean small, ComputerDetails computer, String uniqueId) {
super(activity, listMode ? R.layout.simple_row : (small ? R.layout.app_grid_item_small : R.layout.app_grid_item), R.drawable.image_loading);
super(activity, listMode ? R.layout.simple_row : (small ? R.layout.app_grid_item_small : R.layout.app_grid_item));
int dpi = activity.getResources().getDisplayMetrics().densityDpi;
int dp;
@@ -53,9 +51,7 @@ public class AppGridAdapter extends GenericGridAdapter<AppView.AppObject> {
this.loader = new CachedAppAssetLoader(computer, scalingDivisor,
new NetworkAssetLoader(context, uniqueId),
new MemoryAssetLoader(),
new DiskAssetLoader(context.getCacheDir()),
BitmapFactory.decodeResource(activity.getResources(),
R.drawable.image_loading, options));
new DiskAssetLoader(context.getCacheDir()));
}
public void cancelQueuedOperations() {
@@ -105,11 +101,16 @@ public class AppGridAdapter extends GenericGridAdapter<AppView.AppObject> {
public boolean populateOverlayView(ImageView overlayView, AppView.AppObject obj) {
if (obj.app.getIsRunning()) {
// Show the play button overlay
overlayView.setImageResource(R.drawable.play);
overlayView.setImageResource(R.drawable.ic_play);
return true;
}
// No overlay
return false;
}
@Override
public boolean shouldShowProgressBar(AppView.AppObject obj) {
return true;
}
}
@@ -6,6 +6,7 @@ import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.ImageView;
import android.widget.ProgressBar;
import android.widget.TextView;
import com.limelight.R;
@@ -14,15 +15,13 @@ import java.util.ArrayList;
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) {
public GenericGridAdapter(Context context, int layoutId) {
this.context = context;
this.layoutId = layoutId;
this.defaultImageRes = defaultImageRes;
this.inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
}
@@ -49,6 +48,7 @@ public abstract class GenericGridAdapter<T> extends BaseAdapter {
public abstract boolean populateImageView(ImageView imgView, T obj);
public abstract boolean populateTextView(TextView txtView, T obj);
public abstract boolean populateOverlayView(ImageView overlayView, T obj);
public abstract boolean shouldShowProgressBar(T obj);
@Override
public View getView(int i, View convertView, ViewGroup viewGroup) {
@@ -59,10 +59,19 @@ public abstract class GenericGridAdapter<T> extends BaseAdapter {
ImageView imgView = (ImageView) convertView.findViewById(R.id.grid_image);
ImageView overlayView = (ImageView) convertView.findViewById(R.id.grid_overlay);
TextView txtView = (TextView) convertView.findViewById(R.id.grid_text);
ProgressBar prgView = (ProgressBar) convertView.findViewById(R.id.grid_spinner);
if (prgView != null) {
if (shouldShowProgressBar(itemList.get(i))) {
prgView.setVisibility(View.VISIBLE);
}
else {
prgView.setVisibility(View.INVISIBLE);
}
}
if (imgView != null) {
if (!populateImageView(imgView, itemList.get(i))) {
imgView.setImageResource(defaultImageRes);
imgView.setImageBitmap(null);
}
}
if (!populateTextView(txtView, itemList.get(i))) {
@@ -14,7 +14,7 @@ import java.util.Comparator;
public class PcGridAdapter extends GenericGridAdapter<PcView.ComputerObject> {
public PcGridAdapter(Context context, boolean listMode, boolean small) {
super(context, listMode ? R.layout.simple_row : (small ? R.layout.pc_grid_item_small : R.layout.pc_grid_item), R.drawable.computer);
super(context, listMode ? R.layout.simple_row : (small ? R.layout.pc_grid_item_small : R.layout.pc_grid_item));
}
public void addComputer(PcView.ComputerObject computer) {
@@ -37,20 +37,20 @@ public class PcGridAdapter extends GenericGridAdapter<PcView.ComputerObject> {
@Override
public boolean populateImageView(ImageView imgView, PcView.ComputerObject obj) {
if (obj.details.reachability != ComputerDetails.Reachability.OFFLINE) {
if (obj.details.state == ComputerDetails.State.ONLINE) {
imgView.setAlpha(1.0f);
}
else {
imgView.setAlpha(0.4f);
}
// Return false to use the default drawable
return false;
imgView.setImageResource(R.drawable.ic_computer);
return true;
}
@Override
public boolean populateTextView(TextView txtView, PcView.ComputerObject obj) {
if (obj.details.reachability != ComputerDetails.Reachability.OFFLINE) {
if (obj.details.state == ComputerDetails.State.ONLINE) {
txtView.setAlpha(1.0f);
}
else {
@@ -63,13 +63,17 @@ public class PcGridAdapter extends GenericGridAdapter<PcView.ComputerObject> {
@Override
public boolean populateOverlayView(ImageView overlayView, PcView.ComputerObject obj) {
if (obj.details.reachability == ComputerDetails.Reachability.UNKNOWN) {
// Still refreshing this PC so display the overlay
overlayView.setImageResource(R.drawable.image_loading);
if (obj.details.state == ComputerDetails.State.OFFLINE) {
overlayView.setImageResource(R.drawable.ic_pc_offline);
overlayView.setAlpha(0.4f);
return true;
}
// No overlay
return false;
}
@Override
public boolean shouldShowProgressBar(PcView.ComputerObject obj) {
// Still refreshing this PC so display the progress bar
return obj.details.reachability == ComputerDetails.Reachability.UNKNOWN;
}
}
@@ -53,13 +53,13 @@ public class CachedAppAssetLoader {
public CachedAppAssetLoader(ComputerDetails computer, double scalingDivider,
NetworkAssetLoader networkLoader, MemoryAssetLoader memoryLoader,
DiskAssetLoader diskLoader, Bitmap placeholderBitmap) {
DiskAssetLoader diskLoader) {
this.computer = computer;
this.scalingDivider = scalingDivider;
this.networkLoader = networkLoader;
this.memoryLoader = memoryLoader;
this.diskLoader = diskLoader;
this.placeholderBitmap = placeholderBitmap;
this.placeholderBitmap = Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888);
}
public void cancelBackgroundLoads() {
@@ -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() {
@@ -280,14 +280,14 @@ public class CachedAppAssetLoader {
});
}
public void populateImageView(NvApp app, ImageView view) {
public boolean populateImageView(NvApp app, ImageView view) {
LoaderTuple tuple = new LoaderTuple(computer, app);
// If there's already a task in progress for this view,
// cancel it. If the task is already loading the same image,
// we return and let that load finish.
if (!cancelPendingLoad(tuple, view)) {
return;
return true;
}
// First, try the memory cache in the current context
@@ -296,7 +296,7 @@ public class CachedAppAssetLoader {
// Show the bitmap immediately
view.setAlpha(1.0f);
view.setImageBitmap(bmp);
return;
return true;
}
// If it's not in memory, create an async task to load it. This task will be attached
@@ -308,6 +308,7 @@ public class CachedAppAssetLoader {
// Run the task on our foreground executor
task.executeOnExecutor(foregroundExecutor, tuple);
return false;
}
public class LoaderTuple {
@@ -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);
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

+9
View File
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="256dp"
android:height="256dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM17,13h-4v4h-2v-4L7,13v-2h4L11,7h2v4h4v2z"
android:fillColor="#FFFFFF"/>
</vector>
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="512dp"
android:height="512dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:pathData="M21,2L3,2c-1.1,0 -2,0.9 -2,2v12c0,1.1 0.9,2 2,2h7v2L8,20v2h8v-2h-2v-2h7c1.1,0 2,-0.9 2,-2L23,4c0,-1.1 -0.9,-2 -2,-2zM21,16L3,16L3,4h18v12z"
android:fillColor="#FFFFFF"/>
</vector>
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="128dp"
android:height="128dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:pathData="M23.64,7c-0.45,-0.34 -4.93,-4 -11.64,-4 -1.5,0 -2.89,0.19 -4.15,0.48L18.18,13.8 23.64,7zM17.04,15.22L3.27,1.44 2,2.72l2.05,2.06C1.91,5.76 0.59,6.82 0.36,7l11.63,14.49 0.01,0.01 0.01,-0.01 3.9,-4.86 3.32,3.32 1.27,-1.27 -3.46,-3.46z"
android:fillColor="#FFFFFF"/>
</vector>
+9
View File
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="128dp"
android:height="128dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM10,16.5v-9l6,4.5 -6,4.5z"
android:fillColor="#FFFFFF"/>
</vector>
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="256dp"
android:height="256dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:pathData="M19.43,12.98c0.04,-0.32 0.07,-0.64 0.07,-0.98s-0.03,-0.66 -0.07,-0.98l2.11,-1.65c0.19,-0.15 0.24,-0.42 0.12,-0.64l-2,-3.46c-0.12,-0.22 -0.39,-0.3 -0.61,-0.22l-2.49,1c-0.52,-0.4 -1.08,-0.73 -1.69,-0.98l-0.38,-2.65C14.46,2.18 14.25,2 14,2h-4c-0.25,0 -0.46,0.18 -0.49,0.42l-0.38,2.65c-0.61,0.25 -1.17,0.59 -1.69,0.98l-2.49,-1c-0.23,-0.09 -0.49,0 -0.61,0.22l-2,3.46c-0.13,0.22 -0.07,0.49 0.12,0.64l2.11,1.65c-0.04,0.32 -0.07,0.65 -0.07,0.98s0.03,0.66 0.07,0.98l-2.11,1.65c-0.19,0.15 -0.24,0.42 -0.12,0.64l2,3.46c0.12,0.22 0.39,0.3 0.61,0.22l2.49,-1c0.52,0.4 1.08,0.73 1.69,0.98l0.38,2.65c0.03,0.24 0.24,0.42 0.49,0.42h4c0.25,0 0.46,-0.18 0.49,-0.42l0.38,-2.65c0.61,-0.25 1.17,-0.59 1.69,-0.98l2.49,1c0.23,0.09 0.49,0 0.61,-0.22l2,-3.46c0.12,-0.22 0.07,-0.49 -0.12,-0.64l-2.11,-1.65zM12,15.5c-1.93,0 -3.5,-1.57 -3.5,-3.5s1.57,-3.5 3.5,-3.5 3.5,1.57 3.5,3.5 -1.57,3.5 -3.5,3.5z"
android:fillColor="#FFFFFF"/>
</vector>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

@@ -15,18 +15,18 @@
android:layout_centerInParent="true"
android:layout_centerVertical="true"
android:layout_centerHorizontal="true">
<ImageView
<ProgressBar
android:id="@+id/pcs_loading"
android:layout_width="75dp"
android:layout_height="75dp"
android:src="@drawable/image_loading"/>
android:indeterminate="true"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_toRightOf="@+id/pcs_loading"
android:layout_toEndOf="@+id/pcs_loading"
android:layout_marginLeft="5dp"
android:layout_marginStart="5dp"
android:layout_marginLeft="10dp"
android:layout_marginStart="10dp"
android:layout_centerVertical="true"
android:textAppearance="?android:attr/textAppearanceLarge"
android:gravity="center"
@@ -55,7 +55,7 @@
android:layout_alignParentStart="true"
android:layout_alignParentLeft="true"
android:layout_alignParentTop="true"
android:src="@drawable/settings"
android:src="@drawable/ic_settings"
style="?android:attr/borderlessButtonStyle"/>
<ImageButton
@@ -68,7 +68,7 @@
android:layout_alignParentTop="true"
android:layout_alignParentRight="true"
android:layout_alignParentEnd="true"
android:src="@drawable/add_computer"
android:src="@drawable/ic_add"
style="?android:attr/borderlessButtonStyle"/>
</RelativeLayout>
@@ -15,18 +15,18 @@
android:layout_centerInParent="true"
android:layout_centerVertical="true"
android:layout_centerHorizontal="true">
<ImageView
<ProgressBar
android:id="@+id/pcs_loading"
android:layout_width="75dp"
android:layout_height="75dp"
android:src="@drawable/image_loading"/>
android:indeterminate="true"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_toRightOf="@+id/pcs_loading"
android:layout_toEndOf="@+id/pcs_loading"
android:layout_marginLeft="5dp"
android:layout_marginStart="5dp"
android:layout_marginLeft="10dp"
android:layout_marginStart="10dp"
android:layout_centerVertical="true"
android:textAppearance="?android:attr/textAppearanceLarge"
android:gravity="center"
@@ -54,7 +54,7 @@
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_alignParentTop="true"
android:src="@drawable/settings"
android:src="@drawable/ic_settings"
style="?android:attr/borderlessButtonStyle"/>
<ImageButton
@@ -66,7 +66,7 @@
android:layout_alignParentTop="true"
android:layout_alignParentEnd="true"
android:layout_alignParentRight="true"
android:src="@drawable/add_computer"
android:src="@drawable/ic_add"
style="?android:attr/borderlessButtonStyle"/>
</RelativeLayout>
@@ -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"
@@ -8,6 +8,14 @@
android:layout_centerHorizontal="true"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<ProgressBar
android:id="@+id/grid_spinner"
android:layout_centerHorizontal="true"
android:layout_centerVertical="true"
android:layout_width="75dp"
android:layout_height="75dp"
android:indeterminate="true">
</ProgressBar>
<ImageView
android:id="@+id/grid_image"
android:cropToPadding="false"
@@ -8,6 +8,14 @@
android:layout_centerHorizontal="true"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<ProgressBar
android:id="@+id/grid_spinner"
android:layout_centerHorizontal="true"
android:layout_centerVertical="true"
android:layout_width="50dp"
android:layout_height="50dp"
android:indeterminate="true">
</ProgressBar>
<ImageView
android:id="@+id/grid_image"
android:cropToPadding="false"
+13 -8
View File
@@ -10,23 +10,28 @@
<ImageView
android:id="@+id/grid_image"
android:layout_centerHorizontal="true"
android:layout_width="150dp"
android:layout_height="100dp">
android:layout_width="125dp"
android:layout_height="125dp">
</ImageView>
<ImageView
android:id="@+id/grid_overlay"
android:layout_marginTop="15dp"
android:layout_marginLeft="65dp"
android:layout_marginStart="65dp"
android:layout_marginRight="20dp"
android:layout_marginEnd="20dp"
android:layout_centerHorizontal="true"
android:layout_marginTop="28dp"
android:layout_width="50dp"
android:layout_height="50dp">
</ImageView>
<ProgressBar
android:id="@+id/grid_spinner"
android:layout_centerHorizontal="true"
android:layout_marginTop="28dp"
android:layout_width="50dp"
android:layout_height="50dp"
android:indeterminate="true">
</ProgressBar>
</RelativeLayout>
<TextView
android:id="@+id/grid_text"
android:layout_width="150dp"
android:layout_width="125dp"
android:layout_height="wrap_content"
android:layout_below="@id/grid_image_layout"
android:layout_marginTop="10dp"
+13 -8
View File
@@ -10,23 +10,28 @@
<ImageView
android:id="@+id/grid_image"
android:layout_centerHorizontal="true"
android:layout_width="100dp"
android:layout_height="67dp">
android:layout_width="75dp"
android:layout_height="75dp">
</ImageView>
<ImageView
android:id="@+id/grid_overlay"
android:layout_marginTop="10dp"
android:layout_marginLeft="42dp"
android:layout_marginStart="42dp"
android:layout_marginRight="13dp"
android:layout_marginEnd="13dp"
android:layout_centerHorizontal="true"
android:layout_marginTop="16dp"
android:layout_width="33dp"
android:layout_height="33dp">
</ImageView>
<ProgressBar
android:id="@+id/grid_spinner"
android:layout_centerHorizontal="true"
android:layout_marginTop="16dp"
android:layout_width="33dp"
android:layout_height="33dp"
android:indeterminate="true">
</ProgressBar>
</RelativeLayout>
<TextView
android:id="@+id/grid_text"
android:layout_width="100dp"
android:layout_width="75dp"
android:layout_height="wrap_content"
android:layout_below="@id/grid_image_layout"
android:layout_marginTop="10dp"
-9
View File
@@ -1,14 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string-array name="resolution_names">
<item>720p - 30 FPS</item>
<item>720p - 60 FPS</item>
<item>1080p - 30 FPS</item>
<item>1080p - 60 FPS</item>
<item>4K - 30 FPS</item>
<item>4K - 60 FPS</item>
</string-array>
<string-array name="decoder_names">
<item>Scegli decoder automaticamente</item>
<item>Forza decoder software</item>
+15
View File
@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string-array name="decoder_names">
<item>デコーダを自動選択</item>
<item>ソフトウェアデコードを強制</item>
<item>ハードウェアデコードを強制</item>
</string-array>
<string-array name="video_format_names">
<item>安定している場合にH.265を有効化</item>
<item>H.265を強制的に有効化(クラッシュする可能性があります)</item>
<item>H.265を無効化</item>
</string-array>
</resources>
+127
View File
@@ -0,0 +1,127 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- PC view menu entries -->
<string name="pcview_menu_app_list">ゲームリストを表示</string>
<string name="pcview_menu_pair_pc">コンピュータとペアリング</string>
<string name="pcview_menu_unpair_pc">ペアリングを解除r</string>
<string name="pcview_menu_send_wol">Wake-On-LANリクエストを送信する</string>
<string name="pcview_menu_delete_pc">コンピュータをリストから削除する</string>
<!-- Pair messages -->
<string name="pairing">ペアリング中</string>
<string name="pair_pc_offline">コンピュータはオフラインです</string>
<string name="pair_pc_ingame">現在このコンピュータはゲームを実行中のため、ペアリングする前にゲームを閉じる必要があります</string>
<string name="pair_pairing_title">ペアリング</string>
<string name="pair_pairing_msg">ペアリングしたいコンピュータのPINコードを入力してください:</string>
<string name="pair_incorrect_pin">PINコードが間違っています</string>
<string name="pair_fail">ペアリングに失敗しました</string>
<!-- WOL messages -->
<string name="wol_pc_online">コンピュータはオンラインです</string>
<string name="wol_no_mac">GFEがMACアドレスを送信しなかったため、コンピュータを起動することができません</string>
<string name="wol_waking_pc">コンピュータを起動中</string>
<string name="wol_waking_msg">コンピュータが起動するまで数秒かかる可能性があります。Wake-On-LANが正しく構成されているか確認してください</string>
<string name="wol_fail">Wake-On-LANパケットの送信に失敗しました</string>
<!-- Unpair messages -->
<string name="unpairing">ペアリングの解除中</string>
<string name="unpair_success">ペアリングの解除に成功しました</string>
<string name="unpair_fail">ペアリングの解除に失敗しました</string>
<string name="unpair_error">このデバイスはペアリングされませんでした</string>
<!-- Errors -->
<string name="error_pc_offline">コンピュータはオフラインです</string>
<string name="error_manager_not_running">ComputerManagerサービスが動作していません。しばらく待つか、Moonlightを再起動してください</string>
<string name="error_unknown_host">ホストが解決できません</string>
<string name="error_404">GFEがHTTP 404を返しました。コンピュータでサポートされているGPUが動作しているか確認してください。リモートデスクトップソフトウェアでもこのエラーが発生する可能性があります。コンピュータを再起動するか、GFEを再インストールしてください</string>
<!-- Start application messages -->
<string name="conn_establishing_title">接続を確立</string>
<string name="conn_establishing_msg">接続を開始</string>
<string name="conn_metered">警告: 現在使用中のネットワークは従量制課金接続です</string>
<string name="conn_client_latency"> デコードの平均待ち時間:</string>
<string name="conn_client_latency_hw">ハードウェアデコーダーの待ち時間</string>
<string name="conn_hardware_latency">ハードウェアデコーダーの平均待ち時間:</string>
<string name="conn_starting">開始</string>
<string name="conn_error_title">接続エラー</string>
<string name="conn_error_msg">開始できません</string>
<string name="conn_terminated_title">接続を終了</string>
<string name="conn_terminated_msg">接続は終了しました</string>
<!-- General strings -->
<string name="ip_hint">コンピュータのIPアドレス</string>
<string name="searching_pc">コンピュータを検索中</string>
<string name="yes">はい</string>
<string name="no">いいえ</string>
<string name="lost_connection">コンピュータとの接続が失われました</string>
<!-- AppList activity -->
<string name="title_applist">ゲーム</string>
<string name="applist_menu_resume">セッションを続ける</string>
<string name="applist_menu_quit">セッションを終了する</string>
<string name="applist_menu_quit_and_start">現在のゲームを終了して新しいゲームを始める</string>
<string name="applist_menu_cancel">キャンセル</string>
<string name="applist_refresh_title">ゲームリスト</string>
<string name="applist_refresh_msg">ゲームリストを更新中</string>
<string name="applist_refresh_error_title">エラー</string>
<string name="applist_refresh_error_msg">ゲームリストの取得に失敗しました</string>
<string name="applist_quit_app">終了中</string>
<string name="applist_quit_success">ゲームを終了しました</string>
<string name="applist_quit_fail">ゲームが終了できませんでした</string>
<string name="applist_quit_confirmation">本当にゲームを終了しますか? 保存されていないデータは破棄されます</string>
<!-- Add computer manually activity -->
<string name="title_add_pc">手動でコンピュータを追加する</string>
<string name="msg_add_pc">コンピュータに接続中</string>
<string name="addpc_fail">指定されたコンピュータに接続できませんでした。ファイアーウォールの設定を確認してください</string>
<string name="addpc_success">コンピュータの追加に成功しました</string>
<string name="addpc_unknown_host">コンピュータのアドレスが解決できません。コンピュータのアドレスを確認してください</string>
<string name="addpc_enter_ip">IPアドレスを入力してください</string>
<!-- Preferences -->
<string name="category_basic_settings">基本的な設定</string>
<string name="title_resolution_list">解像度とフレームレート</string>
<string name="summary_resolution_list">品質が高いほどラグとクラッシュが発生しやすくなります</string>
<string name="title_seekbar_bitrate">映像のビットレート</string>
<string name="summary_seekbar_bitrate">ビットレートを低くすればカクつきが抑制され、高くすれば画質が向上します</string>
<string name="suffix_seekbar_bitrate">Mbps</string>
<string name="title_checkbox_stretch_video">映像を全画面に拡大</string>
<string name="title_checkbox_disable_warnings">警告を無効化</string>
<string name="summary_checkbox_disable_warnings">ストリーミング中に画面に警告メッセージを表示しない</string>
<string name="category_audio_settings">音声</string>
<string name="title_checkbox_51_surround">5.1chサラウンド</string>
<string name="summary_checkbox_51_surround">音声に問題が生じる場合はチェックを外してください。バージョン2.7以降のGFEが必要です</string>
<string name="category_gamepad_settings">ゲームコントローラ</string>
<string name="title_checkbox_multi_controller">複数のゲームコントローラ</string>
<string name="summary_checkbox_multi_controller">チェックを外すと、全てのゲームコントローラが単一の物として認識されます</string>
<string name="title_seekbar_deadzone">アナログゲームコントローラのデッドゾーン</string>
<string name="suffix_seekbar_deadzone">%</string>
<string name="title_checkbox_xb1_driver">Xbox 360/Oneコントローラ</string>
<string name="summary_checkbox_xb1_driver">XboxゲームコントローラをサポートしないデバイスでXboxゲームコントローラ用のドライバを有効にします</string>
<string name="category_on_screen_controls_settings">オンスクリーンコントローラ</string>
<string name="title_checkbox_show_onscreen_controls">オンスクリーンコントローラ</string>
<string name="summary_checkbox_show_onscreen_controls">タッチスクリーン上にオンスクリーンコントローラを表示します</string>
<string name="category_ui_settings">インターフェース</string>
<string name="title_language_list">言語</string>
<string name="summary_language_list">Moonlightで使用する言語</string>
<string name="title_checkbox_list_mode">リストメニュー</string>
<string name="summary_checkbox_list_mode">ゲームをグリッドではなく、リストで表示します</string>
<string name="title_checkbox_small_icon_mode">小さなアイコン</string>
<string name="summary_checkbox_small_icon_mode">グリッドで小さなアイコンを使用します</string>
<string name="category_host_settings">ホスト</string>
<string name="title_checkbox_enable_sops">ゲーム設定の最適化</string>
<string name="summary_checkbox_enable_sops">GFEがゲームの設定を最適化します</string>
<string name="title_checkbox_host_audio">コンピュータで音声を再生</string>
<string name="summary_checkbox_host_audio">音声をコンピュータとこのデバイスの両方で再生します</string>
<string name="category_advanced_settings">高度な設定</string>
<string name="title_video_format">H.265</string>
<string name="summary_video_format">H.265は動画に必要な帯域幅を圧縮します。この機能にはなるべく新しいデバイスが必要です</string>
</resources>
+8
View File
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string-array name="decoder_names">
<item>Selecteer Decoder Automatisch</item>
<item>Forceer Software Decoderen</item>
<item>Forceer Hardware Decoderen</item>
</string-array>
</resources>
+120
View File
@@ -0,0 +1,120 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- PC view menu entries -->
<string name="pcview_menu_app_list">Toon Spel Lijst</string>
<string name="pcview_menu_pair_pc">Koppel met PC</string>
<string name="pcview_menu_unpair_pc">Ontkoppen</string>
<string name="pcview_menu_send_wol">Stuur Wake-On-LAN aanvraag</string>
<string name="pcview_menu_delete_pc">Verwijder PC</string>
<!-- Pair messages -->
<string name="pairing">Koppelen…</string>
<string name="pair_pc_offline">Computer is onbereikbaar</string>
<string name="pair_pc_ingame">Computer is op dit moment in een spel. Sluit de spel voordat je koppelt.</string>
<string name="pair_pairing_title">Koppelen</string>
<string name="pair_pairing_msg">Voer de volgende PIN in op de doel PC:</string>
<string name="pair_incorrect_pin">Ongeldige PIN</string>
<string name="pair_fail">Koppelen mislukt</string>
<!-- WOL messages -->
<string name="wol_pc_online">Computer is online</string>
<string name="wol_no_mac">Het is niet mogelijk om de PC uit slaapstand te halen, omdat GFE geen MAC adres heeft verstuurd.</string>
<string name="wol_waking_pc">PC uit slaapstand halen…</string>
<string name="wol_waking_msg"> Het ken even duren voordat de PC reageert.
Als dit niet gebeurt dan moet je de Wake-On-LAN instellingen controleren.
</string>
<string name="wol_fail">Wake-On-LAN packets versturen mislukt</string>
<!-- Unpair messages -->
<string name="unpairing">Ontkoppelen…</string>
<string name="unpair_success">Ontkoppeling succesvol</string>
<string name="unpair_fail">Ontkoppeling mislukt</string>
<string name="unpair_error">Apparaat niet gekoppeld</string>
<!-- Errors -->
<string name="error_pc_offline">Computer is offline</string>
<string name="error_manager_not_running">The ComputerManager service is niet gestart. Wacht een ogenblik of herstart de app.</string>
<string name="error_unknown_host">Host achterhalen mislukt.</string>
<string name="error_404">GFE geeft een HTTP 404 fout. Kijk na of de PC een ondersteunde GPU heeft.
Remote desktop software kan soms ook voor deze fout zorgen. Probeer de computer opnieuw op te starten of GFE te herinstalleren.
</string>
<!-- Start application messages -->
<string name="conn_establishing_title">Verbinding maken</string>
<string name="conn_establishing_msg">Verbinding starten</string>
<string name="conn_metered">Waarschuwing: Dit internet verbinding bevat een datalimiet!</string>
<string name="conn_client_latency">Gemiddelde frame decoding reactietijd:</string>
<string name="conn_client_latency_hw">hardware decoder reactietijd:</string>
<string name="conn_hardware_latency">Gemiddelde hardware decoding reactietijd:</string>
<string name="conn_starting">Starten</string>
<string name="conn_error_title">Verbindingsprobleem</string>
<string name="conn_error_msg">Starten mislukt</string>
<string name="conn_terminated_title">Verbinding beëindigd</string>
<string name="conn_terminated_msg">De verbinding is beëindigd</string>
<!-- General strings -->
<string name="ip_hint">IP adres van GeForce PC</string>
<string name="searching_pc">Zoeken naar PCs…</string>
<string name="yes">Ja</string>
<string name="no">Nee</string>
<string name="lost_connection">Verbinding met PC verloren</string>
<!-- AppList activity -->
<string name="title_applist">Apps op</string>
<string name="applist_menu_resume">Hervat Sessie</string>
<string name="applist_menu_quit">Stop Sessie</string>
<string name="applist_menu_quit_and_start">Stop Huidige Spel en Start</string>
<string name="applist_menu_cancel">Annuleer</string>
<string name="applist_refresh_title">App Lijst</string>
<string name="applist_refresh_msg">Apps Verversen…</string>
<string name="applist_refresh_error_title">Fout</string>
<string name="applist_refresh_error_msg">App lijst ophalen mislukt</string>
<string name="applist_quit_app">Stoppen</string>
<string name="applist_quit_success">Succesvol gestopt.</string>
<string name="applist_quit_fail">Stoppen mislukt</string>
<string name="applist_quit_confirmation">Weet je het zeker dat je de app wilt sluiten? Alle niet-opgeslagen gegevens gaan verloren.</string>
<!-- Add computer manually activity -->
<string name="title_add_pc">Voeg handmatig PC toe</string>
<string name="msg_add_pc">Verbinding maken met PC…</string>
<string name="addpc_fail">Verbinden mislukt. Controleer of de benodigde poorten openstaan in de firewall.</string>
<string name="addpc_success">Computer toevoegen succesvol</string>
<string name="addpc_unknown_host">Adres achterhalen is mislukt. Controleer het ingevoerde adres op typfouten.</string>
<string name="addpc_enter_ip">IP-adres invoeren is verplicht.</string>
<!-- Preferences -->
<string name="category_basic_settings">Algemene Instellingen</string>
<string name="title_resolution_list">Selecteer resolutie en FPS doel</string>
<string name="summary_resolution_list">Instellingen te hoog zetten kunnen crashes en haperingen veroorzaken.</string>
<string name="title_seekbar_bitrate">Selecteer doel video bitsnelheid</string>
<string name="summary_seekbar_bitrate">Verlaag bitsnelheid om haperingen te verminderen. Verhoog de bitsnelheid voor een betere videokwaliteit.</string>
<string name="suffix_seekbar_bitrate">Mbps</string>
<string name="title_checkbox_stretch_video">Rek video uit tot volledig scherm</string>
<string name="title_checkbox_disable_warnings">Verberg waarschuwingsberichten</string>
<string name="summary_checkbox_disable_warnings">Verberg on-screen verbindingswaarschuwingen tijdens het streamen</string>
<string name="category_gamepad_settings">Gamepad Instellingen</string>
<string name="title_checkbox_multi_controller">Multi-gamepad support</string>
<string name="summary_checkbox_multi_controller">Wanneer uitgevinkt, alle controllers verschijnen als één.</string>
<string name="title_seekbar_deadzone">Pas analoge dodezone aan.</string>
<string name="suffix_seekbar_deadzone">%</string>
<string name="title_checkbox_xb1_driver">Xbox One controller stuurprogramma</string>
<string name="summary_checkbox_xb1_driver">Gebruikt de ingebouwde USB stuurprogramma voor apparaten zonder Xbox One controller ondersteuning.</string>
<string name="category_ui_settings">UI Installingen</string>
<string name="title_language_list">Taal</string>
<string name="summary_language_list">Taal te gebruiken in Moonlight</string>
<string name="title_checkbox_list_mode">Gebruik lijsten in plaats van kolommen</string>
<string name="summary_checkbox_list_mode">Display apps and PCs in lists instead of grids</string>
<string name="title_checkbox_small_icon_mode">Gebruik kleine iconen</string>
<string name="summary_checkbox_small_icon_mode">Gebruik kleine iconen in kolom onderdelen zodat meer items tegelijk zichtbaar worden.</string>
<string name="category_host_settings">Host Instellingen</string>
<string name="title_checkbox_enable_sops">Optimaliseer spel instellingen</string>
<string name="summary_checkbox_enable_sops">Sta GFE toe om spel instellingen te veranderen voor een optimale stream</string>
<string name="title_checkbox_host_audio">Speel audio af op PC</string>
<string name="summary_checkbox_host_audio">Speel audio af op de computer en op dit apparaat</string>
<string name="category_advanced_settings">Geavanceerde Instellingen</string>
</resources>
+14
View File
@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string-array name="decoder_names">
<item>Автоматический выбор декодера</item>
<item>Принудительное программное декодирование</item>
<item>Принудительное аппаратное декодирование</item>
</string-array>
<string-array name="video_format_names">
<item>Использовать H.265 только если безопасно</item>
<item>Всегда использовать H.265 если доступно</item>
<item>Никогда не использовать H.265</item>
</string-array>
</resources>
+127
View File
@@ -0,0 +1,127 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- PC view menu entries -->
<string name="pcview_menu_app_list">Посмотреть список игр</string>
<string name="pcview_menu_pair_pc">Создать пару с PC</string>
<string name="pcview_menu_unpair_pc">Разорвать пару</string>
<string name="pcview_menu_send_wol">Отправить Wake-On-LAN запрос</string>
<string name="pcview_menu_delete_pc">Удалить PC</string>
<!-- Pair messages -->
<string name="pairing">Создание пары…</string>
<string name="pair_pc_offline">Компьютер выключен или находится не в сети</string>
<string name="pair_pc_ingame">Компьютер в данный момент находится в игре. Вы должны закрыть игру перед создание пары.</string>
<string name="pair_pairing_title">Создание пары</string>
<string name="pair_pairing_msg">Пожалуйста введите этот PIN на PC:</string>
<string name="pair_incorrect_pin">Неправильный PIN</string>
<string name="pair_fail">Создание пары не удалось</string>
<!-- WOL messages -->
<string name="wol_pc_online">Компьютер в сети</string>
<string name="wol_no_mac">Невозможно разбудить PC потому что GFE не отправило MAC адрес</string>
<string name="wol_waking_pc">Пробуждение PC…</string>
<string name="wol_waking_msg">Пробуждение PC может занять несколько секунд.
Если этого не происходит, удостоверьтесь что Wake-On-LAN настроен правильно.
</string>
<string name="wol_fail">Ошибка при отправке Wake-On-LAN пакетов</string>
<!-- Unpair messages -->
<string name="unpairing">Разрыв пары…</string>
<string name="unpair_success">Разрыв пары закончился успешно.</string>
<string name="unpair_fail">Разрыв пары не удался</string>
<string name="unpair_error">Устройство не было спарено</string>
<!-- Errors -->
<string name="error_pc_offline">Компьютер выключен или находится не в сети</string>
<string name="error_manager_not_running">Сервис ComputerManager не запущен. Пожалуйста, подождите несколько секунд или перезапустите приложение.</string>
<string name="error_unknown_host">Не удалось найти хост</string>
<string name="error_404">GFE вернул ошибку HTTP 404. Убедитесь что ваш PC is испольщует поддерживаемый GPU.
Использование программ для удалённого доступа также можнт вызывать эту ошибку. Попробуйте перезагрузить компьютер или переустановить GFE.
</string>
<!-- Start application messages -->
<string name="conn_establishing_title">Создание соединения.</string>
<string name="conn_establishing_msg">Подключение</string>
<string name="conn_metered">Внимание: Происходит измерение вашего сетевого соединения!</string>
<string name="conn_client_latency">Средняя задержка декодирования кадра: </string>
<string name="conn_client_latency_hw">задержка аппаратного декодирования:</string>
<string name="conn_hardware_latency">Средняя задержка апаратного декодирования:</string>
<string name="conn_starting">Запуск</string>
<string name="conn_error_title">Ошибка соединения</string>
<string name="conn_error_msg">Запуск не удался</string>
<string name="conn_terminated_title">Соединение прекращено</string>
<string name="conn_terminated_msg">Подключение было прервано</string>
<!-- General strings -->
<string name="ip_hint">IP адресс компьютера с GeForce</string>
<string name="searching_pc">Поиск компьютеров…</string>
<string name="yes">Да</string>
<string name="no">Нет</string>
<string name="lost_connection">Потеряно соединение с PC</string>
<!-- AppList activity -->
<string name="title_applist">Приложения на</string>
<string name="applist_menu_resume">Возобновить сессию</string>
<string name="applist_menu_quit">Выйти из сессии</string>
<string name="applist_menu_quit_and_start">Выйти из текущей игры и запустить</string>
<string name="applist_menu_cancel">Отмена</string>
<string name="applist_refresh_title">Список приложений</string>
<string name="applist_refresh_msg">Обновление приложений…</string>
<string name="applist_refresh_error_title">Ошибка</string>
<string name="applist_refresh_error_msg">Ошибка при получении списка приложений</string>
<string name="applist_quit_app">Выход из</string>
<string name="applist_quit_success">Выход произошёл успешно из</string>
<string name="applist_quit_fail">Ошибка при выходе</string>
<string name="applist_quit_confirmation">Вы уверены, что хотите выйти из запущенного приложения? Все несохраненные данные будут потеряны.</string>
<!-- Add computer manually activity -->
<string name="title_add_pc">Добавление PC вручную</string>
<string name="msg_add_pc">Соединение с PC…</string>
<string name="addpc_fail">Не удалось подключиться к выбранному компьютеру. Удостоверьтесь, что необходимые порты разрешены в настройках брандмауэра.</string>
<string name="addpc_success">Компьютер добавлен успешно.</string>
<string name="addpc_unknown_host">Не удалось найти PC по указанному адресу. Убедитесь, что вы не совершили ошибок во время его написания.</string>
<string name="addpc_enter_ip">Вы должны ввести IP адрес</string>
<!-- Preferences -->
<string name="category_basic_settings">Базовые Настройки</string>
<string name="title_resolution_list">Выберите разрешение и частоту кадров.</string>
<string name="summary_resolution_list">Выбор слишком высокого значеня для своего устройства может вызвать тормоза или вылеты.</string>
<string name="title_seekbar_bitrate">Выберите битрейт видео.</string>
<string name="summary_seekbar_bitrate">Низкий битрейт уменьшит зависания. Увеличение битрейта улучшит качество изображения.</string>
<string name="suffix_seekbar_bitrate">Mbps</string>
<string name="title_checkbox_stretch_video">Растягивать видео на весь экран</string>
<string name="title_checkbox_disable_warnings">Отключить сообщения с предупреждениями</string>
<string name="summary_checkbox_disable_warnings">Выключить экранные предупреждения о соединении во время стрима.</string>
<string name="category_audio_settings">Аудио Настройки</string>
<string name="title_checkbox_51_surround">Включить объёмный звук 5.1</string>
<string name="summary_checkbox_51_surround">Отключите, если появляются аудио проблемы. Требуется GFE 2.7 или выше.</string>
<string name="category_gamepad_settings">Настройки Гемпада</string>
<string name="title_checkbox_multi_controller">Поддержка нескольких контроллеров</string>
<string name="summary_checkbox_multi_controller">Когда отключена, все контроллеры определяются как один. </string>
<string name="title_seekbar_deadzone">Регулировать мертвую зону аналогового стика.</string>
<string name="suffix_seekbar_deadzone">%</string>
<string name="title_checkbox_xb1_driver">Драйвер контроллера от Xbox One</string>
<string name="summary_checkbox_xb1_driver">Включить встроенный USB драйвер для устройств без встроенной поддержки контроллера от Xbox One.</string>
<string name="category_ui_settings">Настройки Интерфейса</string>
<string name="title_language_list">Язык</string>
<string name="summary_language_list">Язык, который будет использоваться в Moonlight</string>
<string name="title_checkbox_list_mode">Использовать списки вместо сеток.</string>
<string name="summary_checkbox_list_mode">Выводить приложения и компьютеры списком, вместо использования сетки.</string>
<string name="title_checkbox_small_icon_mode">Использовать маленькие иконки</string>
<string name="summary_checkbox_small_icon_mode">Использовать маленькие иконки в сетки для увеличения числа элементов, отображаемых на экране.</string>
<string name="category_host_settings">Настройки Хоста</string>
<string name="title_checkbox_enable_sops">Оптимизировать игровые настройки</string>
<string name="summary_checkbox_enable_sops">Разрешить GFE изменять настройки игр для оптимальной потоковой передачи</string>
<string name="title_checkbox_host_audio">Проигрывать звук на PC</string>
<string name="summary_checkbox_host_audio">Проигрывать звук на компьютере и текущем устройстве.</string>
<string name="category_advanced_settings">Расширенные Настройки</string>
<string name="title_video_format">Изменить настройки H.265</string>
<string name="summary_video_format">H.265 снижает требования к пропускной способности, но требует очень свежих устройств.</string>
</resources>
+8 -2
View File
@@ -21,11 +21,17 @@
<item>Default</item>
<item>English</item>
<item>Italiano</item>
<item>Japanese</item>
<item>Русский</item>
<item>Nederlands</item>
</string-array>
<string-array name="language_values" translatable="false">
<item>default</item>
<item>en</item>
<item>it</item>
<item>ja</item>
<item>ru</item>
<item>nl</item>
</string-array>
<string-array name="decoder_names">
@@ -40,8 +46,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">
+7 -3
View File
@@ -55,7 +55,7 @@
<!-- General strings -->
<string name="ip_hint">IP address of GeForce PC</string>
<string name="searching_pc">Searching for PCs…</string>
<string name="searching_pc">Searching for PCs with GeForce Experience running</string>
<string name="yes">Yes</string>
<string name="no">No</string>
<string name="lost_connection">Lost connection to PC</string>
@@ -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