Compare commits

...

999 Commits

Author SHA1 Message Date
Cameron Gutman
5c175fecf6 Version 6.0.1 2018-12-05 21:29:12 -08:00
Cameron Gutman
773976b265 Update 49 FPS hack for MTK devices running Oreo which remains broken 2018-12-05 20:43:44 -08:00
Cameron Gutman
80070bbdbe Update readme to new URL 2018-12-03 21:42:47 -08:00
Cameron Gutman
4c8d433b6c Always use the new L+ releaseOutputBuffer() to gain drop support on L 2018-12-03 18:15:51 -08:00
Cameron Gutman
404f096d11 Only enable the FPS toggle on Lollipop or later 2018-12-03 18:15:03 -08:00
Cameron Gutman
d2ac927cec Version 6.0 r2 2018-12-01 14:24:43 -08:00
Cameron Gutman
5e3d59d3d7 Move FPS unlock into basic settings 2018-12-01 14:23:51 -08:00
Cameron Gutman
9cd2ce1309 Add option to unlock FPS 2018-12-01 14:19:29 -08:00
Cameron Gutman
9ed49730d4 Fix 4K streaming resolution 2018-12-01 13:02:04 -08:00
Cameron Gutman
39ebb48f58 Remove old gamepad settings string 2018-11-30 21:33:22 -08:00
Cameron Gutman
1c29c70fba Version 6.0 2018-11-30 21:28:53 -08:00
Cameron Gutman
6993051529 Retransmit OSC gamepad packets every 100 ms to recover from dropped events in GFE 2018-11-30 21:17:12 -08:00
Cameron Gutman
4930087c4d Remove 63 Hz cap for > 60 FPS streams 2018-11-30 19:49:14 -08:00
Cameron Gutman
795f0a013b Create toggle for back and forward mouse support 2018-11-30 18:37:36 -08:00
Cameron Gutman
213414778e Rename multi-controller option 2018-11-30 18:23:15 -08:00
Cameron Gutman
7eac0ccaf8 Fix controller packet loss when zeroing analog sticks on OSC 2018-11-25 15:02:32 -08:00
Cameron Gutman
6adc9dcb2d Add support for 90/120 FPS streaming and 1440p 2018-11-23 18:41:43 -08:00
Cameron Gutman
be620908f9 Update common with 4K check removal 2018-11-22 02:52:53 -08:00
Cameron Gutman
e4edfdb043 Add missing apostrophe escape 2018-11-22 02:45:35 -08:00
bubuleur
3b5028d1a4 Update French (#639)
* Update French

* Update strings.xml
2018-11-22 02:43:35 -08:00
Cameron Gutman
bc8c45bd59 Version 5.10.3 2018-11-21 21:23:12 -08:00
Cameron Gutman
63eb346a70 Use automatic remote streaming detection 2018-11-21 21:20:11 -08:00
Cameron Gutman
27ad691d23 Version 5.10.2 2018-11-15 22:13:44 -08:00
Cameron Gutman
747e920061 Update common for GFE 3.16 2018-11-15 22:11:32 -08:00
Cameron Gutman
8d09f56a0e Fix race condition causing loss of manual IP address after mDNS discovery 2018-11-13 23:16:25 -08:00
Cameron Gutman
113a0e2c45 Version 5.10.1 2018-10-30 20:26:32 -07:00
Cameron Gutman
977215a098 Fix crash when CMS dies and user returns to app view activity and taps a game 2018-10-30 20:21:11 -07:00
Cameron Gutman
a7e65b47f9 Fix race condition on AppView activity startup 2018-10-30 17:52:46 -07:00
Cameron Gutman
7126055ad6 Fix crash on Lenovo Mirage Solo 2018-10-30 17:46:47 -07:00
Cameron Gutman
3de9765eaa Version 5.10 2018-10-27 23:45:01 -07:00
Cameron Gutman
d4072eb295 Avoid nulling activeAddress during polling 2018-10-27 23:38:11 -07:00
Cameron Gutman
cac2bdbb81 Disable back mouse button on Xiaomi devices to workaround issue 2018-10-27 13:50:37 -07:00
Cameron Gutman
66f0aee3f8 Use STUN to discover WAN address when PC is found using mDNS 2018-10-27 10:46:28 -07:00
Cameron Gutman
b690dc5474 Rewrite reachability code and computer DB to bring it inline with other modern Moonlight clients 2018-10-27 02:18:33 -07:00
Cameron Gutman
c2fbe6ad91 Version 5.9.4 2018-10-24 19:41:39 -07:00
Cameron Gutman
cf07c02398 Update AGP to 3.2.1 2018-10-24 19:40:26 -07:00
Cameron Gutman
42dc928ad5 Fixes to make the translation build without warnings 2018-10-24 19:37:41 -07:00
bubuleur
11597f0aa7 Update of the French version (#636)
* Update of the French version

Hello,
Update your application in French
thank you

* Update strings.xml

* Update strings.xml
2018-10-24 19:36:50 -07:00
Cameron Gutman
cdcd4d48f2 Always handle KEYCODE_BACK to prevent synthetic right-clicks on back. Possibly fixes #634 2018-10-24 19:25:47 -07:00
Cameron Gutman
a9af4e54a9 Add confirmation dialog for PC deletion 2018-10-24 18:47:52 -07:00
Cameron Gutman
7eac609219 Fix root mouse capture with su binaries that don't like additional parameters after -c 2018-10-10 21:23:41 -07:00
Cameron Gutman
fa761debc4 Fix root build 2018-10-05 01:44:03 -07:00
Cameron Gutman
62e175f069 Avoid crashing when opening an app context menu in list mode 2018-10-05 01:42:19 -07:00
Cameron Gutman
d7d8c40565 Version 5.9.3 2018-10-05 01:34:59 -07:00
Cameron Gutman
64de13ab50 Try to disambiguate right clicks from back presses 2018-10-05 01:29:18 -07:00
Cameron Gutman
2f02939638 Always process key events before the IME 2018-10-05 01:10:27 -07:00
Cameron Gutman
1d7c8697e9 Add support for X1 and X2 mouse buttons 2018-10-05 00:56:30 -07:00
Cameron Gutman
7dea322bbd Update build tools to 28.0.3 2018-09-29 15:37:31 -07:00
Cameron Gutman
349ecb16ab Increment version code 2018-09-29 15:36:21 -07:00
Cameron Gutman
a3867735c1 Update to AGP 3.2 2018-09-29 15:31:13 -07:00
Cameron Gutman
5b087e9f70 Update common-c with split encode change 2018-09-22 20:22:59 -07:00
Cameron Gutman
eed18223eb Version 5.9.2 2018-09-18 20:36:53 -07:00
Cameron Gutman
30d4d2a918 Update moonlight-common 2018-09-18 20:27:08 -07:00
Cameron Gutman
30f666c70e Update AGP to 3.2 rc3 2018-09-18 20:23:25 -07:00
Cameron Gutman
209fead0e8 Only add the create shortcut option if the box art is present to avoid crashing 2018-09-18 20:22:16 -07:00
Cameron Gutman
5c6889bf6d Version 5.9.1 2018-08-12 00:45:19 -07:00
Cameron Gutman
7d24900756 Update build tools to 28.0.2 2018-08-12 00:44:09 -07:00
Cameron Gutman
79a75b9d19 Update common with audio fix and game launch fix 2018-08-12 00:40:56 -07:00
Cameron Gutman
29b64992bd Enable stale and no-response bots to reduce inactive issues 2018-08-12 00:20:03 -07:00
Cameron Gutman
c9b14540f2 Remove reference to old Moonlight Java project 2018-08-10 22:29:59 -07:00
Cameron Gutman
546843a26c Fix crash on quit confirmation prompt 2018-07-28 00:13:20 -07:00
Cameron Gutman
d03d260535 Add status bar and navigation bar color on L+ 2018-07-27 23:48:20 -07:00
Cameron Gutman
6946e3c7a2 Just use the PC name as the app list title 2018-07-27 23:42:57 -07:00
Cameron Gutman
b79d328961 Version 5.9 2018-07-16 18:46:01 -07:00
Cameron Gutman
c313797d93 Make OSC reconfigure button non-focusable so it doesn't eat hardware enter/space presses. Fixes #611 2018-07-16 18:40:19 -07:00
Cameron Gutman
c8cb8e1346 Update build tools to 28.0.1 2018-07-16 18:25:33 -07:00
Cameron Gutman
6a9f8da14e Update common to reduce syscall overhead 2018-07-16 18:21:48 -07:00
Cameron Gutman
ff9260a0fd Update AGP to 3.2-beta4 2018-07-16 18:16:35 -07:00
zacklmoore
62bedb1609 Pinned Game Shortcuts (Android Oreo) (#603)
* Initial changes to add game shortcuts.

* Initial working cut.

* Cleanup and converting strings to resource strings.

* Additional cleanup.

* Removed a blank line

* Changes based on review feedback.

* Forgot to save some changes before commiting...

* Standardized dialogs and tried to fix the dialogs auto-closing when the PCView is already opened.
2018-07-06 21:53:19 -07:00
Cameron Gutman
a519723d44 Monkey-proof Moonlight 2018-06-20 01:26:59 -07:00
Cameron Gutman
36191781ed Version 5.8.2 2018-06-16 17:05:44 -07:00
Cameron Gutman
61b6a49669 Correct MT8176 errata 2018-06-16 16:37:50 -07:00
Cameron Gutman
e97845e46e Add comments and documentation on MT8176 testing 2018-06-16 16:31:06 -07:00
Cameron Gutman
6bba68207d Ignore spurious ACTION_HOVER_ENTER with wrong coordinates and KEYCODE_BACK repeats. Fixes #554 2018-06-16 15:57:44 -07:00
Cameron Gutman
0e17cccc06 Process historical values for relative mouse events 2018-06-16 15:22:01 -07:00
Cameron Gutman
918e922e40 Avoid processing mouse move history 2018-06-16 15:14:16 -07:00
Cameron Gutman
a08854ddfd Properly handle SOURCE_MOUSE_RELATIVE in the mouse back button hack. Fixes #424 2018-06-16 15:01:11 -07:00
Cameron Gutman
eb6f15c2b7 Add dynamic method for allowing back buttons for navigation 2018-06-16 14:32:07 -07:00
Cameron Gutman
2cd9e31684 Update gitignore to handle app bundles and other new files dropped by AS 2018-06-16 14:27:54 -07:00
Cameron Gutman
791d6624e2 Update to AGP 3.2 alpha 18 2018-06-16 14:21:14 -07:00
Cameron Gutman
af41021271 Use HEVC by default on MediaTek SoCs with PowerVR graphics 2018-06-14 22:55:10 -07:00
Cameron Gutman
d726d939f4 Version 5.8.1-r3 2018-06-09 21:49:41 -07:00
Cameron Gutman
748085e7bb Update common to fix reconnection issue 2018-06-09 21:39:26 -07:00
Cameron Gutman
d57d19174b Version 5.8.1 r2 2018-06-09 18:16:41 -07:00
Cameron Gutman
efebe1828a Update common with Lint fixes 2018-06-09 18:14:39 -07:00
Cameron Gutman
06007e0597 Add a button for adding a PC manually 2018-06-09 18:14:09 -07:00
Cameron Gutman
3a868045d7 Allow the display to go off if the stream disconnects 2018-06-09 17:48:07 -07:00
Cameron Gutman
e0a7ff1880 Remove in-progress toast for WOL 2018-06-09 17:23:59 -07:00
Cameron Gutman
88d43bbd40 Disable density splits until I can figure out why we're crashing 2018-06-08 01:13:00 -07:00
Cameron Gutman
30ff319b13 Version 5.8.1 2018-06-08 01:08:09 -07:00
Cameron Gutman
9a0f48b799 Add support for display cutouts on P 2018-06-08 01:05:32 -07:00
Cameron Gutman
b52c8a1a8f Use ImageDecoder API on P and higher quality decodes on non-low ram devices 2018-06-08 00:46:40 -07:00
Cameron Gutman
3fde115670 Update to AGP 3.2 alpha 17 2018-06-08 00:24:39 -07:00
Cameron Gutman
b6f4d8ff1e Target API 28 2018-06-08 00:23:41 -07:00
Cameron Gutman
a7d85a7dd5 Update to AGP 3.2-alpha16 2018-05-29 18:40:26 -07:00
Cameron Gutman
9b238ab6c3 Version 5.8 (take 2) 2018-05-27 20:05:51 -07:00
Cameron Gutman
f82ee97c05 Update common to fix channel mapping error in 5.1 high quality mode 2018-05-27 20:04:28 -07:00
Cameron Gutman
35fb96f9f4 Version 5.8 2018-05-27 18:56:03 -07:00
Cameron Gutman
37371906d5 Update common for audio and JNI library size improvements 2018-05-27 18:51:28 -07:00
Cameron Gutman
83a9539f4b Version 5.7.7 2018-05-21 19:24:26 -07:00
Cameron Gutman
b214fe5301 Update to AGP 3.2-alpha15 2018-05-21 18:55:36 -07:00
Cameron Gutman
57779b4e89 Always expose gamepad 1 in single controller mode 2018-05-21 18:55:02 -07:00
Cameron Gutman
547932f8b2 Version 5.7.6 2018-05-13 22:23:22 -07:00
Cameron Gutman
762fa0fe2f Tighten ProGuard rules for BC 2018-05-12 21:25:41 -07:00
Cameron Gutman
9cedc57df2 Move the portrait activity_pc_view.xml to the default directory 2018-05-12 18:33:47 -07:00
Cameron Gutman
ba81f8096a Allow software decoding in CrOS emulator 2018-05-11 19:28:30 -07:00
Cameron Gutman
c4fa654166 Fix split breaking language chooser 2018-05-08 21:24:49 -07:00
Cameron Gutman
8ac440b68b Update AGP for AS 3.2 and app bundles 2018-05-08 21:22:10 -07:00
Cameron Gutman
165386b941 Update AGP and D8 2018-05-08 18:43:49 -07:00
Cameron Gutman
3a7398f321 Use ProGuard for minification 2018-05-08 18:43:23 -07:00
Cameron Gutman
ebb1d0dfa2 Version 5.7.5 2018-04-21 23:48:04 -07:00
Cameron Gutman
1ca1ed5d20 Increase OSC analog stick size 2018-04-21 23:46:55 -07:00
Cameron Gutman
b416bafb78 Hide OSC in PiP and scale properly in multi-window 2018-04-21 23:37:38 -07:00
Cameron Gutman
3a301b74a6 Update to D8 v1.0.23 2018-04-21 23:17:04 -07:00
Cameron Gutman
71d463f063 Avoid crashing from unexpected enterPictureInPictureMode() exceptions 2018-04-21 21:32:59 -07:00
Cameron Gutman
1fae816223 Remove the spinner threads (and battery saver option to disable them) 2018-04-21 21:29:42 -07:00
Cameron Gutman
989d6fc169 Fix for broken keyboard d-pad and Shift+Space behavior on Samsung devices 2018-04-21 16:24:23 -07:00
Cameron Gutman
381509b3a6 Properly handle joysticks that only return events for one trigger axis 2018-04-21 15:09:57 -07:00
Cameron Gutman
d8ae40376e Update to AGP 3.1.1 2018-04-09 20:32:06 -07:00
Cameron Gutman
4ea93f5e68 Version 5.7.4 2018-04-08 21:21:37 -07:00
Cameron Gutman
cd84c8f30e Fix grammar issue in decoder crash message 2018-04-08 21:19:49 -07:00
Cameron Gutman
8d4cdca7c3 Fix RFI disabling for KDDI/b5_jp_kdi/b5:7.0/NRD90U/1801120299534:user/release-keys and KDDI/b3_jp_kdi/b3:7.0/NRD90U/180120857f434:user/release-keys 2018-04-08 19:56:01 -07:00
Cameron Gutman
c0239c36fd Update language around decoder crashes 2018-03-27 20:52:01 -07:00
Cameron Gutman
9d9f729e42 Address another buggy LGE variant (b5_jp_kdi) 2018-03-27 20:48:16 -07:00
Cameron Gutman
6c5fe18b6e Update Gradle for AS 3.1 2018-03-26 23:30:33 -07:00
Cameron Gutman
1994bf6522 Version 5.7.3.1 for Amazon 2018-03-24 23:48:04 -07:00
Cameron Gutman
31381e5664 Add Amlogic SoC to HEVC whitelist for Fire TV 3 now that maxNumReferenceFrames support has been out for a while 2018-03-24 23:47:05 -07:00
Cameron Gutman
fac1b1d7e5 Version 5.7.3 2018-03-24 13:05:31 -07:00
Cameron Gutman
40c406051c Ignore non-relative MotionEvents on Oreo to fix mouse jumping when toggling capture 2018-03-20 20:21:21 -07:00
Cameron Gutman
8bac873e67 Make sure the joystick actually has relevant axes to avoid FPing on some weird keyboards 2018-03-20 19:47:33 -07:00
Cameron Gutman
a170e1efd7 Update to Gradle 4.5 2018-03-20 19:21:02 -07:00
Cameron Gutman
17bffa8d78 Fix race condition between polling return and onPause() 2018-03-20 19:10:04 -07:00
Cameron Gutman
289222749b Cover another broken G Pad III 8.0 FHD variant (b3_open_kr) 2018-03-20 18:54:48 -07:00
Cameron Gutman
81d84600d4 Start connection in onSurfaceChanged() just in case we render our first frame prior to surface configuration 2018-03-20 18:37:31 -07:00
Cameron Gutman
0b15fd582d Update gitignore and delete iml file 2018-03-20 18:06:02 -07:00
Cameron Gutman
cbe4a1cde6 Update Gradle for AS 3.1 RC3 2018-03-20 18:02:34 -07:00
Cameron Gutman
89ef16c02e Fix level_idc 31 patch 2018-03-18 00:59:58 -07:00
Cameron Gutman
58b6ed8d00 Update Gradle wrapper 2018-03-11 18:12:01 -07:00
Cameron Gutman
7d01e1a7a4 Fix landscape orientation lock 2018-03-11 15:31:10 -07:00
Cameron Gutman
ab769a1606 Version 5.7.2 2018-03-07 18:53:38 -08:00
Nikita Glazkov
3ac9abbab1 Russian translation (#546)
* Fix russian translation

* Complete russian translation
2018-03-07 18:44:33 -08:00
Marco
288efd0726 Added new strings (#542) 2018-03-07 18:43:33 -08:00
Nikita Glazkov
d2d0ed65d6 Different app label for debug builds (#545)
* Different app label for debug builds

* Remove underscores from app labels
2018-03-07 18:41:34 -08:00
Cameron Gutman
e697ed72db Add missing <network-security-config> tag 2018-03-07 18:17:00 -08:00
Cameron Gutman
b657c746be Pass the BouncyCastle provider directly rather than by name, since the latter doesn't work on Android P (at least DP1) 2018-03-07 17:56:50 -08:00
Cameron Gutman
947f8db2d5 Update for Android Studio 3.1 2018-03-07 17:55:49 -08:00
Cameron Gutman
15857efd36 Add network security config allowing plaintext for Android P 2018-03-07 11:47:19 -08:00
Cameron Gutman
3fd0f20e10 Version 5.7.1 2018-03-01 22:18:32 -08:00
Cameron Gutman
a2e64fd7df Fix crash when running Dutch language. Fixes #543 2018-03-01 22:09:49 -08:00
Cameron Gutman
a620dc7d0c Version 5.7 2018-02-25 13:41:09 -08:00
Cameron Gutman
9d7a28e408 Implement deletion of OSC settings 2018-02-25 13:33:52 -08:00
Cameron Gutman
3244344fc7 Add preference dependencies for USB and OSC 2018-02-25 13:12:37 -08:00
Cameron Gutman
75057f2d39 Persist OSC configuration between launches 2018-02-25 13:07:07 -08:00
Cameron Gutman
bbec3402d9 Reduce opacity of OSC configuration button 2018-02-25 12:18:45 -08:00
Cameron Gutman
dcf4dac8dd Only add L3/R3 buttons for the L3/R3-only config, since the analog sticks work for this 2018-02-25 12:17:59 -08:00
Cameron Gutman
d98f484aaf Change OSC configuration button to work better on rounded screen devices 2018-02-25 12:12:23 -08:00
Cameron Gutman
0218a9ce14 Small string update 2018-02-24 21:07:18 -08:00
Cameron Gutman
0ec6dcd67e Add 360p option and change bitrate to kbps 2018-02-24 21:05:45 -08:00
Cameron Gutman
88f9b68db7 Add mouse emulation and bind all USB devices options 2018-02-24 20:17:14 -08:00
Cameron Gutman
3c2fd32d1e Improve error reporting for incorrect IP address 2018-02-24 19:36:23 -08:00
Cameron Gutman
6557cba307 Add support for scrolling with d-pad in mouse emulation mode 2018-02-24 18:57:09 -08:00
Cameron Gutman
ae6f797436 Handle right-clicks that are synthesized into back button presses 2018-02-19 17:29:18 -08:00
Cameron Gutman
3442a64f4d Update common to fix audio dropouts. Fixes #523 2018-02-19 01:21:13 -08:00
Cameron Gutman
37ddccde0c Version 5.6.7 2018-02-14 18:39:04 -08:00
Cameron Gutman
ffc59c6bd6 Update moonlight-common to fix pairing timeout issue 2018-02-14 18:34:45 -08:00
Cameron Gutman
88f84a0c12 Version 5.6.6 2018-02-10 17:21:36 -08:00
Cameron Gutman
03ecf3e5ac Fix crash on Knox devices with USB blocking policies 2018-02-10 16:56:01 -08:00
Cameron Gutman
617c8582b4 Fix crash on MediaTek PAL Android TVs 2018-02-10 16:42:45 -08:00
Cameron Gutman
ef3b28295b Update dependency versions 2018-02-05 18:38:23 -08:00
Cameron Gutman
3bcd2ee068 Ignore bogus refresh rates just to be on the safe side 2018-02-04 15:26:40 -08:00
Cameron Gutman
d4ff58b3ad Version 5.6.5 2018-02-04 12:59:52 -08:00
Cameron Gutman
c797318ece Use frame drop hack to reduce latency and micro-stuttering for now 2018-02-04 12:59:04 -08:00
Cameron Gutman
82387d23f8 Send client's display refresh rate to server for better frame pacing 2018-02-03 22:09:42 -08:00
Cameron Gutman
766e629be5 Use applicationId com.limelight.unofficial for release builds by default 2018-02-03 19:45:18 -08:00
Cameron Gutman
b93aa42c0c Fix detection on HEVC support on some buggy devices 2018-01-28 21:16:28 -08:00
Cameron Gutman
36f132942f Version 5.6.4 2018-01-20 15:13:17 -08:00
Cameron Gutman
e4c251e7ee Ignore NVIDIA mouse capture extension on root builds to avoid broken LineageOS implementation 2018-01-20 02:31:40 -08:00
Cameron Gutman
fb54bd5c78 Send the initial number of connected gamepads during launch to fix some games like L4D2 2018-01-20 01:16:25 -08:00
Cameron Gutman
8d4c86e113 Update common to support sending initial gamepads and fixing WoL 2018-01-20 01:11:39 -08:00
Cameron Gutman
7fafb8e0ff Revert extractNativeLibraries=false change due to install failure on Fire TV 3 2018-01-11 00:03:03 -08:00
Cameron Gutman
fbcbe09255 Version 5.6.3 2018-01-10 00:40:08 -08:00
Cameron Gutman
e336a4446a Update common to work with GFE 3.12 2018-01-10 00:38:15 -08:00
Cameron Gutman
ffb35b2cdd Use smaller packets for streaming at 1080p and below to attempt to mitigate some reported regressions with v5.6.2 2018-01-09 23:38:25 -08:00
Cameron Gutman
2d0af6281c Ensure polling threads terminate even when polling resumes immediately 2017-12-29 14:05:29 -08:00
Cameron Gutman
472a7f6c8a Version 5.6.2 2017-12-27 22:45:07 -08:00
Cameron Gutman
cd06559c66 Also count link-local addresses as local 2017-12-27 22:41:21 -08:00
Cameron Gutman
d833933aaa Allow up to 1 second for fast poll to address connection flakiness 2017-12-27 22:27:35 -08:00
Cameron Gutman
dc3495d59b Improve local vs. remote heuristics 2017-12-27 21:43:12 -08:00
Cameron Gutman
e3a2e40043 Shrink large box art down to the normal size by changing sample size 2017-12-27 21:28:38 -08:00
Cameron Gutman
31e1fb743e Update common to address some null PC name crashes 2017-12-27 20:36:07 -08:00
Cameron Gutman
bc59f11096 Disable RFI on b3_att_us 2017-12-27 20:34:02 -08:00
Cameron Gutman
6d97775aa9 Try disabling RFI if the previous run crashes 2017-12-27 20:32:34 -08:00
Cameron Gutman
3fff34e08a Don't extract native libraries for non-root build 2017-12-27 19:40:49 -08:00
Cameron Gutman
15e856dccb Move AudioTrack flush to cleanup() callback since all sample submission has ceased by then 2017-12-06 20:43:58 -08:00
Cameron Gutman
07d04171c3 Force HEVC enabled if HDR is requested 2017-12-05 17:38:25 -08:00
Cameron Gutman
42bd93cb3a Update common to fix mDNS race condition 2017-12-05 17:33:55 -08:00
Cameron Gutman
7d289f1134 Fix race conditions when frames are submitted after stop() has been called 2017-12-05 17:28:04 -08:00
Cameron Gutman
214461e123 Version 5.6.1 2017-12-01 00:42:43 -08:00
Cameron Gutman
b0144a3256 Update decoder-errata.txt with HEVC errata 2017-12-01 00:40:32 -08:00
Cameron Gutman
3171256c6e Remove EvdevCaptureProvider components from non-root build 2017-12-01 00:37:25 -08:00
Cameron Gutman
5c69f6716c Don't build evdev_reader for the non-root variant 2017-12-01 00:10:55 -08:00
Cameron Gutman
6264781539 Update common to get decoder compatibility fixes 2017-11-30 23:47:08 -08:00
Cameron Gutman
0225f534d0 Fix H.265 streaming issues with MediaTek Android TV devices 2017-11-29 20:27:33 -08:00
Cameron Gutman
284a31737e Catch input buffer too small 2017-11-28 19:33:34 -08:00
Cameron Gutman
b37a2dea57 Fix help display on some Android TV devices 2017-11-25 15:08:22 -08:00
Cameron Gutman
5c865e7f36 Version 5.6 r4 2017-11-25 14:33:41 -08:00
Cameron Gutman
04d9aea8c8 Detect and report decoder hangs 2017-11-25 14:27:04 -08:00
Cameron Gutman
b6f52db9c3 Fix crash when input events are received and no H.264 decoder is present 2017-11-25 13:35:46 -08:00
Cameron Gutman
99d2e40683 Reset HDR when decoder crashes 3 times in a row 2017-11-25 13:21:04 -08:00
Cameron Gutman
02c4ed2724 Improve decoder crash reporting reliability 2017-11-25 13:19:30 -08:00
Cameron Gutman
5f4aab8f94 Improve decoder crash reporting detail 2017-11-25 12:56:54 -08:00
Cameron Gutman
ec65901003 Report frames rendered in decoder crash report 2017-11-25 11:25:04 -08:00
Cameron Gutman
915acee88d Version 5.6 r3 2017-11-23 11:41:20 -08:00
Cameron Gutman
300d444f71 Ensure inForeground is set before CMS binding can complete 2017-11-23 11:34:22 -08:00
Cameron Gutman
f37ab40c2f Fix race condition between completeOnCreate() and onConfigurationChanged() 2017-11-23 11:25:51 -08:00
Cameron Gutman
16e285d926 Version 5.6 r2 2017-11-21 21:33:06 -08:00
Cameron Gutman
f2d122a275 Fix screen dimensions for portrait devices 2017-11-21 20:18:28 -08:00
Cameron Gutman
bfa5a6349e Ensure MediaCodecHelper is initialized before evaluating codecs 2017-11-21 19:27:08 -08:00
Cameron Gutman
a56689aea3 Always include resolutions that fit on the display 2017-11-21 19:18:41 -08:00
Cameron Gutman
3a5ba820cb Version 5.6 2017-11-20 23:08:43 -08:00
Cameron Gutman
ec69fef36f Ignore back button presses on the default context 2017-11-20 22:46:57 -08:00
Cameron Gutman
ff38074f55 Report GL Renderer in RendererException 2017-11-20 22:38:22 -08:00
Cameron Gutman
85d0ce0c40 Update Gradle to 3.0.1 2017-11-20 22:28:54 -08:00
Cameron Gutman
777129ca90 Move GLRenderer fetching into PcView to avoid race conditions inside Game activity and cache the result 2017-11-20 22:28:19 -08:00
Cameron Gutman
06156c4d68 Ignore back from goodix_fp device 2017-11-20 21:03:36 -08:00
Cameron Gutman
1c725b9dac Don't use reference picture invalidation on low-end Snapdragon SoCs 2017-11-20 20:56:31 -08:00
Cameron Gutman
f761ee52db Exclude resolutions that are not supported by the decoders 2017-11-18 19:47:39 -08:00
Cameron Gutman
05e8cfcc0a Report adaptive playback status in crash reports 2017-11-18 18:31:12 -08:00
Cameron Gutman
912925ef2c Disable performance optimizations when in multi-window 2017-11-18 17:14:40 -08:00
Cameron Gutman
4deb881ec8 Enable adaptive playback on non-Intel devices 2017-11-18 16:37:17 -08:00
Cameron Gutman
f55d6308ce Pass source rect to PiP to smoothly animate to 16:9 2017-11-18 16:29:03 -08:00
Cameron Gutman
44a3a141c0 Submit H.264 CSD in a single blob to try to prevent some decoder crashes 2017-11-18 15:14:25 -08:00
Cameron Gutman
37b5ba004c Fix IDR frame NALU drop race condition 2017-11-18 14:43:04 -08:00
Cameron Gutman
b774b47213 Update for NDK 16 (deprecating MIPS) 2017-11-18 13:38:45 -08:00
Cameron Gutman
74dc00445e Version 5.5 2017-11-10 01:19:23 -08:00
Cameron Gutman
3b4563d5ea Suppress digital trigger events if an analog trigger axis is present. Fixes #465 2017-11-10 00:50:02 -08:00
Cameron Gutman
38669817b4 Update common to fix HEVC artifacts in some apps 2017-11-10 00:10:21 -08:00
Cameron Gutman
8f1d3ae04e Add support for PiP on Oreo 2017-11-09 23:28:22 -08:00
Cameron Gutman
74ed95871b Exclude HDR toggle when the device doesn't support it 2017-11-09 21:57:33 -08:00
Cameron Gutman
cc5d67616c Prevent false USB access prompts due to races with kernel input stack bringup 2017-11-09 21:14:10 -08:00
Cameron Gutman
eed7f09e6f Fix numpad operator keys not working 2017-11-07 22:03:40 -08:00
Cameron Gutman
e3c1d23744 Fix SHIELD remote back button not working 2017-11-07 21:45:07 -08:00
Cameron Gutman
c4b1200b43 Update build tools to 27.0.1 2017-11-07 21:44:27 -08:00
Cameron Gutman
dff09f33a3 Fix shift not working on soft keyboard 2017-11-07 00:27:27 -08:00
Cameron Gutman
1f6b1dc2fe Send different VK codes for left and right ctrl/alt/shift keys. Fixes #318 2017-11-06 23:38:48 -08:00
Cameron Gutman
3f118dae93 Add HDR support and tweak HEVC supported decoders 2017-11-05 19:31:05 -08:00
Cameron Gutman
91a30ff6fe Target O MR1 2017-11-05 15:43:11 -08:00
BryanHaley
5102669b06 Virtual L3 R3 Buttons (#453)
* Added virtual L3 R3 options to better support gamepads missing these buttons.

* Update preferences.xml
2017-11-05 13:57:02 -08:00
Cameron Gutman
2e2f09be00 Fix frame drops when stopping the stream 2017-11-05 13:49:06 -08:00
Cameron Gutman
c402103fe3 Avoid colliding with System UI in multi-window mode 2017-11-05 13:15:06 -08:00
Cameron Gutman
5e5df8abc8 Add never drop frames option for devices with micro-stuttering issues 2017-11-05 12:29:33 -08:00
Cameron Gutman
d125eb7b16 Update to gradle 3.0.0 2017-11-05 12:08:16 -08:00
Cameron Gutman
a116858493 Add .debug suffix to debug builds 2017-11-05 12:07:52 -08:00
Cameron Gutman
5f3b333e98 Version 5.2.1 2017-10-17 00:38:59 -07:00
Cameron Gutman
80a37855c7 Merge branch 'master' of github.com:moonlight-stream/moonlight-android 2017-10-17 00:37:00 -07:00
Cameron Gutman
5db1ec8ec0 Fix support for GFE 3.10 2017-10-17 00:35:36 -07:00
Cameron Gutman
8911c58e50 Block OMX.ffmpeg software decoders 2017-10-17 00:31:26 -07:00
Cameron Gutman
780a64694d Fix NPE when input device is removed during enumeration 2017-10-17 00:07:51 -07:00
Cameron Gutman
3c5ea9c8c3 Remove Nvidia's HEVC decoder from the hard blacklist now that it seems to be fine on Foster NRD90M 2017-10-08 22:06:06 -07:00
Cameron Gutman
40d1436ce3 Update for AS 3.0 Beta 7 2017-10-04 19:30:36 -07:00
Cameron Gutman
dbb02acd37 Reintroduce the 75% HEVC bitrate multiplier that the old streaming core had 2017-09-25 21:39:53 -07:00
Cameron Gutman
20c4eac4ef Force HEVC disabled on Qualcomm SoCs older than Snapdragon 805 2017-09-19 21:21:23 -07:00
Cameron Gutman
b9f1142af7 Version 5.2 2017-09-09 18:53:36 -07:00
Cameron Gutman
38a6a2b74a A few fixes for decoder crash notifications 2017-09-09 18:44:06 -07:00
Cameron Gutman
fd2421618a Update common-c with crash fix 2017-09-09 17:40:53 -07:00
Cameron Gutman
79a9ea7179 Add decoder crash notification and settings reset on continued crashing 2017-09-09 17:40:07 -07:00
Cameron Gutman
34a11c9262 Correct reachability when restoring a lost address 2017-09-09 16:02:39 -07:00
Cameron Gutman
84a9845c1d Fix polling overwriting manually entered IP addresses 2017-09-09 15:40:07 -07:00
Cameron Gutman
5b05220008 Prevent mDNS from overwriting external IP addresses 2017-09-09 15:21:31 -07:00
Cameron Gutman
b2bd7257e1 Fix Lint warnings 2017-09-09 14:12:54 -07:00
Cameron Gutman
46a998c113 Convert address fields to strings to better manage DNS names 2017-09-09 13:39:54 -07:00
Cameron Gutman
60cd951774 Rename localIp/remoteIp fields to localAddress/remoteAddress to prepare for DNS names 2017-09-09 12:47:23 -07:00
Cameron Gutman
d4f8d8f689 Switch database storage to use strings for addresses 2017-09-09 12:43:20 -07:00
Cameron Gutman
608a0ebb5b Update build files for AS3b5 2017-09-09 11:50:42 -07:00
Cameron Gutman
f01a15d182 Removed duplicated current address logic 2017-09-09 11:49:15 -07:00
Cameron Gutman
0268b4f958 Update gradle for AS 3.0b4 2017-09-03 12:52:18 -07:00
Cameron Gutman
d71cf0eb98 Add app category for Oreo 2017-09-02 13:48:45 -07:00
Cameron Gutman
10ab40f823 Add/update remaining assets 2017-09-02 13:48:11 -07:00
Cameron Gutman
427edfa021 Update common submodule 2017-09-01 19:11:49 -07:00
Cameron Gutman
6f18831d5c Update BouncyCastle libs 2017-09-01 18:39:49 -07:00
Cameron Gutman
a3db09f422 Disable input compatibility mode on ChromeOS 2017-09-01 18:07:18 -07:00
Cameron Gutman
d185a05b1d Sort and sync vendor IDs with xpad 2017-08-25 21:04:36 -07:00
Cameron Gutman
78e575504a Update straggling app icon 2017-08-23 23:07:03 -07:00
Cameron Gutman
0a0be19b69 Fix brown-paper-bag bug in audio init error checking 2017-08-22 00:17:03 -07:00
Cameron Gutman
0792157e9d Fix some markdown errors and tweak supported GPUs 2017-08-13 23:53:18 -07:00
madmario1000
cdd0ecf0b7 Update README.md (#400)
Clarify the required specs a bit
2017-08-13 23:49:49 -07:00
Cameron Gutman
1ac721a35b Bump to version 5.1.2 2017-08-13 18:51:22 -07:00
Cameron Gutman
e49b1c92a2 Update for AS 3.0 Beta 2 2017-08-13 18:37:31 -07:00
Cameron Gutman
db4295bf83 Add adaptive icon for PC shortcut 2017-08-13 18:31:09 -07:00
Cameron Gutman
824c37f9d5 Adaptive launcher icon 2017-08-13 18:06:53 -07:00
Cameron Gutman
acf4426952 Update for Gradle 4 2017-06-24 12:56:52 -07:00
Cameron Gutman
e8c50342ab Version 5.1.1 2017-06-17 16:06:57 -07:00
Cameron Gutman
598995de3b Fix audio renderer using non-existant classes on Lollipop 2017-06-16 20:27:03 -07:00
Cameron Gutman
01cf0cc649 Fix Lint error 2017-06-16 20:06:45 -07:00
Cameron Gutman
fa560f462f Add battery saver mode 2017-06-16 20:01:41 -07:00
Cameron Gutman
f6e40118a9 Bring back the warning displayed if video decoder initialization fails 2017-06-16 19:50:50 -07:00
Cameron Gutman
fe7148dbd4 Only throw decoder exceptions if we're still receiving them after 3 seconds 2017-06-16 19:39:15 -07:00
Cameron Gutman
60de065836 Cleanup video decoder teardown paths 2017-06-16 19:11:39 -07:00
Cameron Gutman
6f82f82abb Use low latency audio pathway on Lollipop and later 2017-06-16 19:08:15 -07:00
Cameron Gutman
42f18cb4ac Version 5.1 2017-06-11 17:14:15 -07:00
Cameron Gutman
1bbd0054c2 Update common again to fix another long haul testing bug 2017-06-11 14:56:41 -07:00
Cameron Gutman
acdde37a3a Update common library again 2017-06-11 13:54:05 -07:00
Cameron Gutman
ad40e12167 Update common to fix incorrect assert firing 2017-06-11 13:09:29 -07:00
Cameron Gutman
1b3322b5ee Suppress crashes if the surface has become invalid 2017-06-10 17:25:23 -07:00
Cameron Gutman
6340ec6c6d Consolidate handling of decoder exceptions 2017-06-10 16:57:37 -07:00
Cameron Gutman
babd92c8c0 Add additional information to total frame latency and RendererException 2017-06-10 16:45:07 -07:00
Cameron Gutman
7f1fe5f520 Fix NPE if the device doesn't support H.264 hardware decoding 2017-06-10 11:48:25 -07:00
Cameron Gutman
01458770d2 Fix NPE enumerating input devices 2017-06-10 11:45:12 -07:00
Cameron Gutman
8d05f044f5 Allow software decoding on the emulator for testing 2017-06-08 22:21:51 -07:00
Cameron Gutman
f5680b59a5 Use debug moonlight-common with asserts enabled on debug builds and release moonlight-common with asserts disabled on release builds 2017-06-08 19:57:55 -07:00
Cameron Gutman
0ecf86c7ed At long last, Android has native mouse capture. Don't show the root version to users running O 2017-06-08 18:26:12 -07:00
Cameron Gutman
6789e8d497 Immediately call stopConnection() rather than waiting for activity stop on connection failure 2017-06-08 18:24:22 -07:00
Cameron Gutman
7d0160d556 Update gradle and SDK to O 2017-06-08 18:17:59 -07:00
Cameron Gutman
f6a0990432 Final fixes for Android O pointer capture 2017-06-08 18:17:34 -07:00
Cameron Gutman
5d6094df97 Version 5.0.2 2017-06-08 17:57:57 -07:00
Cameron Gutman
d98d4aeda2 Fix FEC fencepost error in moonlight-common 2017-06-08 17:57:43 -07:00
Cameron Gutman
852dcf5a2d Merge branch 'o-bringup' 2017-06-08 17:30:10 -07:00
Cameron Gutman
82e5aa122d Update common with FEC and latency fixes 2017-06-07 23:17:06 -07:00
Cameron Gutman
fe237d1da3 Fix some exceptions that escaped on decoder shutdown and surface loss 2017-06-07 20:01:09 -07:00
Cameron Gutman
e199fcd2d9 Try allowing decoder exceptions after initial start since we shouldn't throw on stop anymore 2017-06-06 22:50:08 -07:00
Cameron Gutman
d7c6f63592 Force Qualcomm and Samsung HEVC decoders disabled to avoid crashes and poor performance 2017-06-06 22:49:09 -07:00
Cameron Gutman
4b9c6b149a Remove the decoder stop hack and try to workaround the issue differently 2017-06-06 22:48:28 -07:00
Cameron Gutman
d1e41e41a1 Stop the connection in onStop() to try to avoid deadlocks due to surface loss. Also avoid calling stopConnection() from connection listener callbacks due to deadlock risk. 2017-06-05 20:33:23 -07:00
Cameron Gutman
96dfe25a14 Support packet size adjustments on LANs 2017-06-03 11:51:35 -07:00
Cameron Gutman
f76d78607a Improve HEVC decoder compatibility by submitting VPS+SPS+PPS in one CSD blob rather than individually 2017-06-03 11:46:29 -07:00
Cameron Gutman
a96f688bb2 Disable backup of preferences due to the device-specific data contained there 2017-06-02 20:00:37 -07:00
Cameron Gutman
90a1e68c68 Move input capture check to not mask touch events 2017-06-02 18:17:18 -07:00
Cameron Gutman
b287606106 Fix Pixel C keyboard d-pad regression due to aliasing with SOURCE_GAMEPAD 2017-05-31 21:51:32 -07:00
Cameron Gutman
a413185085 Fix Pixel C keyboard d-pad regression due to aliasing with SOURCE_GAMEPAD 2017-05-31 21:51:01 -07:00
Cameron Gutman
aa1b283570 Initial working pointer capture using onClick 2017-05-31 21:46:53 -07:00
Cameron Gutman
f07c886711 Add isCapturing() method to mouse capture providers 2017-05-31 21:26:26 -07:00
Cameron Gutman
e66b1ebec9 Initial pointer capture work for O 2017-05-31 20:50:47 -07:00
Cameron Gutman
d06912e81a Name the spinner threads so they are easily identified 2017-05-31 19:05:25 -07:00
Cameron Gutman
08bcd97594 Use a less power intensive way of keeping the DVFS state friendly 2017-05-29 20:11:39 -07:00
Cameron Gutman
49e51f5f6f 5.0.1 r2 2017-05-21 14:43:53 -07:00
Cameron Gutman
4223a7fd30 Version 5.0.1 2017-05-21 14:27:39 -07:00
Cameron Gutman
6edd0ab540 Only use RFI on modern Intel devices 2017-05-21 14:15:05 -07:00
Cameron Gutman
ce7146175a Merge remote-tracking branch 'origin/new-core' 2017-05-21 14:05:00 -07:00
Cameron Gutman
3176a85f35 Enable RFI for Intel decoders 2017-05-21 14:01:30 -07:00
Cameron Gutman
ad1c11bba5 Decouple direct submit producer and polling consumer 2017-05-21 13:48:02 -07:00
Cameron Gutman
ac640a6842 Fix a few small nits with keyboard and dpad navigation of the UI 2017-05-21 13:24:18 -07:00
Cameron Gutman
8962497a8c Fix deadlocks in audio and video stream shutdown using the new callbacks 2017-05-21 13:07:19 -07:00
Cameron Gutman
83141d3f91 Version 5.0.0 r2 2017-05-18 13:42:48 -07:00
Cameron Gutman
55f2e89bbe Reuse callback buffers 2017-05-18 13:37:02 -07:00
Cameron Gutman
3558655b72 Change submodule remote to use HTTPS link 2017-05-18 11:26:47 -07:00
Cameron Gutman
44cbf8adc1 Fix crash on stream disconnect on Android 7.0+ devices (root only) 2017-05-18 10:52:17 -07:00
Cameron Gutman
686490ba70 Handle decoder exceptions in dequeueInputBuffer 2017-05-18 10:25:48 -07:00
Cameron Gutman
d0ecde1e16 Fix crash if video decoder fails to initialize 2017-05-18 09:58:28 -07:00
Cameron Gutman
9417908848 Fix crash in virtual controller if a release event happens without a press 2017-05-17 21:32:24 -07:00
Cameron Gutman
93b0073467 Finish the activity if the computer wasn't found 2017-05-17 20:51:33 -07:00
Cameron Gutman
1434be262c Make sure a USB context exists before reporting input 2017-05-17 20:38:55 -07:00
Cameron Gutman
75aabd6471 Perform cleanup tasks in onDestroy() to avoid crashing if onStop() is called twice 2017-05-17 20:22:10 -07:00
Cameron Gutman
bafa2addd3 Fix crash queuing input buffer on stop 2017-05-17 20:09:11 -07:00
Cameron Gutman
32b787e77c Eat more decoder exceptions on stop/teardown 2017-05-17 19:45:55 -07:00
Cameron Gutman
43b58b7a5e Exclude Qualcomm's software HEVC decoder which chokes on our streams 2017-05-17 19:41:43 -07:00
Cameron Gutman
9ae1fe2696 Version 5.0.0 2017-05-15 23:49:01 -07:00
Cameron Gutman
6d0f34e2c4 Version 4.8.5 2017-05-15 23:30:30 -07:00
Cameron Gutman
f7d91b5107 Merge remote-tracking branch 'origin/master' into new-core 2017-05-15 23:23:45 -07:00
Cameron Gutman
a3c95480d8 Enable reference frame invalidation for recent Qualcomm and NVIDIA decoders 2017-05-15 23:23:17 -07:00
Cameron Gutman
732311c2a4 Fix codec display after streaming and restore polling behavior of non-direct submit decoders 2017-05-15 21:41:41 -07:00
joeyenfield
043c9a978e Fix issue with ipega controller not capturing keypresses on Samsung phones. (#386) 2017-05-15 18:07:54 -07:00
Cameron Gutman
36b248be4b Fix logging and deadlock on stream termination 2017-05-15 01:06:35 -07:00
Cameron Gutman
8e247ad9a6 Basic streaming working with new-core 2017-05-15 00:31:03 -07:00
Cameron Gutman
a2de98c91a JNI code complete 2017-05-14 23:08:21 -07:00
Cameron Gutman
81d1e615bf Adapt to new-core reworking of moonlight-common (likely buggy) 2017-05-14 17:14:45 -07:00
Cameron Gutman
244fae07ab Update gradle and build tools 2017-05-14 15:11:21 -07:00
Cameron Gutman
e7d96f0ac2 Explicitly set resizeableActivity=true so DeX will let us run in a resizeable window 2017-05-13 10:33:47 -07:00
Cameron Gutman
4555b3c74c Move JNI libraries over to moonlight-common/new-core 2017-05-12 18:57:26 -07:00
Cameron Gutman
8c13186757 Ignore iml files 2017-05-12 18:23:53 -07:00
Cameron Gutman
feafc4ef3c Get build working with AAR moonlight-common 2017-05-12 18:22:28 -07:00
Cameron Gutman
5c03295478 Add moonlight-common submodule 2017-05-12 17:48:33 -07:00
Cameron Gutman
dc3a923041 Bump version to 4.8.4 2017-05-11 23:20:43 -07:00
Cameron Gutman
eccba807bc Update gradle 2017-05-04 23:02:17 -07:00
Cameron Gutman
35fa8f5bcc Fix keyboard arrow keys being sent as gamepad d-pad events 2017-05-04 23:00:47 -07:00
laurentquark
0380910588 Add French language support (cleaned up by me) 2017-05-04 22:41:09 -07:00
Cameron Gutman
e85bb4372e Fix some build warnings and errors with the Dutch translation 2017-05-04 22:33:42 -07:00
Subject
2c345cd6c2 Update: Dutch Translation #1 (#261)
* Halfway through string translation

* Fixed up translation -- Ready for pull request

* Updated Translation to comply with Moonlight update with H265

* 4k & other languages option added, Matched with english strings.xml completely. Ready for pull
2017-05-04 22:30:53 -07:00
Cameron Gutman
b5c96cbb53 Fix manually switching language to Chinese 2017-05-04 22:24:18 -07:00
James Liu
b21ee5ca31 Add Chinese Translation (#345)
* Chinese Translation

I have made a Chinese Translation which contains both Simplified and
Traditional.But I donk't know what the heck is going on.
Now the Simplied one works perfectly but the Tradinional one cannot work
at all,It will turns to English......

* Some Fixes
2017-05-04 22:04:52 -07:00
Cameron Gutman
9c7bff6c75 Merge branch 'master' of git://github.com/Nyaran/moonlight-android into Nyaran-master 2017-05-04 21:43:31 -07:00
Phonedolly
3d470d9aed add korean supports (#338)
* add korean supports

It might have some typos.

* translated one more sentence

* some fixes

"..." was replaced “…” with ellipsis character and some was fixed

* few modifications
2017-05-04 21:35:06 -07:00
Cameron Gutman
b2a36c2c73 Use app context for getting WiFi service to address warnings in new build tools 2017-03-10 22:18:23 -08:00
Cameron Gutman
7978687bfc Update gradle and gradle wrapper 2017-03-10 22:08:09 -08:00
Cameron Gutman
f612ec80e2 Fix active gamepad mask when multi-controller is disabled 2017-02-06 19:26:05 -08:00
Cameron Gutman
7df1a39fcb Update common jar to allow the client to tell the host which controllers are attached 2017-02-04 21:02:11 -08:00
Cameron Gutman
a539ac62ec Version 4.8.3 2017-01-02 19:03:14 -08:00
Cameron Gutman
fa52e5edc2 Remove automatic disabling of back button due to false-positives 2017-01-02 19:02:30 -08:00
Cameron Gutman
3ca681f050 Set isGame to get lower video processing latency on some Android TVs 2017-01-02 18:52:20 -08:00
Cameron Gutman
8086c3d46b Bump version to 4.8.2 2016-12-13 21:28:45 -08:00
Cameron Gutman
928fca843f Update moonlight-common to support GFE 3.2 2016-12-13 21:27:28 -08:00
Cameron Gutman
25d74785d0 Update build tools to 25.0.2 2016-12-13 20:54:24 -08:00
Cameron Gutman
e12a8e7946 Update Gradle to 2.2.3 2016-12-13 20:51:39 -08:00
colin-foster-in-advantage
b14f2ce219 Fixed typo in NAL parser (#311)
Added a missing "()" in the NAL parser script
2016-12-06 09:36:17 -08:00
Cameron Gutman
d31be3d64e Prevent the help activity from reloading across config changes 2016-11-24 11:25:08 -08:00
Cameron Gutman
0704f2aaf6 Set noHistory for the Game activity 2016-11-24 11:23:18 -08:00
Cameron Gutman
832e52ac74 Reload PcView and AppView if the locale changes 2016-11-24 11:22:06 -08:00
Cameron Gutman
f5444551b2 Avoid looping when the thread is trying to be interrupted 2016-11-22 23:20:00 -08:00
Cameron Gutman
3143797b55 Fix transparent background when switching apps in multi-window 2016-11-22 23:18:55 -08:00
Cameron Gutman
cc9b1aeaab Use a MediaCodecInfo object to describe a codec rather than a codec name 2016-11-20 17:56:53 -08:00
Cameron Gutman
3d177e97e4 Add support for displaying the rendered frame times 2016-11-17 23:34:56 -08:00
Cameron Gutman
6c3aaedc83 Version 4.8.1 2016-11-13 20:30:26 -08:00
Cameron Gutman
bf84ebef6d Fix help launch crash with MxPlayer acting as default browser 2016-11-13 20:29:34 -08:00
Cameron Gutman
8991b29329 Run at maximum decoder operating rate on M 2016-11-13 20:26:58 -08:00
Cameron Gutman
fa84575be5 Increment version code 2016-11-11 14:04:28 -08:00
Cameron Gutman
0432d5725b Properly handle Xbox One S controllers with updated firmware 2016-11-11 14:03:20 -08:00
Cameron Gutman
8e7b144339 Increment version code 2016-11-10 19:58:14 -08:00
Cameron Gutman
fc629db653 Improve shortcut handling when the list is full 2016-11-10 19:45:17 -08:00
Cameron Gutman
d5863e1bef Only try twice before initially reporting a PC as offline 2016-11-09 21:08:27 -08:00
Cameron Gutman
c2c3a6b37c Increment version code 2016-11-07 19:00:39 -08:00
Cameron Gutman
e701699dea Improve launcher shortcut backstack and stop leaking a ServiceConnection 2016-11-07 18:58:59 -08:00
Cameron Gutman
17179bd027 Update version to 4.8 2016-11-06 01:08:56 -07:00
Cameron Gutman
b2f210700d Update common with mouse move trick 2016-11-06 01:06:07 -07:00
Cameron Gutman
f0e85c4c53 Fix some additional launcher shortcut issues 2016-11-06 00:43:18 -07:00
Cameron Gutman
92f8425ace Better handle offline PCs in launcher shortcuts 2016-11-06 00:24:04 -07:00
Cameron Gutman
6ad001e8be Update help viewer for Amazon devices 2016-11-06 00:01:37 -07:00
Cameron Gutman
b6e4d5528b Fix and enable launcher shortcuts on Android 7.1 2016-11-05 21:29:59 -07:00
Cameron Gutman
0f0b83badc Properly invalidate pairing state after pairing 2016-11-05 20:20:09 -07:00
Cameron Gutman
453fbb5f58 Use scancode mapping for DS4 for consistent mappings across devices 2016-11-05 20:08:42 -07:00
Cameron Gutman
e7dc3a4c11 Timeout polling results after 30 seconds 2016-11-05 19:56:54 -07:00
Cameron Gutman
d68b2382cf Integrate help buttons into dialogs and the PcView 2016-11-05 19:51:43 -07:00
Cameron Gutman
1b5330323c Fix root mouse capture on Nougat 2016-11-04 00:14:05 -07:00
Cameron Gutman
8aba4888e1 Fix being unable to press the same key down on different gamepads at the same time 2016-10-30 15:26:52 -07:00
Cameron Gutman
1c3b9a3859 Fix race condition in USB driver initialization and cleanup 2016-10-27 18:09:33 -07:00
Cameron Gutman
e8f04f5a3b Cleanup ShortcutHelper 2016-10-26 12:28:29 -07:00
Cameron Gutman
56b814e877 Bump versioncode 2016-10-21 16:22:07 -07:00
Cameron Gutman
628ccd39d6 Fix default context getting picked up falsely during gamepad removal 2016-10-21 16:19:05 -07:00
Cameron Gutman
59db3f9b62 Fix Xbox button behavior quirks on Xbox One S controller 2016-10-21 15:57:00 -07:00
Cameron Gutman
416f922b56 Fix triggers stuck at 50% after controller reconnect 2016-10-21 14:54:21 -07:00
Cameron Gutman
b52a86e6cc Fix app grid isRunning icon not displaying 2016-10-21 14:04:31 -07:00
Cameron Gutman
e523b5069e Version 4.7.3 2016-10-21 13:48:03 -07:00
Cameron Gutman
e8ae8d9807 Manually set pairing state after pairing 2016-10-21 13:47:03 -07:00
Cameron Gutman
64e56a861d Ignore case when sorting apps and PCs 2016-10-21 13:44:59 -07:00
Cameron Gutman
c1bcd09c9b Disable launcher shortcuts pending further work 2016-10-21 12:52:37 -07:00
Cameron Gutman
574258804f Update to AS 2.2.2 and SDK 25 2016-10-21 12:50:04 -07:00
Cameron Gutman
21ea3d8a2b Fix 3rd party Xbox controller d-pads 2016-10-21 12:38:55 -07:00
Cameron Gutman
6de4288a85 Fix running app state on GFE 3.1 2016-10-21 12:28:15 -07:00
Cameron Gutman
a107b5e652 Add launcher shortcuts and fix duplicate pairing error 2016-10-20 13:09:24 -07:00
Cameron Gutman
b02db2c182 Fix JNI build warnings with modern NDKs 2016-10-19 20:47:23 -07:00
Cameron Gutman
f8a04cda7a Version 4.7.2 2016-10-05 18:53:36 -07:00
Cameron Gutman
226e8edefc Update common jar to 55f0114 2016-10-05 18:49:57 -07:00
Cameron Gutman
9b90b30a1f Update build tools and version code 2016-09-28 22:31:55 -07:00
Cameron Gutman
2ed245b25a Add some extra text for GFE 3.0, since GameStream isn't on by default 2016-09-28 22:30:05 -07:00
Cameron Gutman
4b769839d0 Update common jar to 3c56730 2016-09-24 20:57:19 -07:00
Cameron Gutman
239dd1d5a1 Only display box art progress bar if a network load is required 2016-09-20 12:11:06 -07:00
Cameron Gutman
37509cce9b Update common jar to a76df84 2016-09-20 11:29:13 -07:00
Cameron Gutman
227c71549b Migrate project and NDK build to Android Studio 2.2 2016-09-20 11:26:56 -07:00
Cameron Gutman
a10d8334f3 Update Gradle 2016-08-26 22:02:51 -07:00
Nyaran
f88c9904fb Added support for Spanish language 2016-08-14 11:08:30 +02:00
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
Cameron Gutman
e7e73aa1d2 Bump version to 4.0.2 2015-12-21 15:28:38 -08:00
Cameron Gutman
394221f3df Use file locks to synchronize stdout instead of a pthread mutex 2015-12-21 15:07:37 -08:00
Cameron Gutman
7d2647f830 Set the shutdown flag before killing the reader 2015-12-21 15:04:22 -08:00
Cameron Gutman
563c90a8c4 Build native binaries for all modern ABIs 2015-12-21 15:03:14 -08:00
Cameron Gutman
0e0352fdd6 Disable HEVC on NVIDIA hardware until the 16 frame buffering problem can be solved 2015-12-21 15:02:50 -08:00
Cameron Gutman
d6a8db97d8 Rewrite root input capturing to be compatible with Android 6.0 (and be much more secure in general) 2015-12-19 23:55:34 -08:00
Cameron Gutman
05f8fa21de Update version 2015-12-17 03:35:51 -08:00
Cameron Gutman
ab8779086b Fix broken video on Galaxy S5 and Note III 2015-12-17 03:35:39 -08:00
Cameron Gutman
ed8305b199 Revert "Blacklist the whole device from HEVC decoding if Qualcomm's HEVC hybrid decoder is found"
This reverts commit 3c2dd88fd3.
2015-12-17 02:23:22 -08:00
Cameron Gutman
1def825c7f Bump version to 4.0.1 2015-12-16 19:32:08 -08:00
Cameron Gutman
3c9b5d3b17 Update common 2015-12-16 19:31:24 -08:00
Cameron Gutman
3c2dd88fd3 Blacklist the whole device from HEVC decoding if Qualcomm's HEVC hybrid decoder is found 2015-12-16 19:20:00 -08:00
Cameron Gutman
0e21d5e166 Enable Amlogic and Rockchip decoders for HEVC 2015-12-16 18:46:37 -08:00
Cameron Gutman
8c221bd786 Remove the decoder option preference 2015-12-16 18:36:20 -08:00
Cameron Gutman
3b1fcdfb10 Display an error dialog if we can't find an H.264 decoder 2015-12-16 18:30:53 -08:00
Cameron Gutman
9bb91e1085 Remove FFMPEG decoding and supporting code 2015-12-16 18:21:11 -08:00
Cameron Gutman
98bee122fe Don't report any HEVC decoders on pre-Lollipop devices 2015-12-16 17:14:16 -08:00
Cameron Gutman
6aaa9a83a6 Bump version to 4.0 2015-12-13 13:31:37 -08:00
Cameron Gutman
2eaea8ce7c Update common 2015-12-13 13:26:00 -08:00
Cameron Gutman
f5ded03b9b Add a line break to avoid "H.264" being split between 2 lines in the toast 2015-12-13 13:25:53 -08:00
Cameron Gutman
f509a4b3ab Don't use HEVC on the Shield Tablet by default (until further performance testing) 2015-12-13 13:20:11 -08:00
Cameron Gutman
3f46485382 Add support for streaming H.265 from Maxwell 2 cards 2015-12-12 21:11:08 -08:00
Cameron Gutman
2c5e6c0788 Merge branch '4k' 2015-12-11 23:48:29 -08:00
Cameron Gutman
a7d4a04ac2 Missed the SPS replay code when fixing the Annex B escape sequence issues 2015-11-20 18:57:23 -08:00
Cameron Gutman
d199c1b6c4 Merge branch 'master' into 4k
Conflicts:
	app/build.gradle
	app/libs/limelight-common.jar
2015-11-11 17:36:08 -08:00
Cameron Gutman
92f24d20db Bump version to 3.1.13 2015-11-11 17:28:42 -08:00
Cameron Gutman
0dd43df7aa Update common for GFE 2.8 support 2015-11-11 17:28:32 -08:00
Cameron Gutman
1675586a29 Add uses-feature for USB host 2015-11-11 17:28:10 -08:00
Cameron Gutman
a1e511b19a Remove ACCESS_SUPERUSER permission since it's deprecated in SuperSU 2015-11-11 17:27:55 -08:00
Cameron Gutman
5606ed1308 Update version to 3.5-beta4 2015-11-08 19:18:06 -08:00
Cameron Gutman
a301575dd7 Merge branch 'master' into 4k
Conflicts:
	app/build.gradle
	app/src/main/java/com/limelight/preferences/PreferenceConfiguration.java
2015-11-08 19:08:13 -08:00
Cameron Gutman
e89e803d54 Zero controller values before removing a controller 2015-11-08 19:05:22 -08:00
Cameron Gutman
4486a126ad Fix some listener bugs in the XB1 driver 2015-11-08 19:03:12 -08:00
Cameron Gutman
d740e7a521 Add an Xbox One controller driver developed based on the xpad driver in the Linux kernel 2015-11-08 16:12:18 -08:00
Cameron Gutman
cb8eab443c Bump version to 3.5-beta3 2015-10-31 18:34:31 -07:00
Cameron Gutman
fe3b649fe9 Bump version to 3.1.12 2015-10-31 17:07:55 -07:00
Cameron Gutman
51c85a1b10 Merge branch 'master' into 4k
Conflicts:
	app/libs/limelight-common.jar
2015-10-31 16:46:46 -07:00
Cameron Gutman
7223efb9f8 Update common to fix video corruption bugs 2015-10-31 16:45:40 -07:00
Cameron Gutman
c3296cce3d Use setFixedSize if aspect ratios are compatible. This seems necessary for 4K video. 2015-10-31 16:25:15 -07:00
Cameron Gutman
74ea87676e Merge branch 'master' into 4k
Conflicts:
	app/libs/limelight-common.jar
2015-10-31 15:44:36 -07:00
Cameron Gutman
5ef20aba21 Decrease polling period and increase polls before declaring the machine offline. Try requesting the app list again every 2 seconds if the app list has not been received yet. 2015-10-28 01:36:35 -07:00
Cameron Gutman
54eaee3f79 Use a lock to prevent serverinfo polling on a machine while applist is pending 2015-10-28 01:15:09 -07:00
Cameron Gutman
4c82da1f5c Update common with image quality improvements 2015-10-28 00:42:24 -07:00
Cameron Gutman
080dc01c21 Use a reference resolution rather than the actual stream resolution when scaling mouse movement 2015-10-28 00:24:26 -07:00
Cameron Gutman
f09fbf4ba6 Fix incorrect usage of SeqParameterSet.read() by feeding it possibly escaped Annex B NALUs 2015-10-28 00:24:16 -07:00
Cameron Gutman
9d1510f14d Use setFixedSize if aspect ratios are compatible. This seems necessary for 4K video. 2015-10-27 00:04:28 -07:00
Cameron Gutman
62ea92335d Use a reference resolution rather than the actual stream resolution when scaling mouse movement 2015-10-26 23:59:53 -07:00
Cameron Gutman
d1e2822b92 Update version to 3.5-beta1 2015-10-23 14:47:35 -07:00
Cameron Gutman
533cb747df Fix incorrect usage of SeqParameterSet.read() by feeding it possibly escaped Annex B NALUs 2015-10-23 14:46:03 -07:00
Cameron Gutman
33a0f9c97f Add 4K resolutions 2015-10-23 13:52:09 -07:00
Cameron Gutman
ef9a442718 Add 5.1 options in settings 2015-10-23 13:49:12 -07:00
Cameron Gutman
ad10413714 Update decoder code 2015-10-19 22:37:46 -07:00
Cameron Gutman
c9014da186 Transition to Opus Multistream Decoder API 2015-10-17 17:16:58 -07:00
Cameron Gutman
c025f9f02b Reduce code duplication 2015-10-17 15:44:24 -07:00
Cameron Gutman
b737acedb0 Bump version to 3.1.11 2015-10-15 02:00:29 -07:00
Cameron Gutman
f15bfe3038 Add support for mouse drag using long press 2015-10-15 01:50:05 -07:00
Cameron Gutman
8938f51292 Fix weird stair-stepping upward mouse movement on devices with a low scaling factor caused by rounding error (Nexus 9) 2015-10-15 01:48:31 -07:00
Cameron Gutman
4b92b8f714 Fix bug allowing computer polling to continue when the stream is resumed from the PcView activity 2015-10-15 00:55:05 -07:00
Cameron Gutman
5f13b9bca4 Don't set constraints 4 & 5 when using baseline profile hack 2015-10-13 19:29:36 -07:00
Cameron Gutman
2f219aac6f Only apply the constrained high profile SPS modification to Intel devices to avoid crashing other devices 2015-10-12 20:54:50 -07:00
Cameron Gutman
1d9efb30e2 Update to version 3.1.10 2015-10-11 17:11:48 -07:00
Cameron Gutman
ed7be00881 Update IML file 2015-10-11 17:11:08 -07:00
Cameron Gutman
a6003f6bff Remove MediaTek decoders from the decoders that need bitstream restrictions. The correct fix was to lower level_idc to reduce the required buffering. On newer MediaTek chipsets, sending bitstream restrictions actually slows down decoding by a factor of 3. 2015-10-11 16:57:37 -07:00
Cameron Gutman
4619045375 Revert "Update common to increase SSL handshake timeout"
This reverts commit 57b0da1a3a.
2015-10-11 14:54:45 -07:00
Cameron Gutman
e61b8f1b34 Try a TCP connection before trying HTTPS to quickly eliminate transport layer connectivity issues 2015-10-11 14:39:02 -07:00
Cameron Gutman
79b6ec839a Fix machines becoming unreachable after they report IP addresses that they can't be contacted with 2015-10-11 14:16:38 -07:00
Cameron Gutman
fd12e30c53 Set constraint flags corresponding to Constrained High Profile on KitKat and higher. Fixes Nexus Player high latency on Android 6.0. 2015-10-10 23:28:48 -07:00
Cameron Gutman
87a9ca4318 Make touchscreen and stylus support more robust (supporting Bluetooth stylus in 6.0 and hopefully fixing broken touchscreen input on some devices) 2015-10-10 19:17:19 -07:00
Cameron Gutman
3f64411174 Only reload the PcView activity if UI settings were changed 2015-10-10 18:53:10 -07:00
Cameron Gutman
57b0da1a3a Update common to increase SSL handshake timeout 2015-10-10 18:15:01 -07:00
Cameron Gutman
7d3e74a67f Allow the offline context menu to be opened when the PC state is unknown 2015-10-10 18:10:57 -07:00
Cameron Gutman
d704e322df Use RGB_565 for box art to reduce image size in memory 2015-10-10 18:09:51 -07:00
Cameron Gutman
f598153818 Small improvements to Media Codec DR 2015-10-10 15:17:24 -07:00
Cameron Gutman
f395a0c170 Fix warning 2015-10-10 15:04:49 -07:00
Cameron Gutman
654b33d27f Update build tools version to 23.0.1 2015-10-10 15:04:16 -07:00
Cameron Gutman
6c12da96c9 Add patched Jcodec library built from master a5d138efec2e940897e7e3d91a63a1f58abedd95 with changes from https://github.com/jcodec/jcodec/pull/90 2015-10-10 15:03:47 -07:00
Cameron Gutman
1a6f639b81 Fix discovery issues when adding a PC 2015-10-10 14:43:29 -07:00
Cameron Gutman
59a00a38c9 Limit box art assets to 5 MB each to prevent OOM crashes 2015-10-10 14:43:17 -07:00
Cameron Gutman
2beee168e3 Update README.md with additional download links 2015-08-30 12:37:23 -07:00
Cameron Gutman
a92bbc7e5a Increment version to 3.1.9 2015-08-18 00:44:01 -07:00
Cameron Gutman
fbc921dd07 Update build files 2015-08-18 00:39:01 -07:00
Cameron Gutman
59c6c3d777 Target Android 6.0 SDK 2015-08-18 00:38:49 -07:00
Cameron Gutman
e7ab61c8d0 Use jmDNS 3.4.2 which works properly on Android 6.0 2015-08-17 23:14:20 -07:00
Cameron Gutman
7023760782 Use monotonic system time for rendering timestamps. This is required now in Android 6.0 since these timestamps are propagated to the codec. 2015-08-17 18:40:25 -07:00
Cameron Gutman
932ce435b5 Remove unnecessary buffer clear 2015-08-17 18:26:33 -07:00
Cameron Gutman
af384d88f7 Handle potentially poisoned bitmap cache caused by truncated images from the server 2015-08-17 17:59:15 -07:00
Cameron Gutman
792846ddad Update MediaCodec renderer to avoid deprecated features on Lollipop 2015-08-17 17:52:57 -07:00
Cameron Gutman
1187d9c78c Update libraries 2015-08-14 09:29:56 -07:00
Cameron Gutman
37db9ab072 Update common with latest fixes 2015-08-12 00:39:07 -07:00
Cameron Gutman
fb40060560 Upgrade common jar to incorporate IDR frame fix 2015-08-04 23:46:20 -07:00
Cameron Gutman
a4f4887647 Upgrade build tools and libraries 2015-08-04 23:46:03 -07:00
Cameron Gutman
f1d7f556fd Bump to version 3.1.8 2015-07-21 18:03:37 -07:00
Cameron Gutman
1e70e1d329 GFE 2.5.11 update to fix black screen on Fire TV Stick 2015-07-18 17:06:41 -07:00
Cameron Gutman
e02a009635 Add support for the Razer Serval controller. The start and select buttons are manually handled for devices without a mapping for them. The back button is ignored so it can be used to exit the stream. 2015-07-18 00:46:25 -07:00
Cameron Gutman
bd6ff35603 Update to 3.1.7 2015-06-15 10:37:58 -07:00
Cameron Gutman
1cb7727dc7 Update common 2015-06-15 10:28:31 -07:00
Cameron Gutman
0c73e3d0ae Only propagate a decoder exception if it happens at the beginning of a stream 2015-06-15 10:28:09 -07:00
Cameron Gutman
6371d364e1 Lint warning cleanup 2015-05-29 23:22:40 -05:00
Cameron Gutman
ded9c9140d Handle being online but not having a known reachability 2015-05-29 23:20:04 -05:00
Cameron Gutman
7c8a108e28 Use the leanback feature on API 21+ devices 2015-05-29 23:18:56 -05:00
Cameron Gutman
2a18ffcdba Update to Gradle 1.2.3 2015-05-19 10:10:18 -05:00
Cameron Gutman
381d0d5e81 Add support for multi-window functionality on Samsung devices 2015-05-10 00:02:04 -05:00
Cameron Gutman
be126acfd1 Update version info to 3.1.6 2015-05-05 20:52:53 -04:00
Cameron Gutman
fc2f5cfe4d Manually pass through Samsung capacitive buttons 2015-05-05 20:20:37 -04:00
Cameron Gutman
9878902a89 Use IDs to track controllers instead of descriptors. Fixes #64 2015-05-05 20:08:58 -04:00
Cameron Gutman
f1230d46f3 Android Studio 1.2 and Grade 1.2.2 update 2015-05-05 20:02:53 -04:00
Michelle Bergeron
d8822392f1 Link to site 2015-05-03 23:36:19 -07:00
Cameron Gutman
1d9cf71517 Total Eclipse of the Lime 2015-04-21 21:50:40 -04:00
Cameron Gutman
2160e87fef Fix division by zero in ARC 2015-03-31 20:29:22 -04:00
Cameron Gutman
88249ba8aa Enable direct submission for ARC 2015-03-31 19:59:16 -04:00
Cameron Gutman
2856617fb3 Only release controller numbers if they were reserved 2015-03-31 19:58:47 -04:00
Cameron Gutman
d822980d5a Fix missing close of Closeables caught by StrictMode 2015-03-29 23:25:00 -04:00
Cameron Gutman
b5ba59b413 Fix database reference leak 2015-03-29 23:06:32 -04:00
Cameron Gutman
1148e0163c Only assign a controller number when a valid controller input has been received. Fixes misdetection of other input devices as controllers (issue #65). 2015-03-29 22:54:48 -04:00
Cameron Gutman
cf36c7adb1 Increment version 2015-03-25 02:33:46 -04:00
Cameron Gutman
eac6998e17 Update the latency message strings to be more clear that this isn't end to end latency 2015-03-25 01:20:55 -04:00
Cameron Gutman
17afbffdb5 Include the time it takes to get an input buffer in the frame latency calculation 2015-03-25 01:08:23 -04:00
Cameron Gutman
072a439c2d Update common and decode unit API 2015-03-25 00:32:22 -04:00
Cameron Gutman
c533600983 Update for 3.1.3 release 2015-03-23 17:26:37 -04:00
Cameron Gutman
5847fbb6b6 Add TI decoders to the direct submit whitelist 2015-03-23 17:14:02 -04:00
Cameron Gutman
1876b30c1b Forgot this file 2015-03-23 16:51:57 -04:00
Cameron Gutman
5c71f55993 Add another Exynos prefix 2015-03-23 16:51:32 -04:00
Cameron Gutman
9c0960d03d Add options to quit and resume streaming from the PC view 2015-03-23 16:36:43 -04:00
Cameron Gutman
29a395f3f4 Prevent updating the UI while quitting is in progress 2015-03-23 15:57:29 -04:00
Cameron Gutman
a676b8d8e6 Restore the legacy path and only use direct submit for certain whitelisted decoders 2015-03-23 15:51:11 -04:00
Cameron Gutman
7ab0be3b62 Optimize app grid performance on lower end devices 2015-03-23 15:12:25 -04:00
Cameron Gutman
115853fed2 Update version to 3.1.3-beta1 2015-03-16 21:29:07 -04:00
Cameron Gutman
60beb81ae4 Target API 22 2015-03-16 21:28:49 -04:00
Cameron Gutman
5310375d42 Target Android 5.1 2015-03-16 21:28:33 -04:00
Cameron Gutman
7ce29e3a09 Add a workaround for the Nexus 9 dropping frames with the new renderer 2015-03-16 21:26:02 -04:00
Cameron Gutman
42c65f4f16 Use smaller deadzones for SHIELD controllers 2015-03-16 19:36:09 -04:00
Cameron Gutman
bf2cc2a4d5 Don't assign controller numbers to devices that don't have an analog stick 2015-03-16 19:35:43 -04:00
Cameron Gutman
6d6d7121f6 Remove the Playpad Pro hack that worked around an issue with old firmware and caused the D-pad to be unresponsive on updated firmware. Fixes #41 2015-03-15 14:30:56 -07:00
Cameron Gutman
2ab67380d6 Use direct submit decoding for MediaCodec. Based on my profiling of a few devices, dequeueInputBuffer and queueInputBuffer don't take much time anyway. It allows us to stop our semi-busy looping which saves power. The depacketizer can avoid expensive synchronization and additional context switching which costs time and CPU cycles. 2015-03-09 01:49:52 -05:00
Cameron Gutman
899387caa1 Use a separate executor for network loads to avoid stalling cached loads. Optimize background cache fill loads. 2015-03-02 18:34:21 -05:00
Cameron Gutman
56c8a9e6fe Use the regular serverinfo query to update the running status of apps 2015-03-02 17:05:45 -05:00
Cameron Gutman
896288a40b Use AsyncTasks and attached Drawables to track background image loading 2015-03-02 17:03:08 -05:00
Cameron Gutman
fc8ce5e4b9 Quiet down disk cache misses 2015-03-02 16:13:54 -05:00
Cameron Gutman
4affc3c4ce Update to 3.1.2 release 2015-02-27 18:12:49 -05:00
Cameron Gutman
067be54715 Show the discovery in progress view if no computers remain after one is deleted 2015-02-27 18:05:02 -05:00
Cameron Gutman
0dad2dc64b Only close the app list activity if the PC is offline not unknown 2015-02-27 15:15:01 -05:00
Cameron Gutman
867b703644 Evict cached bitmaps when closing the app list 2015-02-27 15:13:43 -05:00
Cameron Gutman
3d398ef6dd Update common 2015-02-27 14:22:35 -05:00
Cameron Gutman
85d95b2d8e Revert "Immediately show the PC as offline if the first poll fails"
This reverts commit 7b12fd1ad2.
2015-02-27 13:52:17 -05:00
Cameron Gutman
d091d9db6b Start apps by ID to work correctly with duplicate app names 2015-02-27 13:42:40 -05:00
Cameron Gutman
e081ab5239 Code cleanup and Lint suggestions 2015-02-27 01:43:24 -05:00
Cameron Gutman
7b12fd1ad2 Immediately show the PC as offline if the first poll fails 2015-02-27 01:33:33 -05:00
Cameron Gutman
80d8c5953e Rewrite the app art caching and fetching (again!) to finally address OOM problems and speed up art loading 2015-02-27 01:16:06 -05:00
Cameron Gutman
194037ff41 Clear the bitmap cache since it can get pretty large 2015-02-26 22:04:39 -05:00
Cameron Gutman
094d642739 Stop scaling bitmaps down 2015-02-26 22:04:22 -05:00
Cameron Gutman
010e03252e Encapsulate the cache IO streams in buffered streams 2015-02-26 21:39:26 -05:00
Cameron Gutman
98638186b5 Use weak references to allow the image views to be garbage collected while a load is in progress 2015-02-26 21:05:33 -05:00
Cameron Gutman
c5293ef21f Reduce the size of the LRU cache by 2 2015-02-26 21:04:40 -05:00
Cameron Gutman
366a1c91b8 Always close the output stream 2015-02-26 21:04:17 -05:00
Cameron Gutman
157450e674 Update common with stricter applist parser 2015-02-26 18:33:18 -05:00
Cameron Gutman
1b8d2bc81c Cancel asset fetching when the app view is paused 2015-02-26 18:30:02 -05:00
Cameron Gutman
f1787c43e5 Generalize the polling grace period to all users of CMS 2015-02-26 18:27:50 -05:00
Cameron Gutman
95ea88e932 Only replace the MAC address if the existing one is non-null 2015-02-26 15:11:46 -05:00
Cameron Gutman
f2b8461bb9 Increment version to beta 3.1.2 2015-02-25 23:15:10 -05:00
Cameron Gutman
7838a787df Fix a bug in app removal 2015-02-25 22:27:38 -05:00
Cameron Gutman
cc3f2ecb07 Always close the cache output stream if an exception occurs 2015-02-25 22:15:41 -05:00
Cameron Gutman
833b7c3916 Fetch app assets in the background while in the app view 2015-02-25 21:57:54 -05:00
Cameron Gutman
90209f2ca2 Update iml files generated by Android Studio 1.1 2015-02-25 21:07:59 -05:00
Cameron Gutman
2681036c32 Update common 2015-02-25 21:07:44 -05:00
Cameron Gutman
ee58071ff1 Fix huge performance issues when dealing with large app lists 2015-02-25 21:07:35 -05:00
Cameron Gutman
e222f2f6c3 Fix fast polling 2015-02-22 18:34:28 -05:00
Cameron Gutman
0b7becb161 Remove unused function 2015-02-22 18:10:08 -05:00
Cameron Gutman
bf795ab7a5 Fix removal of apps in app list updates 2015-02-22 18:04:42 -05:00
Cameron Gutman
59df38ae8a Cancel app icon requests when the view is recycled 2015-02-22 17:49:52 -05:00
Cameron Gutman
e04ff048b8 Implement a fast polling method to speed up polling. Save the old MAC address if it's empty. 2015-02-11 17:04:31 -05:00
Cameron Gutman
7d25d07c6d Update version to 3.1.1 2015-02-11 00:55:47 -05:00
Cameron Gutman
7b0ddfae42 Update iml files 2015-02-11 00:34:56 -05:00
Cameron Gutman
43fa1a7245 Update common to fix null app name issue 2015-02-09 00:12:29 -05:00
Cameron Gutman
057530eed0 Correctly identify computers that are the same 2015-02-08 23:46:03 -05:00
Cameron Gutman
aee34f6365 Remove redundant null checks 2015-02-08 23:44:33 -05:00
Cameron Gutman
5519d92243 Disable the start key shortcut to start the keyboard because the keyboard can't receive input after it's started 2015-02-07 13:58:53 -05:00
Cameron Gutman
3d95ac1f93 Fix keyboard dismissal on Fire TV devices 2015-02-07 13:42:49 -05:00
Cameron Gutman
5c938535be Fix app list focus issues with remotes/gamepads 2015-02-07 13:20:01 -05:00
Cameron Gutman
2fdecc551a Tabs -> Spaces 2015-02-07 11:54:46 -05:00
Cameron Gutman
10204afdb4 Only add PCs to the computer list when they have been polled once to get a UUID for equality comparison. Fix equality comparison in PcView to avoid duplicate PCs enumerated over mDNS. 2015-02-07 11:44:56 -05:00
Cameron Gutman
55c800c2a5 Fade in box art when scrolling 2015-02-07 06:52:28 -05:00
Cameron Gutman
265b3f9963 Use image alpha to make images transparent while loading 2015-02-07 06:23:35 -05:00
Cameron Gutman
a8bf2cd1cf Fix UI dropped frames when loading images 2015-02-07 06:08:00 -05:00
Cameron Gutman
4fcd8b3dfe Replace unpair option with delete PC 2015-02-07 05:57:30 -05:00
Cameron Gutman
e1a1a6344d Fill the whole height with the list view 2015-02-06 13:38:32 -05:00
Cameron Gutman
a095c10a25 Increment version to 3.1 and update build files 2015-02-05 16:15:16 -05:00
Cameron Gutman
b1ea487e22 Use the mode (power) button on the Asus Nexus Player Gamepad as a select button 2015-02-05 16:06:55 -05:00
Cameron Gutman
47265d0d10 Add another SELinux policy change needed on Nexus 9 2015-02-05 16:06:22 -05:00
Cameron Gutman
6a41b41a38 Merge pull request #55 from Ansa89/italian-translation
Italian translation: update
2015-02-05 13:34:03 -05:00
Cameron Gutman
2247e43a48 Remove unused imports 2015-02-05 13:23:01 -05:00
Cameron Gutman
d3986080a3 Tighten up a bunch of declarations to make Lint happier 2015-02-05 13:21:04 -05:00
Cameron Gutman
07277e1a5b Fix a few Lint warnings 2015-02-05 13:01:35 -05: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
Ansa89
39d7fc748f Italian translation: update 2015-02-03 01:06:36 +01:00
Cameron Gutman
4d3a69cf6a Fix GFE 2.1.x controller regression 2015-02-02 18:10:18 -05:00
Cameron Gutman
b806522751 Unassign the controller number when a device is removed 2015-02-02 02:13:27 -05:00
Cameron Gutman
256fa897a7 Fix build issues 2015-02-01 18:31:34 -05:00
Cameron Gutman
5c812eed6c Beta 2 version update 2015-02-01 18:20:55 -05:00
Cameron Gutman
f0b22f9119 Don't use small mode on TVs 2015-02-01 18:20:39 -05:00
Cameron Gutman
7e1884acb5 Trap Shield's back button as controller 0 2015-02-01 18:07:03 -05:00
Cameron Gutman
9512521783 Update common 2015-02-01 15:06:27 -05:00
Cameron Gutman
da7904a767 Add multiple controller support 2015-02-01 15:06:18 -05:00
Cameron Gutman
3a0c1db168 Merge pull request #54 from Ansa89/italian-translation
Italian translation: update
2015-02-01 00:44:17 -05:00
Cameron Gutman
bd21692323 Properly center text on the app view 2015-02-01 00:43:57 -05:00
Cameron Gutman
5ae245bdca Trim spaces from the IP address 2015-02-01 00:39:47 -05:00
Cameron Gutman
d3052cd97d Set small icon by default on phones 2015-02-01 00:33:43 -05:00
Cameron Gutman
336f85a31c Fix loading bugs with uncached images 2015-01-31 22:14:12 -05:00
Cameron Gutman
b01f7c796e Fix duplicated fragments 2015-01-31 17:19:54 -05:00
Cameron Gutman
56f438fe47 Fix some crashes and caching issues 2015-01-31 17:01:46 -05:00
Cameron Gutman
baa5199b83 Load cached images in the background to avoid stalling the UI thread 2015-01-31 16:59:45 -05:00
Cameron Gutman
23ca62b304 Fix dp constant 2015-01-31 14:00:47 -05:00
Cameron Gutman
2c3511195c Use small mode by default on things smaller than 7 inch tablets 2015-01-31 13:21:21 -05:00
Ansa89
d31ef481f3 Italian translation: update 2015-01-31 10:03:37 +01:00
Cameron Gutman
a490da5e5c Fix some caching bugs 2015-01-31 00:13:51 -05:00
Cameron Gutman
72d3576257 Fix a crash and a hang in the new computer manager code 2015-01-30 19:33:42 -05:00
Cameron Gutman
ebd93a55a0 Fix small icon mode 2015-01-30 19:17:00 -05:00
Cameron Gutman
4d01e1afe6 Stub icon scaling and allow background updating of the applist 2015-01-30 18:49:01 -05: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
Cameron Gutman
9ff1386751 Add a quit confirmation dialog 2015-01-27 15:31:01 -05:00
Cameron Gutman
5fca35f0b1 Sort app list alphabetically 2015-01-26 20:58:33 -05:00
Cameron Gutman
d23c763441 Remove unused imports 2015-01-26 20:50:14 -05:00
Cameron Gutman
fa058c4783 Merge pull request #53 from Ansa89/italian-translation
Italian translation: update
2015-01-26 20:47:00 -05:00
Ansa89
e0ddd5f045 Italian translation: update 2015-01-26 10:53:30 +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
Cameron Gutman
b7443451a4 Fix release build failure for beta 2015-01-25 23:38:33 -05:00
Cameron Gutman
e90e4a22c4 Increment version 2015-01-25 23:35:24 -05:00
Cameron Gutman
3a53172145 Apply list mode preference immediately 2015-01-25 23:28:13 -05:00
Cameron Gutman
1dfcb7bc29 Fix root input device capture on the Nexus 9 2015-01-25 23:16:32 -05:00
Cameron Gutman
897bb76858 Forgot this file 2015-01-25 22:58:17 -05:00
Cameron Gutman
bcc67269ab Add gestures to bring up the software keyboard - Long press start or tap with 3 fingers 2015-01-25 22:55:12 -05:00
Cameron Gutman
4d24c654b9 Remove the old fragment when adding the new one 2015-01-25 22:11:38 -05:00
Cameron Gutman
cba44b091b Add common with GFE 2.1.x backwards compatibility 2015-01-25 21:35:03 -05:00
Cameron Gutman
f2d8f8a41b Update preference text 2015-01-25 21:04:27 -05:00
Cameron Gutman
4b1c7e7e3c Fix state loss crashes 2015-01-25 21:04:13 -05:00
Cameron Gutman
1cba278876 Cache box art locally 2015-01-25 21:00:34 -05:00
Cameron Gutman
766898fdf9 Add list support back for users that don't like the grid 2015-01-25 20:23:35 -05:00
Cameron Gutman
13e91d594b Fix Lint and build issues 2015-01-25 18:50:31 -05:00
Cameron Gutman
ca0a0da19f Fix fusion of computers that were re-added after becoming unreachable 2015-01-25 18:41:44 -05:00
Cameron Gutman
82cabce86e Merge pull request #33 from Ansa89/language_chooser
Add language chooser
2015-01-25 16:46:10 -05: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
Cameron Gutman
51a630995a Update common for GFE 2.2.2+ support 2015-01-22 15:29:58 -05:00
Cameron Gutman
3a74f0726c Updated libs 2015-01-22 15:29:46 -05:00
Cameron Gutman
efa6c7bba0 Fix build of root package 2015-01-22 15:29:16 -05: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
Michelle Bergeron
b8141542f8 Add wiki link 2015-01-17 18:44:31 -08:00
Cameron Gutman
8fc9a90207 Switch back to Maven repos of ion and androidasync packages 2014-12-13 20:41:32 -08:00
Cameron Gutman
13d707d98d Use final release of Gradle 1.0 2014-12-09 01:13:13 -08:00
Cameron Gutman
aae0ff6e7a Migrate the project to Android Studio 1.0 RC4 2014-12-08 00:12:37 -08:00
Cameron Gutman
69c7b5a0d5 Update version 2014-12-03 20:52:52 -08:00
Cameron Gutman
d1ad3115fa Add remote to stream config 2014-12-02 00:55:46 -08:00
Cameron Gutman
770af402a4 Reduce default 1080p60 bitrate to 20 Mbps 2014-12-02 00:55:31 -08:00
Cameron Gutman
3236c0b93a Lower the level_idc of the SPS to the minimum required for streaming at a given resolution 2014-12-01 22:58:52 -08:00
Cameron Gutman
51aacc3f38 Remove extra newlines 2014-12-01 22:39:17 -08:00
Cameron Gutman
397c6f46f9 Fix a security issue which caused input devices to remain world readable after the stream is ended 2014-12-01 22:29:16 -08:00
Cameron Gutman
d00f78f859 Revert square to circle analog work since it seems to be handled correctly already 2014-12-01 22:27:02 -08:00
Cameron Gutman
29fec2e0de Add initial support for rooted devices running Lollipop with SELinux set to enforcing. This should really be improved in the future since we're modifying policies for untrusted_app. 2014-12-01 22:26:35 -08:00
Cameron Gutman
88d28665ef Attempt to fix IndexOutOfBoundsException (index 0 size 0) reported by a couple users 2014-11-30 18:34:34 -06:00
Cameron Gutman
de1f4da258 Apply the square to circle plane mapping before evaluating the deadzone. Cleanup some dead code. 2014-11-30 15:52:49 -06:00
Cameron Gutman
7985be57ab Translate the analog stick values of controllers with "square" analog stick planes (DS3, DS4, and others) to the circular plane that XInput programs expect 2014-11-30 15:35:20 -06:00
Cameron Gutman
a835e7aaa2 Increase DS4 controller responsiveness by ignoring historical values again 2014-11-30 12:34:30 -06:00
Ansa89
22958cfbb1 Language chooser: use constants 2014-11-29 14:40:03 +01:00
Cameron Gutman
c4dc5eb9e1 Update common for faster IDR recovery 2014-11-28 22:17:42 -06:00
Cameron Gutman
db758f386e Comment out unused variable 2014-11-28 22:16:46 -06:00
Cameron Gutman
3fb3eefa94 Fix Nyko Playpad input issue 2014-11-28 22:16:33 -06:00
Ansa89
9340dff45d PreferenceConfiguration.java: add language preference 2014-11-28 10:26:14 +01:00
Cameron Gutman
2d6c756e70 Always consider a PC to be remote if localIP == remoteIP 2014-11-27 21:56:20 -06:00
Cameron Gutman
03e965d449 Merge pull request #34 from Ansa89/italian-translation
Italian translation: better wording
2014-11-27 20:35:57 -06:00
Cameron Gutman
34f72544d8 Increment version 2014-11-25 14:56:40 -08:00
Cameron Gutman
d839ea9781 Increase deadzone on triggers to Xinput defaults and add special handling of the Nexus Player Controller and Nexus Remote 2014-11-25 14:54:36 -08:00
Cameron Gutman
2b7f13fdbb Increase max frame time to improve accuracy of latency counter 2014-11-25 13:34:00 -08:00
Cameron Gutman
7557a3a4ae Don't capture the back button on remotes 2014-11-25 11:16:47 -08:00
Cameron Gutman
fcecba484f Fix a crash caught by Monkey 2014-11-25 02:05:24 -08:00
Cameron Gutman
fa85a0a0bd Improve CPU decoder frame latency when rendering speed is less than decoding speed 2014-11-25 02:04:51 -08:00
Cameron Gutman
dc64bfeba2 Slightly reduce max packet size in an attempt to cut packet losses 2014-11-25 01:05:55 -08:00
Cameron Gutman
871b73c48d Fix PC duplication issue when multiple machines report the same remote IP address 2014-11-24 20:10:02 -08:00
Cameron Gutman
5dcff91d27 Only grab Fire TV remotes if a gamepad isn't attached 2014-11-24 18:43:08 -08:00
Cameron Gutman
0041fc1dab Fix broken de-duplication of computers 2014-11-24 18:25:58 -08:00
Cameron Gutman
314242ab08 Update to Ion with fixes for SSLContext and self-signed certificates 2014-11-24 18:10:23 -08:00
Cameron Gutman
09e8ddfd74 Use the bitstream restrictions fixup on Broadcom VideoCore IV devices 2014-11-24 18:03:47 -08:00
Ansa89
4cea483a87 Italian translation: better wording 2014-11-24 11:53:18 +01:00
Ansa89
99aa616188 Add language chooser
Implement limelight-stream/limelight-android#32.
2014-11-24 11:47:02 +01:00
Cameron Gutman
444c4602c1 Update libraries. Seems to improve image caching behavior with Ion. 2014-11-23 23:39:20 -08:00
Cameron Gutman
5b6eac7140 Update build.gradle for re-release 2014-11-23 02:07:03 -08:00
Cameron Gutman
7cdd184197 Fix null pointer exception on ATV emulator 2014-11-23 02:06:49 -08:00
Cameron Gutman
be153b84cb Update build for final 3.0 release 2014-11-22 23:15:11 -08:00
Cameron Gutman
06c53e2251 Update decoder errata 2014-11-22 22:08:29 -08:00
Cameron Gutman
695519bdf5 Reduce Nexus Player video latency by 10x 2014-11-22 22:05:59 -08:00
Cameron Gutman
bf7d033ab2 Don't use adaptive playback at all to avoid extra added latency on some decoders 2014-11-22 20:35:31 -08:00
Cameron Gutman
df67795c4a Use back as start on Android TV 2014-11-22 19:33:26 -08:00
Cameron Gutman
72c1696f43 Fix missing PCs in PC list after my NPE fix 2014-11-21 22:56:56 -08:00
Cameron Gutman
8eca3683c9 Add method for getting video decoder name 2014-11-21 11:08:35 -08:00
Cameron Gutman
80c17b4913 Update common 2014-11-20 19:22:28 -08:00
Cameron Gutman
e5050f10bb Fix a potential null pointer exception 2014-11-20 19:22:20 -08:00
Cameron Gutman
e912e4de57 Don't do deadzone scaling because the PC should be handling that. Return to non-scaled controller packets. Disable the deadzone option in preferences. 2014-11-20 00:00:48 -08:00
Cameron Gutman
8dee1f0d80 Add a trigger deadzone 2014-11-19 23:59:42 -08:00
Cameron Gutman
53594ada66 Disable the Android TV controller hack for now 2014-11-19 23:27:10 -08:00
Cameron Gutman
848ed1ad72 Scale touch inputs based on the ratio of the stream size to the screen size 2014-11-19 23:26:50 -08:00
Cameron Gutman
307e807c8f Replay motion event history during input processing 2014-11-19 23:08:34 -08:00
Cameron Gutman
6a27780d56 Remove hat flat values 2014-11-19 22:57:17 -08:00
Cameron Gutman
57f98dbb4a Add missing import 2014-11-19 22:07:57 -08:00
Cameron Gutman
5af7d83ec1 Fix RTL Lint warnings by using start/end 2014-11-19 22:06:22 -08:00
Cameron Gutman
4a6f77f43a Remove an unused string 2014-11-19 22:05:51 -08:00
Cameron Gutman
c96f9fb635 Prevent deadzone and bitrate from dropping below 1 2014-11-19 20:11:13 -08:00
Cameron Gutman
e3a477a243 Don't send a bunch of duplicate controller packets if a button is being held down 2014-11-19 19:05:59 -08:00
Cameron Gutman
9fcd641143 Make the back button function as the start button on Android TV controllers (needs testing) 2014-11-19 18:40:22 -08:00
Cameron Gutman
6d1cbc5a64 Add a hack for the Tablet Remote app to fix the B button 2014-11-19 18:39:15 -08:00
Cameron Gutman
ec71060d98 Fix broken keyboards and gamepads when an input device wasn't provided (such as a virtual gamepad or IME) 2014-11-19 18:37:47 -08:00
Cameron Gutman
03f706fb85 Update common 2014-11-19 10:43:09 -08:00
Cameron Gutman
7ad87bd3ee Small fix to the frame timing code 2014-11-19 10:43:00 -08:00
Cameron Gutman
4e088f6183 Fix minor grammar error 2014-11-18 19:09:23 -08:00
Cameron Gutman
1b16ea6f53 Merge pull request #31 from Ansa89/NewUI-italian-translation
NewUI: Add italian translation
2014-11-17 19:51:38 -08:00
Ansa89
f262503bc8 Italian translation: update 2014-11-17 09:50:17 +01:00
Cameron Gutman
b2ba216cd1 Poll every 3 seconds instead of every 5 seconds 2014-11-16 18:14:50 -08:00
Cameron Gutman
94ba7f8e45 Fix a bunch of bugs in the new (and old) computer manager service 2014-11-16 18:09:31 -08:00
Cameron Gutman
a267cf59c7 Increment version 2014-11-16 17:20:27 -08:00
Cameron Gutman
79e8bef289 Update common 2014-11-16 17:20:11 -08:00
Cameron Gutman
99e3b5f33b Rewrite a large portion of the computer manager service to fix some thread leaks and improve performance 2014-11-16 17:20:04 -08:00
Cameron Gutman
afbe64f3ff Remove an unused import 2014-11-16 17:19:07 -08:00
Cameron Gutman
43b1a73ae0 Use a transparent background for the streaming activity to avoid overdraw 2014-11-16 17:18:53 -08:00
Cameron Gutman
d08eeb8a2d Don't display a toast after pairing has completed 2014-11-16 16:37:41 -08:00
Cameron Gutman
7c39e5c974 Fix a race condition 2014-11-16 14:57:54 -08:00
Cameron Gutman
cd49334199 If we've previously been able to reach a machine via a local or remote IP, always try that one first when polling on subsequent tries 2014-11-16 14:35:36 -08:00
Cameron Gutman
dd59f0bc6d Fix app grid UI issues 2014-11-16 14:27:20 -08:00
Cameron Gutman
cf2d83a1ea Fix comment typo 2014-11-16 14:23:58 -08:00
Cameron Gutman
d5b6130936 Use 40% larger packets (1450 bytes) on local networks 2014-11-16 12:09:32 -08:00
Cameron Gutman
4ae29b0075 Improve performance of the CPU decoder and add some details about changing decoders 2014-11-16 11:52:08 -08:00
Ansa89
34e35cd493 Add italian translation 2014-11-14 11:19:03 +01:00
Cameron Gutman
a17af070c5 Condense some text to better fit the UI 2014-11-13 23:30:42 -08:00
Cameron Gutman
fbe0a26800 Select the PC grid when the down button is pressed when focused on one of the buttons 2014-11-13 23:28:23 -08:00
Cameron Gutman
25ad99df94 Update common 2014-11-13 23:22:37 -08:00
Cameron Gutman
6338e7b8eb Add deadzone preference 2014-11-13 23:22:13 -08:00
Cameron Gutman
1b9846d519 Close the app list instead of displaying an error if the app view is resumed and fails to update 2014-11-13 22:31:19 -08:00
Cameron Gutman
a4ece13a1d Fix refreshing apps text 2014-11-13 22:30:45 -08:00
Cameron Gutman
066b8430a0 Update common with fix for 404 error message 2014-11-13 21:56:28 -08:00
Cameron Gutman
2b54a91f3d Replace ... with elipsis character 2014-11-13 21:50:12 -08:00
Cameron Gutman
2d01633372 Fix small error in strings.xml 2014-11-13 21:49:59 -08:00
Cameron Gutman
5dc01069fc Update PC view to avoid scrunched up text when looking for a PC on phones in portrait orientation 2014-11-13 21:49:44 -08:00
Cameron Gutman
d450008833 Don't use deprecated constants 2014-11-13 21:49:12 -08:00
Cameron Gutman
a37fff6eb5 Fix a bunch of Lint errors 2014-11-13 21:37:11 -08:00
Cameron Gutman
6604675bf9 Lint: Remove unused imports 2014-11-13 21:30:32 -08:00
Cameron Gutman
1965cc2347 Merge branch 'NewUI-prepare-for-translation' into NewUI
Conflicts:
	app/src/main/java/com/limelight/PcView.java
2014-11-13 21:30:11 -08:00
Cameron Gutman
312ca27906 Open the app list after successfully pairing 2014-11-13 21:22:45 -08:00
Cameron Gutman
0bceadbd9a New common with disabled FEC 2014-11-13 21:16:32 -08:00
Cameron Gutman
dfc3daabcd Use a 2 frame audio buffer if possible to reduce audio latency 2014-11-13 21:16:14 -08:00
Ansa89
b9ba9adc1f Forgot about these 2014-11-12 12:52:18 +01:00
Ansa89
f112d45e1a Some cleanup 2014-11-12 09:58:26 +01:00
Ansa89
88f139873c Resolve merge conflicts 2014-11-12 09:41:12 +01:00
Ansa89
d317c5bf03 Try to make limelight more translatable 2014-11-11 16:30:20 +01:00
Cameron Gutman
9d72314b9c Update common again 2014-11-11 01:14:32 -08:00
Cameron Gutman
2cc7243573 Update beta version 2014-11-10 22:06:02 -08:00
Cameron Gutman
269d9a6bc6 Update to support GFE 2.1.4 2014-11-10 22:01:19 -08:00
Cameron Gutman
244130fc1b Add visual indication when no PCs have been found yet 2014-11-08 13:56:16 -08:00
Cameron Gutman
a67791b8aa Display the delete PC option for local PCs too, even though it may not always work 2014-11-08 13:20:14 -08:00
Cameron Gutman
21e46a5c3b Display machines as they are being refreshed 2014-11-08 13:14:35 -08:00
Cameron Gutman
2df2f850d5 Remove dead code 2014-11-08 13:13:39 -08:00
Cameron Gutman
406d26ec1c Add visual feeback for offline machines and running games 2014-11-08 12:51:07 -08:00
Cameron Gutman
68c1aaf433 Add new app view UI 2014-11-08 01:07:21 -08:00
Cameron Gutman
9ef577dbdd Update UI for add PC 2014-11-07 23:09:45 -08:00
Cameron Gutman
982ecbc015 Improve the look of the buttons and PC view UI 2014-11-07 22:28:07 -08:00
Cameron Gutman
7e44b5abd5 Remove margins from landscape pc view 2014-11-07 01:20:55 -08:00
Cameron Gutman
6dbb1a0c1f Fix UI performance issues 2014-11-07 01:18:14 -08:00
Cameron Gutman
94b1c04fa6 GridView WIP 2014-11-07 00:27:58 -08:00
Cameron Gutman
9758276f1c Use the normal margins for AddComputerManually 2014-11-06 22:30:10 -08:00
Cameron Gutman
971263c52d Update common 2014-11-06 22:14:12 -08:00
Cameron Gutman
9b58e7bb4d Fix right clicking inconsistency on different devices 2014-11-06 20:38:29 -08:00
Cameron Gutman
69ecf0251d Forgot one activity 2014-11-06 20:07:59 -08:00
Cameron Gutman
350a4d8825 Add a helper class to perform initial UI fixups (currently adding padding on TV devices) 2014-11-06 20:07:01 -08:00
Cameron Gutman
44f447df7b Remove some old layout cruft 2014-11-06 20:01:32 -08:00
Cameron Gutman
e8c4df4897 Add new Material-style launcher assets 2014-11-06 09:29:28 -08:00
Cameron Gutman
5ee16124bc Add new banners from phantom-playR 2014-11-05 18:50:40 -08:00
580 changed files with 16869 additions and 104432 deletions

8
.github/no-response.yml vendored Normal file
View File

@@ -0,0 +1,8 @@
# ProBot No Response (https://probot.github.io/apps/no-response/)
daysUntilClose: 7
responseRequiredLabel: 'need more info'
closeComment: >
This issue has been automatically closed because there was no response to a
request for more information from the issue opener. Please leave a comment or
open a new issue if you have additional information related to this issue.

14
.github/stale.yml vendored Normal file
View File

@@ -0,0 +1,14 @@
# ProBot Stale (https://probot.github.io/apps/stale/)
daysUntilStale: 90
daysUntilClose: 7
exemptLabels:
- accepted
- bug
- enhancement
- meta
staleLabel: stale
markComment: >
This issue has been automatically marked as stale because it has not had
recent activity. It will be closed if no further activity occurs.
closeComment: false

9
.gitignore vendored
View File

@@ -1,6 +1,9 @@
#built application files
# built application files
*.apk
*.ap_
*.aab
output.json
out/
# files for the dex VM
*.dex
@@ -30,6 +33,8 @@ Thumbs.db
#.idea/workspace.xml - remove # and delete .idea if it better suit your needs.
.gradle
build/
*.iml
# Compiled JNI libraries folder
**/jniLibs
**/jniLibs
app/.externalNativeBuild/

3
.gitmodules vendored Normal file
View File

@@ -0,0 +1,3 @@
[submodule "moonlight-common"]
path = moonlight-common
url = https://github.com/moonlight-stream/moonlight-common.git

View File

@@ -20,7 +20,7 @@ function p_h264raw.dissector(buf, pkt, root)
local i = 0
local data_start = -1
while i < buf:len do
while i < buf:len() do
-- Make sure we have a potential start sequence and type
if buf:len() - i < 5 then
-- We need more data

View File

@@ -1,42 +1,42 @@
#Limelight
# Moonlight Android
Limelight is an open source implementation of NVIDIA's GameStream, as used by the NVIDIA Shield.
[Moonlight](https://moonlight-stream.org) is an open source implementation of NVIDIA's GameStream, as used by the NVIDIA Shield.
We reverse engineered the Shield streaming software and created a version that can be run on any Android device.
Limelight will allow you to stream your full collection of games from your Windows PC to your Android device,
Moonlight will allow you to stream your full collection of games from your Windows PC to your Android device,
whether in your own home or over the internet.
[Limelight-pc](https://github.com/limelight-stream/limelight-pc) is also currently in development for Windows, OS X and Linux. Versions for [iOS](https://github.com/limelight-stream/limelight-ios) and [Windows and Windows Phone](https://github.com/limelight-stream/limelight-windows) are also in development.
Check our [wiki](https://github.com/moonlight-stream/moonlight-docs/wiki) for more detailed information or a troubleshooting guide.
##Features
## Features
* Streams any of your games from your PC to your Android device
* Full gamepad support for MOGA, Xbox 360, PS3, OUYA, and Shield
* Automatically finds GameStream-compatible PCs on your network
##Installation
## Installation
* Download and install Limelight for Android from
[Google Play](https://play.google.com/store/apps/details?id=com.limelight)
* Download and install Moonlight for Android from
[Google Play](https://play.google.com/store/apps/details?id=com.limelight), [Amazon App Store](http://www.amazon.com/gp/product/B00JK4MFN2), or directly from the [releases page](https://github.com/moonlight-stream/moonlight-android/releases)
* Download [GeForce Experience](http://www.geforce.com/geforce-experience) and install on your Windows PC
##Requirements
## Requirements
* [GameStream compatible](http://shield.nvidia.com/play-pc-games/) computer with GTX 600/700 series GPU
* [GameStream compatible](http://shield.nvidia.com/play-pc-games/) computer with an NVIDIA GeForce GTX 600 series or higher desktop or mobile GPU (GT-series and AMD GPUs not supported)
* Android device running 4.1 (Jelly Bean) or higher
* High-end wireless router (802.11n dual-band recommended)
##Usage
## Usage
* Turn on GameStream in the GFE settings
* If you are connecting from outside the same network, turn on internet
streaming
* When on the same network as your PC, open Limelight and tap on your PC in the list
* Accept the pairing confirmation on your PC
* When on the same network as your PC, open Moonlight and tap on your PC in the list
* Accept the pairing confirmation on your PC and add the PIN if needed
* Tap your PC again to view the list of apps to stream
* Play games!
##Contribute
## Contribute
This project is being actively developed at [XDA Developers](http://forum.xda-developers.com/showthread.php?t=2505510)
@@ -44,14 +44,18 @@ This project is being actively developed at [XDA Developers](http://forum.xda-de
2. Write code
3. Send Pull Requests
Check out our [website](http://limelight-stream.com) for project links and information.
## 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
## Authors
* [Cameron Gutman](https://github.com/cgutman)
* [Diego Waxemberg](https://github.com/dwaxemberg)
* [Aaron Neyer](https://github.com/Aaronneyer)
* [Andrew Hennessy](https://github.com/yetanothername)
Limelight is the work of students at [Case Western](http://case.edu) and was
Moonlight is the work of students at [Case Western](http://case.edu) and was
started as a project at [MHacks](http://mhacks.org).

View File

@@ -1,112 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<module external.linked.project.path="$MODULE_DIR$" external.root.project.path="$MODULE_DIR$/.." external.system.id="GRADLE" external.system.module.group="limelight-android" external.system.module.version="unspecified" type="JAVA_MODULE" version="4">
<component name="FacetManager">
<facet type="android-gradle" name="Android-Gradle">
<configuration>
<option name="GRADLE_PROJECT_PATH" value=":app" />
</configuration>
</facet>
<facet type="android" name="Android">
<configuration>
<option name="SELECTED_BUILD_VARIANT" value="nonRootDebug" />
<option name="ASSEMBLE_TASK_NAME" value="assembleNonRootDebug" />
<option name="COMPILE_JAVA_TASK_NAME" value="compileNonRootDebugSources" />
<option name="ASSEMBLE_TEST_TASK_NAME" value="assembleNonRootDebugTest" />
<option name="SOURCE_GEN_TASK_NAME" value="generateNonRootDebugSources" />
<option name="TEST_SOURCE_GEN_TASK_NAME" value="generateNonRootDebugTestSources" />
<option name="ALLOW_USER_CONFIGURATION" value="false" />
<option name="MANIFEST_FILE_RELATIVE_PATH" value="/src/main/AndroidManifest.xml" />
<option name="RES_FOLDER_RELATIVE_PATH" value="/src/main/res" />
<option name="RES_FOLDERS_RELATIVE_PATH" value="file://$MODULE_DIR$/src/main/res" />
<option name="ASSETS_FOLDER_RELATIVE_PATH" value="/src/main/assets" />
</configuration>
</facet>
</component>
<component name="NewModuleRootManager" inherit-compiler-output="false">
<output url="file://$MODULE_DIR$/build/intermediates/classes/nonRoot/debug" />
<exclude-output />
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/r/nonRoot/debug" isTestSource="false" generated="true" />
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/aidl/nonRoot/debug" isTestSource="false" generated="true" />
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/buildConfig/nonRoot/debug" isTestSource="false" generated="true" />
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/rs/nonRoot/debug" isTestSource="false" generated="true" />
<sourceFolder url="file://$MODULE_DIR$/build/generated/res/rs/nonRoot/debug" type="java-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/nonRootDebug/res" type="java-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/nonRootDebug/resources" type="java-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/nonRootDebug/assets" type="java-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/nonRootDebug/aidl" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/src/nonRootDebug/java" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/src/nonRootDebug/jni" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/src/nonRootDebug/rs" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/r/test/nonRoot/debug" isTestSource="true" generated="true" />
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/aidl/test/nonRoot/debug" isTestSource="true" generated="true" />
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/buildConfig/test/nonRoot/debug" isTestSource="true" generated="true" />
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/rs/test/nonRoot/debug" isTestSource="true" generated="true" />
<sourceFolder url="file://$MODULE_DIR$/build/generated/res/rs/test/nonRoot/debug" type="java-test-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/nonRoot/res" type="java-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/nonRoot/resources" type="java-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/nonRoot/assets" type="java-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/nonRoot/aidl" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/src/nonRoot/java" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/src/nonRoot/jni" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/src/nonRoot/rs" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/src/androidTestNonRoot/res" type="java-test-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/androidTestNonRoot/resources" type="java-test-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/androidTestNonRoot/assets" type="java-test-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/androidTestNonRoot/aidl" isTestSource="true" />
<sourceFolder url="file://$MODULE_DIR$/src/androidTestNonRoot/java" isTestSource="true" />
<sourceFolder url="file://$MODULE_DIR$/src/androidTestNonRoot/jni" isTestSource="true" />
<sourceFolder url="file://$MODULE_DIR$/src/androidTestNonRoot/rs" isTestSource="true" />
<sourceFolder url="file://$MODULE_DIR$/src/debug/res" type="java-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/debug/resources" type="java-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/debug/assets" type="java-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/debug/aidl" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/src/debug/java" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/src/debug/jni" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/src/debug/rs" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/src/main/res" type="java-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/main/resources" type="java-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/main/assets" type="java-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/main/aidl" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/src/main/java" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/src/main/rs" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/src/androidTest/res" type="java-test-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/androidTest/resources" type="java-test-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/androidTest/assets" type="java-test-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/androidTest/aidl" isTestSource="true" />
<sourceFolder url="file://$MODULE_DIR$/src/androidTest/java" isTestSource="true" />
<sourceFolder url="file://$MODULE_DIR$/src/androidTest/jni" isTestSource="true" />
<sourceFolder url="file://$MODULE_DIR$/src/androidTest/rs" isTestSource="true" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/assets" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/bundles" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/classes" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/coverage-instrumented-classes" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/dependency-cache" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/dex" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/dex-cache" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/incremental" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/jacoco" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/javaResources" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/libs" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/lint" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/manifests" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/ndk" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/pre-dexed" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/proguard" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/res" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/rs" />
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/symbols" />
<excludeFolder url="file://$MODULE_DIR$/build/outputs" />
<excludeFolder url="file://$MODULE_DIR$/build/tmp" />
</content>
<orderEntry type="jdk" jdkName="Android API 21 Platform" jdkType="Android SDK" />
<orderEntry type="sourceFolder" forTests="false" />
<orderEntry type="library" exported="" name="bcprov-jdk15on-1.51" level="project" />
<orderEntry type="library" exported="" name="jmdns-fixed" level="project" />
<orderEntry type="library" exported="" name="jcodec-0.1.6-3" level="project" />
<orderEntry type="library" exported="" name="bcpkix-jdk15on-1.51" level="project" />
<orderEntry type="library" exported="" name="tinyrtsp" level="project" />
<orderEntry type="library" exported="" name="limelight-common" level="project" />
</component>
</module>

View File

@@ -1,71 +1,122 @@
import com.android.builder.model.ProductFlavor
import org.apache.tools.ant.taskdefs.condition.Os
apply plugin: 'com.android.application'
android {
compileSdkVersion 21
buildToolsVersion "21.0.2"
buildToolsVersion '28.0.3'
compileSdkVersion 28
defaultConfig {
minSdkVersion 16
targetSdkVersion 21
targetSdkVersion 28
versionName "2.9"
versionCode = 38
versionName "6.0.1"
versionCode = 181
}
flavorDimensions "root"
productFlavors {
root {
// Android O has native mouse capture, so don't show the rooted
// version to devices running O on the Play Store.
maxSdkVersion 25
externalNativeBuild {
ndkBuild {
arguments "PRODUCT_FLAVOR=root"
}
}
applicationId "com.limelight.root"
dimension "root"
}
nonRoot {
externalNativeBuild {
ndkBuild {
arguments "PRODUCT_FLAVOR=nonRoot"
}
}
applicationId "com.limelight"
dimension "root"
}
}
lintOptions {
disable 'MissingTranslation'
}
bundle {
language {
// Avoid splitting by language, since we allow users
// to manually switch language in settings.
enableSplit = false
}
density {
// FIXME: This should not be neccessary but we get
// weird crashes due to missing drawable resources
// when this split is enabled.
enableSplit = false
}
}
buildTypes {
debug {
applicationIdSuffix ".debug"
minifyEnabled true
useProguard false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
release {
runProguard false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt'
// To whomever is releasing/using an APK in release mode with
// Moonlight's official application ID, please stop. I see every
// single one of your crashes in my Play Console and it makes
// Moonlight's reliability look worse and makes it more difficult
// to distinguish real crashes from your crashy VR app. Seriously,
// 44 of the *same* native crash in 72 hours and a few each of
// several other crashes.
//
// This is technically not your fault. I would have hoped Google
// would validate the signature of the APK before attributing
// the crash to it. I asked their Play Store support about this
// and they said they don't and don't have plans to, so that sucks.
//
// In any case, it's bad form to release an APK using someone
// else's application ID. There is no legitimate reason, that
// anyone would need to comment out the following line, except me
// when I release an official signed Moonlight build. If you feel
// like doing so would solve something, I can tell you it will not.
// You can't upgrade an app while retaining data without having the
// same signature as the official version. Nor can you post it on
// the Play Store, since that application ID is already taken.
// Reputable APK hosting websites similarly validate the signature
// is consistent with the Play Store and won't allow an APK that
// isn't signed the same as the original.
//
// I wish any and all people using Moonlight as the basis of other
// cool projects the best of luck with their efforts. All I ask
// is to please change the applicationId before you publish.
//
// TL;DR: Leave the following line alone!
applicationIdSuffix ".unofficial"
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
sourceSets.main.jni.srcDirs = []
//noinspection GroovyAssignabilityCheck,GroovyAssignabilityCheck
task ndkBuild(type: Exec, description: 'Compile JNI source via NDK') {
Properties properties = new Properties()
properties.load(project.rootProject.file('local.properties').newDataInputStream())
def ndkDir = properties.getProperty('ndk.dir')
if (Os.isFamily(Os.FAMILY_WINDOWS)) {
commandLine "$ndkDir\\ndk-build.cmd",
'NDK_PROJECT_PATH=build/intermediates/ndk',
'NDK_LIBS_OUT=src/main/jniLibs',
'APP_BUILD_SCRIPT=src/main/jni/Android.mk',
'NDK_APPLICATION_MK=src/main/jni/Application.mk'
externalNativeBuild {
ndkBuild {
path "src/main/jni/Android.mk"
}
else {
commandLine "$ndkDir/ndk-build",
'NDK_PROJECT_PATH=build/intermediates/ndk',
'NDK_LIBS_OUT=src/main/jniLibs',
'APP_BUILD_SCRIPT=src/main/jni/Android.mk',
'NDK_APPLICATION_MK=src/main/jni/Application.mk'
}
}
tasks.withType(JavaCompile) {
compileTask -> compileTask.dependsOn ndkBuild
}
}
dependencies {
compile group: 'org.jcodec', name: 'jcodec', version: '0.1.6-3'
compile group: 'org.bouncycastle', name: 'bcprov-jdk15on', version: '1.51'
compile group: 'org.bouncycastle', name: 'bcpkix-jdk15on', version: '1.51'
compile files('libs/jmdns-fixed.jar')
compile files('libs/limelight-common.jar')
compile files('libs/tinyrtsp.jar')
implementation 'org.bouncycastle:bcprov-jdk15on:1.59'
implementation 'org.bouncycastle:bcpkix-jdk15on:1.59'
implementation 'org.jcodec:jcodec:0.2.3'
implementation project(':moonlight-common')
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

27
app/proguard-rules.pro vendored Normal file
View File

@@ -0,0 +1,27 @@
# Don't obfuscate code
-dontobfuscate
# Our code
-keep class com.limelight.binding.input.evdev.* {*;}
# Moonlight common
-keep class com.limelight.nvstream.jni.* {*;}
# Okio
-keep class sun.misc.Unsafe {*;}
-dontwarn java.nio.file.*
-dontwarn org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement
-dontwarn okio.**
# BouncyCastle
-keep class org.bouncycastle.jcajce.provider.asymmetric.* {*;}
-keep class org.bouncycastle.jcajce.provider.asymmetric.util.* {*;}
-keep class org.bouncycastle.jcajce.provider.asymmetric.rsa.* {*;}
-keep class org.bouncycastle.jcajce.provider.digest.** {*;}
-keep class org.bouncycastle.jcajce.provider.symmetric.** {*;}
-keep class org.bouncycastle.jcajce.spec.* {*;}
-keep class org.bouncycastle.jce.** {*;}
-dontwarn javax.naming.**
# jMDNS
-dontwarn javax.jmdns.impl.DNSCache

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_label" translatable="false">Moonlight (Debug)</string>
<string name="app_label_root" translatable="false">Moonlight (Root Debug)</string>
</resources>

View File

@@ -7,74 +7,133 @@
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.CHANGE_WIFI_MULTICAST_STATE" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-feature android:name="android.hardware.touchscreen" android:required="false" />
<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.touchscreen"
android:required="false" />
<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" />
<!-- Disable legacy input emulation on ChromeOS -->
<uses-feature
android:name="android.hardware.type.pc"
android:required="false"/>
<application
android:allowBackup="true"
android:icon="@drawable/ic_launcher"
android:theme="@style/AppTheme" >
<!-- Launcher for traditional devices -->
android:fullBackupContent="@xml/backup_rules"
android:networkSecurityConfig="@xml/network_security_config"
android:isGame="true"
android:banner="@drawable/atv_banner"
android:appCategory="game"
android:icon="@mipmap/ic_launcher"
android:roundIcon="@mipmap/ic_launcher"
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" />
<!-- Samsung DeX support requires explicit placement of android:resizeableActivity="true"
in each activity even though it is implied by targeting API 24+ -->
<activity
android:name=".PcView"
android:configChanges="mcc|mnc|locale|touchscreen|keyboard|keyboardHidden|navigation|screenLayout|fontScale|uiMode|orientation|screenSize|smallestScreenSize|layoutDirection">
android:resizeableActivity="true"
android:configChanges="mcc|mnc|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.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 -->
<!-- Small hack to support launcher shortcuts without relaunching over and over again when the back button is pressed -->
<activity
android:name=".PcViewTv"
android:logo="@drawable/atv_banner"
android:icon="@drawable/atv_banner"
android:name=".ShortcutTrampoline"
android:noHistory="true"
android:exported="true"
android:resizeableActivity="true"
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>
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value="com.limelight.PcView" />
</activity>
<activity
android:name=".AppView"
android:configChanges="mcc|mnc|locale|touchscreen|keyboard|keyboardHidden|navigation|screenLayout|fontScale|uiMode|orientation|screenSize|smallestScreenSize|layoutDirection" >
android:resizeableActivity="true"
android:configChanges="mcc|mnc|touchscreen|keyboard|keyboardHidden|navigation|screenLayout|fontScale|uiMode|orientation|screenSize|smallestScreenSize|layoutDirection">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value="com.limelight.PcView" />
</activity>
<activity
android:name=".preferences.StreamSettings"
android:label="Streaming Settings" >
android:resizeableActivity="true"
android:label="Streaming Settings">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value="com.limelight.PcView" />
</activity>
<activity
android:name=".preferences.AddComputerManually"
android:label="Add Computer Manually" >
android:resizeableActivity="true"
android:label="Add Computer Manually">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value="com.limelight.PcView" />
</activity>
<!-- This will fall back to sensorLandscape at runtime on Android 4.2 and below -->
<activity
android:name=".Game"
android:screenOrientation="sensorLandscape"
android:configChanges="mcc|mnc|locale|touchscreen|keyboard|keyboardHidden|navigation|screenLayout|fontScale|uiMode|orientation|screenSize|smallestScreenSize|layoutDirection" >
android:configChanges="mcc|mnc|locale|touchscreen|keyboard|keyboardHidden|navigation|screenLayout|fontScale|uiMode|orientation|screenSize|smallestScreenSize|layoutDirection"
android:screenOrientation="userLandscape"
android:noHistory="true"
android:supportsPictureInPicture="true"
android:resizeableActivity="true"
android:launchMode="singleTask"
android:excludeFromRecents="true"
android:theme="@style/StreamTheme">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value="com.limelight.Connection" />
android:value="com.limelight.AppView" />
</activity>
<service
android:name=".discovery.DiscoveryService"
android:label="mDNS PC Auto-Discovery Service" />
<service
android:name=".computers.ComputerManagerService"
android:label="Computer Management Service" />
<service
android:name=".binding.input.driver.UsbDriverService"
android:label="Usb Driver Service" />
<activity
android:name=".HelpActivity"
android:resizeableActivity="true"
android:configChanges="mcc|mnc|touchscreen|keyboard|keyboardHidden|navigation|screenLayout|fontScale|uiMode|orientation|screenSize|smallestScreenSize|layoutDirection">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value="com.limelight.PcView" />
</activity>
</application>
</manifest>
</manifest>

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

View File

@@ -1,295 +1,579 @@
package com.limelight;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.io.StringReader;
import java.util.List;
import java.util.UUID;
import org.xmlpull.v1.XmlPullParserException;
import com.limelight.binding.PlatformBinding;
import com.limelight.nvstream.http.GfeHttpResponseException;
import com.limelight.computers.ComputerManagerListener;
import com.limelight.computers.ComputerManagerService;
import com.limelight.grid.AppGridAdapter;
import com.limelight.nvstream.http.ComputerDetails;
import com.limelight.nvstream.http.NvApp;
import com.limelight.nvstream.http.NvHTTP;
import com.limelight.R;
import com.limelight.nvstream.http.PairingManager;
import com.limelight.preferences.PreferenceConfiguration;
import com.limelight.ui.AdapterFragment;
import com.limelight.ui.AdapterFragmentCallbacks;
import com.limelight.utils.CacheHelper;
import com.limelight.utils.Dialog;
import com.limelight.utils.ServerHelper;
import com.limelight.utils.ShortcutHelper;
import com.limelight.utils.SpinnerDialog;
import com.limelight.utils.UiHelper;
import android.app.Activity;
import android.app.Service;
import android.content.ComponentName;
import android.content.Intent;
import android.content.ServiceConnection;
import android.graphics.Bitmap;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.os.Bundle;
import android.os.IBinder;
import android.view.ContextMenu;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.ContextMenu.ContextMenuInfo;
import android.widget.AbsListView;
import android.widget.AdapterView;
import android.widget.AdapterView.OnItemClickListener;
import android.widget.ArrayAdapter;
import android.widget.ListView;
import android.widget.ImageView;
import android.widget.TextView;
import android.widget.Toast;
import android.widget.AdapterView.AdapterContextMenuInfo;
public class AppView extends Activity {
private ListView appList;
private ArrayAdapter<AppObject> appListAdapter;
private InetAddress ipAddress;
private String uniqueId;
private boolean remote;
private final static int RESUME_ID = 1;
private final static int QUIT_ID = 2;
private final static int CANCEL_ID = 3;
public final static String ADDRESS_EXTRA = "Address";
public final static String UNIQUEID_EXTRA = "UniqueId";
public final static String NAME_EXTRA = "Name";
public final static String REMOTE_EXTRA = "Remote";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_app_view);
byte[] address = getIntent().getByteArrayExtra(ADDRESS_EXTRA);
uniqueId = getIntent().getStringExtra(UNIQUEID_EXTRA);
remote = getIntent().getBooleanExtra(REMOTE_EXTRA, false);
if (address == null || uniqueId == null) {
return;
}
String labelText = "App List for "+getIntent().getStringExtra(NAME_EXTRA);
TextView label = (TextView) findViewById(R.id.appListText);
setTitle(labelText);
label.setText(labelText);
try {
ipAddress = InetAddress.getByAddress(address);
} catch (UnknownHostException e) {
return;
}
// Setup the list view
appList = (ListView)findViewById(R.id.pcListView);
appListAdapter = new ArrayAdapter<AppObject>(this, R.layout.simplerow, R.id.rowTextView);
appListAdapter.setNotifyOnChange(false);
appList.setAdapter(appListAdapter);
appList.setItemsCanFocus(true);
appList.setOnItemClickListener(new OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> arg0, View arg1, int pos,
long id) {
AppObject app = appListAdapter.getItem(pos);
if (app == null || app.app == null) {
return;
}
// Only open the context menu if something is running, otherwise start it
if (getRunningAppId() != -1) {
openContextMenu(arg1);
}
else {
doStart(app.app);
}
}
});
registerForContextMenu(appList);
}
@Override
protected void onDestroy() {
super.onDestroy();
SpinnerDialog.closeDialogs(this);
Dialog.closeDialogs();
}
@Override
protected void onResume() {
super.onResume();
updateAppList();
}
private int getRunningAppId() {
int runningAppId = -1;
for (int i = 0; i < appListAdapter.getCount(); i++) {
AppObject app = appListAdapter.getItem(i);
if (app.app == null) {
continue;
}
if (app.app.getIsRunning()) {
runningAppId = app.app.getAppId();
break;
}
public class AppView extends Activity implements AdapterFragmentCallbacks {
private AppGridAdapter appGridAdapter;
private String uuidString;
private ShortcutHelper shortcutHelper;
private ComputerDetails computer;
private ComputerManagerService.ApplistPoller poller;
private SpinnerDialog blockingLoadSpinner;
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;
private final static int CANCEL_ID = 3;
private final static int START_WITH_QUIT = 4;
private final static int VIEW_DETAILS_ID = 5;
private final static int CREATE_SHORTCUT_ID = 6;
public final static String NAME_EXTRA = "Name";
public final static String UUID_EXTRA = "UUID";
private ComputerManagerService.ComputerManagerBinder managerBinder;
private final ServiceConnection serviceConnection = new ServiceConnection() {
public void onServiceConnected(ComponentName className, IBinder binder) {
final ComputerManagerService.ComputerManagerBinder localBinder =
((ComputerManagerService.ComputerManagerBinder)binder);
// Wait in a separate thread to avoid stalling the UI
new Thread() {
@Override
public void run() {
// Wait for the binder to be ready
localBinder.waitForReady();
// Get the computer object
computer = localBinder.getComputer(UUID.fromString(uuidString));
if (computer == null) {
finish();
return;
}
try {
appGridAdapter = new AppGridAdapter(AppView.this,
PreferenceConfiguration.readPreferences(AppView.this).listMode,
PreferenceConfiguration.readPreferences(AppView.this).smallIconMode,
computer, localBinder.getUniqueId());
} catch (Exception e) {
e.printStackTrace();
finish();
return;
}
// Now make the binder visible. We must do this after appGridAdapter
// is set to prevent us from reaching updateUiWithServerinfo() and
// touching the appGridAdapter prior to initialization.
managerBinder = localBinder;
// Load the app grid with cached data (if possible).
// This must be done _before_ startComputerUpdates()
// so the initial serverinfo response can update the running
// icon.
populateAppGridWithCache();
// Start updates
startComputerUpdates();
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();
}
return runningAppId;
}
@Override
public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) {
super.onCreateContextMenu(menu, v, menuInfo);
AdapterContextMenuInfo info = (AdapterContextMenuInfo) menuInfo;
AppObject selectedApp = appListAdapter.getItem(info.position);
if (selectedApp == null || selectedApp.app == null) {
return;
public void onServiceDisconnected(ComponentName className) {
managerBinder = null;
}
int runningAppId = getRunningAppId();
if (runningAppId != -1) {
if (runningAppId == selectedApp.app.getAppId()) {
menu.add(Menu.NONE, RESUME_ID, 1, "Resume Session");
menu.add(Menu.NONE, QUIT_ID, 2, "Quit Session");
}
else {
menu.add(Menu.NONE, RESUME_ID, 1, "Quit Current Game and Start");
menu.add(Menu.NONE, CANCEL_ID, 2, "Cancel");
}
};
private void startComputerUpdates() {
// Don't start polling if we're not bound or in the foreground
if (managerBinder == null || !inForeground) {
return;
}
managerBinder.startPolling(new ComputerManagerListener() {
@Override
public void notifyComputerUpdated(final ComputerDetails details) {
// Do nothing if updates are suspended
if (suspendGridUpdates) {
return;
}
// Don't care about other computers
if (!details.uuid.toString().equalsIgnoreCase(uuidString)) {
return;
}
if (details.state == ComputerDetails.State.OFFLINE) {
// The PC is unreachable now
AppView.this.runOnUiThread(new Runnable() {
@Override
public void run() {
// Display a toast to the user and quit the activity
Toast.makeText(AppView.this, getResources().getText(R.string.lost_connection), Toast.LENGTH_SHORT).show();
finish();
}
});
return;
}
// Close immediately if the PC is no longer paired
if (details.state == ComputerDetails.State.ONLINE && details.pairState != PairingManager.PairState.PAIRED) {
AppView.this.runOnUiThread(new Runnable() {
@Override
public void run() {
// Disable shortcuts referencing this PC for now
shortcutHelper.disableShortcut(details.uuid.toString(),
getResources().getString(R.string.scut_not_paired));
// Display a toast to the user and quit the activity
Toast.makeText(AppView.this, getResources().getText(R.string.scut_not_paired), Toast.LENGTH_SHORT).show();
finish();
}
});
return;
}
// App list is the same or empty
if (details.rawAppList == null || details.rawAppList.equals(lastRawApplist)) {
// Let's check if the running app ID changed
if (details.runningGameId != lastRunningAppId) {
// Update the currently running game using the app ID
lastRunningAppId = details.runningGameId;
updateUiWithServerinfo(details);
}
return;
}
lastRunningAppId = details.runningGameId;
lastRawApplist = details.rawAppList;
try {
updateUiWithAppList(NvHTTP.getAppListByReader(new StringReader(details.rawAppList)));
updateUiWithServerinfo(details);
if (blockingLoadSpinner != null) {
blockingLoadSpinner.dismiss();
blockingLoadSpinner = null;
}
} catch (Exception ignored) {}
}
});
if (poller == null) {
poller = managerBinder.createAppListPoller(computer);
}
poller.start();
}
private void stopComputerUpdates() {
if (poller != null) {
poller.stop();
}
if (managerBinder != null) {
managerBinder.stopPolling();
}
if (appGridAdapter != null) {
appGridAdapter.cancelQueuedOperations();
}
}
@Override
public void onContextMenuClosed(Menu menu) {
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// Assume we're in the foreground when created to avoid a race
// between binding to CMS and onResume()
inForeground = true;
shortcutHelper = new ShortcutHelper(this);
UiHelper.setLocale(this);
setContentView(R.layout.activity_app_view);
UiHelper.notifyNewRootView(this);
uuidString = getIntent().getStringExtra(UUID_EXTRA);
String computerName = getIntent().getStringExtra(NAME_EXTRA);
TextView label = findViewById(R.id.appListText);
setTitle(computerName);
label.setText(computerName);
// Add a launcher shortcut for this PC (forced, since this is user interaction)
shortcutHelper.createAppViewShortcut(uuidString, computerName, uuidString, true);
shortcutHelper.reportShortcutUsed(uuidString);
// Bind to the computer manager service
bindService(new Intent(this, ComputerManagerService.class), serviceConnection,
Service.BIND_AUTO_CREATE);
}
private void populateAppGridWithCache() {
try {
// Try to load from cache
lastRawApplist = CacheHelper.readInputStreamToString(CacheHelper.openCacheFileForInput(getCacheDir(), "applist", uuidString));
List<NvApp> applist = NvHTTP.getAppListByReader(new StringReader(lastRawApplist));
updateUiWithAppList(applist);
LimeLog.info("Loaded applist from cache");
} catch (Exception e) {
if (lastRawApplist != null) {
LimeLog.warning("Saved applist corrupted: "+lastRawApplist);
e.printStackTrace();
}
LimeLog.info("Loading applist from the network");
// We'll need to load from the network
loadAppsBlocking();
}
}
private void loadAppsBlocking() {
blockingLoadSpinner = SpinnerDialog.displayDialog(this, getResources().getString(R.string.applist_refresh_title),
getResources().getString(R.string.applist_refresh_msg), true);
}
@Override
protected void onDestroy() {
super.onDestroy();
SpinnerDialog.closeDialogs(this);
Dialog.closeDialogs();
if (managerBinder != null) {
unbindService(serviceConnection);
}
}
@Override
protected void onResume() {
super.onResume();
// Display a decoder crash notification if we've returned after a crash
UiHelper.showDecoderCrashDialog(this);
inForeground = true;
startComputerUpdates();
}
@Override
protected void onPause() {
super.onPause();
inForeground = false;
stopComputerUpdates();
}
@Override
public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) {
super.onCreateContextMenu(menu, v, menuInfo);
AdapterContextMenuInfo info = (AdapterContextMenuInfo) menuInfo;
AppObject selectedApp = (AppObject) appGridAdapter.getItem(info.position);
if (lastRunningAppId != 0) {
if (lastRunningAppId == selectedApp.app.getAppId()) {
menu.add(Menu.NONE, START_OR_RESUME_ID, 1, getResources().getString(R.string.applist_menu_resume));
menu.add(Menu.NONE, QUIT_ID, 2, getResources().getString(R.string.applist_menu_quit));
}
else {
menu.add(Menu.NONE, START_WITH_QUIT, 1, getResources().getString(R.string.applist_menu_quit_and_start));
menu.add(Menu.NONE, CANCEL_ID, 2, getResources().getString(R.string.applist_menu_cancel));
}
}
menu.add(Menu.NONE, VIEW_DETAILS_ID, 3, getResources().getString(R.string.applist_menu_details));
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
// Only add an option to create shortcut if box art is loaded
// and when we're in grid-mode (not list-mode).
ImageView appImageView = info.targetView.findViewById(R.id.grid_image);
if (appImageView != null) {
// We have a grid ImageView, so we must be in grid-mode
BitmapDrawable drawable = (BitmapDrawable)appImageView.getDrawable();
if (drawable != null && drawable.getBitmap() != null) {
// We have a bitmap loaded too
menu.add(Menu.NONE, CREATE_SHORTCUT_ID, 4, getResources().getString(R.string.applist_menu_scut));
}
}
}
}
@Override
public void onContextMenuClosed(Menu menu) {
}
@Override
public boolean onContextItemSelected(MenuItem item) {
AdapterContextMenuInfo info = (AdapterContextMenuInfo) item.getMenuInfo();
AppObject app = appListAdapter.getItem(info.position);
switch (item.getItemId())
{
case RESUME_ID:
// Resume is the same as start for us
doStart(app.app);
return true;
case QUIT_ID:
doQuit(app.app);
return true;
case CANCEL_ID:
return true;
default:
return super.onContextItemSelected(item);
final AppObject app = (AppObject) appGridAdapter.getItem(info.position);
switch (item.getItemId()) {
case START_WITH_QUIT:
// Display a confirmation dialog first
UiHelper.displayQuitConfirmationDialog(this, new Runnable() {
@Override
public void run() {
ServerHelper.doStart(AppView.this, app.app, computer, managerBinder);
}
}, null);
return true;
case START_OR_RESUME_ID:
// Resume is the same as start for us
ServerHelper.doStart(AppView.this, app.app, computer, managerBinder);
return true;
case QUIT_ID:
// Display a confirmation dialog first
UiHelper.displayQuitConfirmationDialog(this, new Runnable() {
@Override
public void run() {
suspendGridUpdates = true;
ServerHelper.doQuit(AppView.this,
ServerHelper.getCurrentAddressFromComputer(computer),
app.app, managerBinder, new Runnable() {
@Override
public void run() {
// Trigger a poll immediately
suspendGridUpdates = false;
if (poller != null) {
poller.pollNow();
}
}
});
}
}, null);
return true;
case CANCEL_ID:
return true;
case VIEW_DETAILS_ID:
Dialog.displayDialog(AppView.this, getResources().getString(R.string.title_details),
getResources().getString(R.string.applist_details_id) + " " + app.app.getAppId(), false);
return true;
case CREATE_SHORTCUT_ID:
ImageView appImageView = info.targetView.findViewById(R.id.grid_image);
Bitmap appBits = ((BitmapDrawable)appImageView.getDrawable()).getBitmap();
if (!shortcutHelper.createPinnedGameShortcut(uuidString + Integer.valueOf(app.app.getAppId()).toString(), appBits, computer, app.app)) {
Toast.makeText(AppView.this, getResources().getString(R.string.unable_to_pin_shortcut), Toast.LENGTH_LONG).show();
}
return true;
default:
return super.onContextItemSelected(item);
}
}
private static String generateString(NvApp app) {
StringBuilder str = new StringBuilder();
str.append(app.getAppName());
if (app.getIsRunning()) {
str.append(" - Running");
}
return str.toString();
private void updateUiWithServerinfo(final ComputerDetails details) {
AppView.this.runOnUiThread(new Runnable() {
@Override
public void run() {
boolean updated = false;
// Look through our current app list to tag the running app
for (int i = 0; i < appGridAdapter.getCount(); i++) {
AppObject existingApp = (AppObject) appGridAdapter.getItem(i);
// There can only be one or zero apps running.
if (existingApp.isRunning &&
existingApp.app.getAppId() == details.runningGameId) {
// This app was running and still is, so we're done now
return;
}
else if (existingApp.app.getAppId() == details.runningGameId) {
// This app wasn't running but now is
existingApp.isRunning = true;
updated = true;
}
else if (existingApp.isRunning) {
// This app was running but now isn't
existingApp.isRunning = false;
updated = true;
}
else {
// This app wasn't running and still isn't
}
}
if (updated) {
appGridAdapter.notifyDataSetChanged();
}
}
});
}
private void addListPlaceholder() {
appListAdapter.add(new AppObject("No apps found. Try rescanning for games in GeForce Experience.", null));
private void updateUiWithAppList(final List<NvApp> appList) {
AppView.this.runOnUiThread(new Runnable() {
@Override
public void run() {
boolean updated = false;
// First handle app updates and additions
for (NvApp app : appList) {
boolean foundExistingApp = false;
// Try to update an existing app in the list first
for (int i = 0; i < appGridAdapter.getCount(); i++) {
AppObject existingApp = (AppObject) appGridAdapter.getItem(i);
if (existingApp.app.getAppId() == app.getAppId()) {
// Found the app; update its properties
if (!existingApp.app.getAppName().equals(app.getAppName())) {
existingApp.app.setAppName(app.getAppName());
updated = true;
}
foundExistingApp = true;
break;
}
}
if (!foundExistingApp) {
// This app must be new
appGridAdapter.addApp(new AppObject(app));
updated = true;
}
}
// Next handle app removals
int i = 0;
while (i < appGridAdapter.getCount()) {
boolean foundExistingApp = false;
AppObject existingApp = (AppObject) appGridAdapter.getItem(i);
// Check if this app is in the latest list
for (NvApp app : appList) {
if (existingApp.app.getAppId() == app.getAppId()) {
foundExistingApp = true;
break;
}
}
// This app was removed in the latest app list
if (!foundExistingApp) {
appGridAdapter.removeApp(existingApp);
updated = true;
// Check this same index again because the item at i+1 is now at i after
// the removal
continue;
}
// Move on to the next item
i++;
}
if (updated) {
appGridAdapter.notifyDataSetChanged();
}
}
});
}
private void updateAppList() {
final SpinnerDialog spinner = SpinnerDialog.displayDialog(this, "App List", "Refreshing app list...", true);
new Thread() {
@Override
public void run() {
NvHTTP httpConn = new NvHTTP(ipAddress, uniqueId, null, PlatformBinding.getCryptoProvider(AppView.this));
try {
final List<NvApp> appList = httpConn.getAppList();
AppView.this.runOnUiThread(new Runnable() {
@Override
public void run() {
appListAdapter.clear();
if (appList.isEmpty()) {
addListPlaceholder();
}
else {
for (NvApp app : appList) {
appListAdapter.add(new AppObject(generateString(app), app));
}
}
appListAdapter.notifyDataSetChanged();
}
});
// Success case
return;
} catch (GfeHttpResponseException ignored) {
} catch (IOException ignored) {
} catch (XmlPullParserException ignored) {
} finally {
spinner.dismiss();
}
Dialog.displayDialog(AppView.this, "Error", "Failed to get app list", true);
}
}.start();
@Override
public int getAdapterFragmentLayoutId() {
return PreferenceConfiguration.readPreferences(this).listMode ?
R.layout.list_view : (PreferenceConfiguration.readPreferences(AppView.this).smallIconMode ?
R.layout.app_grid_view_small : R.layout.app_grid_view);
}
private void doStart(NvApp app) {
Intent intent = new Intent(this, Game.class);
intent.putExtra(Game.EXTRA_HOST, ipAddress.getHostAddress());
intent.putExtra(Game.EXTRA_APP, app.getAppName());
intent.putExtra(Game.EXTRA_UNIQUEID, uniqueId);
intent.putExtra(Game.EXTRA_STREAMING_REMOTE, remote);
startActivity(intent);
}
private void doQuit(final NvApp app) {
Toast.makeText(AppView.this, "Quitting "+app.getAppName()+"...", Toast.LENGTH_SHORT).show();
new Thread(new Runnable() {
@Override
public void run() {
NvHTTP httpConn;
String message;
try {
httpConn = new NvHTTP(ipAddress, uniqueId, null, PlatformBinding.getCryptoProvider(AppView.this));
if (httpConn.quitApp()) {
message = "Successfully quit "+app.getAppName();
}
else {
message = "Failed to quit "+app.getAppName();
}
updateAppList();
} catch (UnknownHostException e) {
message = "Failed to resolve host";
} catch (FileNotFoundException e) {
message = "GFE returned an HTTP 404 error. Make sure your PC is running a supported GPU. Using remote desktop software can also cause this error. "
+ "Try rebooting your machine or reinstalling GFE.";
} catch (Exception e) {
message = e.getMessage();
}
final String toastMessage = message;
runOnUiThread(new Runnable() {
@Override
public void run() {
Toast.makeText(AppView.this, toastMessage, Toast.LENGTH_LONG).show();
}
});
}
}).start();
}
public class AppObject {
public String text;
public NvApp app;
public AppObject(String text, NvApp app) {
this.text = text;
this.app = app;
}
@Override
public String toString() {
return text;
}
}
}
@Override
public void receiveAbsListView(AbsListView listView) {
listView.setAdapter(appGridAdapter);
listView.setOnItemClickListener(new OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> arg0, View arg1, int pos,
long id) {
AppObject app = (AppObject) appGridAdapter.getItem(pos);
// Only open the context menu if something is running, otherwise start it
if (lastRunningAppId != 0) {
openContextMenu(arg1);
} else {
ServerHelper.doStart(AppView.this, app.app, computer, managerBinder);
}
}
});
registerForContextMenu(listView);
listView.requestFocus();
}
public class AppObject {
public final NvApp app;
public boolean isRunning;
public AppObject(NvApp app) {
if (app == null) {
throw new IllegalArgumentException("app must not be null");
}
this.app = app;
}
@Override
public String toString() {
return app.getAppName();
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,73 @@
package com.limelight;
import android.app.Activity;
import android.graphics.Bitmap;
import android.os.Bundle;
import android.webkit.WebView;
import android.webkit.WebViewClient;
import com.limelight.utils.SpinnerDialog;
public class HelpActivity extends Activity {
private SpinnerDialog loadingDialog;
private WebView webView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
webView = new WebView(this);
setContentView(webView);
// These allow the user to zoom the page
webView.getSettings().setBuiltInZoomControls(true);
webView.getSettings().setDisplayZoomControls(false);
// This sets the view to display the whole page by default
webView.getSettings().setUseWideViewPort(true);
webView.getSettings().setLoadWithOverviewMode(true);
// This allows the links to places on the same page to work
webView.getSettings().setJavaScriptEnabled(true);
webView.setWebViewClient(new WebViewClient() {
@Override
public void onPageStarted(WebView view, String url, Bitmap favicon) {
if (loadingDialog == null) {
loadingDialog = SpinnerDialog.displayDialog(HelpActivity.this,
getResources().getString(R.string.help_loading_title),
getResources().getString(R.string.help_loading_msg), false);
}
}
@Override
public void onPageFinished(WebView view, String url) {
if (loadingDialog != null) {
loadingDialog.dismiss();
loadingDialog = null;
}
}
@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
return !(url.toUpperCase().startsWith("https://github.com/moonlight-stream/moonlight-docs/wiki/".toUpperCase()) ||
url.toUpperCase().startsWith("http://github.com/moonlight-stream/moonlight-docs/wiki/".toUpperCase()));
}
});
webView.loadUrl(getIntent().getData().toString());
}
@Override
public void onBackPressed() {
// Back goes back through the WebView history
// until no more history remains
if (webView.canGoBack()) {
webView.goBack();
}
else {
super.onBackPressed();
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -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 {}

View File

@@ -0,0 +1,267 @@
package com.limelight;
import android.app.Activity;
import android.app.Service;
import android.content.ComponentName;
import android.content.Intent;
import android.content.ServiceConnection;
import android.os.Bundle;
import android.os.IBinder;
import com.limelight.computers.ComputerManagerListener;
import com.limelight.computers.ComputerManagerService;
import com.limelight.nvstream.http.ComputerDetails;
import com.limelight.nvstream.http.NvApp;
import com.limelight.nvstream.http.PairingManager;
import com.limelight.utils.Dialog;
import com.limelight.utils.ServerHelper;
import com.limelight.utils.SpinnerDialog;
import com.limelight.utils.UiHelper;
import java.util.ArrayList;
import java.util.UUID;
public class ShortcutTrampoline extends Activity {
private String uuidString;
private String appIdString;
private ArrayList<Intent> intentStack = new ArrayList<>();
private ComputerDetails computer;
private SpinnerDialog blockingLoadSpinner;
public final static String APP_ID_EXTRA = "AppId";
private ComputerManagerService.ComputerManagerBinder managerBinder;
private final ServiceConnection serviceConnection = new ServiceConnection() {
public void onServiceConnected(ComponentName className, IBinder binder) {
final ComputerManagerService.ComputerManagerBinder localBinder =
((ComputerManagerService.ComputerManagerBinder)binder);
// Wait in a separate thread to avoid stalling the UI
new Thread() {
@Override
public void run() {
// Wait for the binder to be ready
localBinder.waitForReady();
// Now make the binder visible
managerBinder = localBinder;
// Get the computer object
computer = managerBinder.getComputer(UUID.fromString(uuidString));
if (computer == null) {
Dialog.displayDialog(ShortcutTrampoline.this,
getResources().getString(R.string.conn_error_title),
getResources().getString(R.string.scut_pc_not_found),
true);
if (blockingLoadSpinner != null) {
blockingLoadSpinner.dismiss();
blockingLoadSpinner = null;
}
if (managerBinder != null) {
unbindService(serviceConnection);
managerBinder = null;
}
return;
}
// Force CMS to repoll this machine
managerBinder.invalidateStateForComputer(computer.uuid);
// Start polling
managerBinder.startPolling(new ComputerManagerListener() {
@Override
public void notifyComputerUpdated(final ComputerDetails details) {
// Don't care about other computers
if (!details.uuid.toString().equalsIgnoreCase(uuidString)) {
return;
}
if (details.state != ComputerDetails.State.UNKNOWN) {
runOnUiThread(new Runnable() {
@Override
public void run() {
// Stop showing the spinner
if (blockingLoadSpinner != null) {
blockingLoadSpinner.dismiss();
blockingLoadSpinner = null;
}
// If the managerBinder was destroyed before this callback,
// just finish the activity.
if (managerBinder == null) {
finish();
return;
}
if (details.state == ComputerDetails.State.ONLINE && details.pairState == PairingManager.PairState.PAIRED) {
// Launch game if provided app ID, otherwise launch app view
if (appIdString != null && appIdString.length() > 0) {
if (details.runningGameId == 0 || details.runningGameId == Integer.parseInt(appIdString)) {
intentStack.add(ServerHelper.createStartIntent(ShortcutTrampoline.this,
new NvApp("app", Integer.parseInt(appIdString), false), details, managerBinder));
// Close this activity
finish();
// Now start the activities
startActivities(intentStack.toArray(new Intent[]{}));
} else {
// Create the start intent immediately, so we can safely unbind the managerBinder
// below before we return.
final Intent startIntent = ServerHelper.createStartIntent(ShortcutTrampoline.this,
new NvApp("app", Integer.parseInt(appIdString), false), details, managerBinder);
UiHelper.displayQuitConfirmationDialog(ShortcutTrampoline.this, new Runnable() {
@Override
public void run() {
intentStack.add(startIntent);
// Close this activity
finish();
// Now start the activities
startActivities(intentStack.toArray(new Intent[]{}));
}
}, new Runnable() {
@Override
public void run() {
// Close this activity
finish();
}
});
}
} else {
// Close this activity
finish();
// Add the PC view at the back (and clear the task)
Intent i;
i = new Intent(ShortcutTrampoline.this, PcView.class);
i.setAction(Intent.ACTION_MAIN);
i.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK | Intent.FLAG_ACTIVITY_NEW_TASK);
intentStack.add(i);
// Take this intent's data and create an intent to start the app view
i = new Intent(getIntent());
i.setClass(ShortcutTrampoline.this, AppView.class);
intentStack.add(i);
// If a game is running, we'll make the stream the top level activity
if (details.runningGameId != 0) {
intentStack.add(ServerHelper.createStartIntent(ShortcutTrampoline.this,
new NvApp("app", details.runningGameId, false), details, managerBinder));
}
// Now start the activities
startActivities(intentStack.toArray(new Intent[]{}));
}
}
else if (details.state == ComputerDetails.State.OFFLINE) {
// Computer offline - display an error dialog
Dialog.displayDialog(ShortcutTrampoline.this,
getResources().getString(R.string.conn_error_title),
getResources().getString(R.string.error_pc_offline),
true);
} else if (details.pairState != PairingManager.PairState.PAIRED) {
// Computer not paired - display an error dialog
Dialog.displayDialog(ShortcutTrampoline.this,
getResources().getString(R.string.conn_error_title),
getResources().getString(R.string.scut_not_paired),
true);
}
// We don't want any more callbacks from now on, so go ahead
// and unbind from the service
if (managerBinder != null) {
managerBinder.stopPolling();
unbindService(serviceConnection);
managerBinder = null;
}
}
});
}
}
});
}
}.start();
}
public void onServiceDisconnected(ComponentName className) {
managerBinder = null;
}
};
protected boolean validateInput() {
// Validate UUID
try {
UUID.fromString(uuidString);
} catch (IllegalArgumentException ex) {
Dialog.displayDialog(ShortcutTrampoline.this,
getResources().getString(R.string.conn_error_title),
getResources().getString(R.string.scut_invalid_uuid),
true);
return false;
}
// Validate App ID (if provided)
if (appIdString != null && !appIdString.isEmpty()) {
try {
Integer.parseInt(appIdString);
} catch (NumberFormatException ex) {
Dialog.displayDialog(ShortcutTrampoline.this,
getResources().getString(R.string.conn_error_title),
getResources().getString(R.string.scut_invalid_app_id),
true);
return false;
}
}
return true;
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
UiHelper.notifyNewRootView(this);
uuidString = getIntent().getStringExtra(AppView.UUID_EXTRA);
appIdString = getIntent().getStringExtra(APP_ID_EXTRA);
if (validateInput()) {
// Bind to the computer manager service
bindService(new Intent(this, ComputerManagerService.class), serviceConnection,
Service.BIND_AUTO_CREATE);
blockingLoadSpinner = SpinnerDialog.displayDialog(this, getResources().getString(R.string.conn_establishing_title),
getResources().getString(R.string.applist_connect_msg), true);
}
}
@Override
protected void onStop() {
super.onStop();
if (blockingLoadSpinner != null) {
blockingLoadSpinner.dismiss();
blockingLoadSpinner = null;
}
Dialog.closeDialogs();
if (managerBinder != null) {
managerBinder.stopPolling();
unbindService(serviceConnection);
managerBinder = null;
}
finish();
}
}

View File

@@ -8,17 +8,17 @@ import com.limelight.nvstream.av.audio.AudioRenderer;
import com.limelight.nvstream.http.LimelightCryptoProvider;
public class PlatformBinding {
public static String getDeviceName() {
String deviceName = android.os.Build.MODEL;
public static String getDeviceName() {
String deviceName = android.os.Build.MODEL;
deviceName = deviceName.replace(" ", "");
return deviceName;
}
public static AudioRenderer getAudioRenderer() {
return new AndroidAudioRenderer();
}
public static LimelightCryptoProvider getCryptoProvider(Context c) {
return new AndroidCryptoProvider(c);
}
}
public static AudioRenderer getAudioRenderer() {
return new AndroidAudioRenderer();
}
public static LimelightCryptoProvider getCryptoProvider(Context c) {
return new AndroidCryptoProvider(c);
}
}

View File

@@ -1,70 +1,189 @@
package com.limelight.binding.audio;
import android.media.AudioAttributes;
import android.media.AudioFormat;
import android.media.AudioManager;
import android.media.AudioTrack;
import android.os.Build;
import com.limelight.LimeLog;
import com.limelight.nvstream.av.audio.AudioRenderer;
import com.limelight.nvstream.jni.MoonBridge;
public class AndroidAudioRenderer implements AudioRenderer {
public static final int FRAME_SIZE = 960;
private AudioTrack track;
private AudioTrack track;
@Override
public boolean streamInitialized(int channelCount, int sampleRate) {
int channelConfig;
int bufferSize;
private AudioTrack createAudioTrack(int channelConfig, int bufferSize, boolean lowLatency) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
return new AudioTrack(AudioManager.STREAM_MUSIC,
48000,
channelConfig,
AudioFormat.ENCODING_PCM_16BIT,
bufferSize,
AudioTrack.MODE_STREAM);
}
else {
AudioAttributes.Builder attributesBuilder = new AudioAttributes.Builder()
.setUsage(AudioAttributes.USAGE_GAME);
AudioFormat format = new AudioFormat.Builder()
.setEncoding(AudioFormat.ENCODING_PCM_16BIT)
.setSampleRate(48000)
.setChannelMask(channelConfig)
.build();
switch (channelCount)
{
case 1:
channelConfig = AudioFormat.CHANNEL_OUT_MONO;
break;
case 2:
channelConfig = AudioFormat.CHANNEL_OUT_STEREO;
break;
default:
LimeLog.severe("Decoder returned unhandled channel count");
return false;
}
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
// Use FLAG_LOW_LATENCY on L through N
if (lowLatency) {
attributesBuilder.setFlags(AudioAttributes.FLAG_LOW_LATENCY);
}
}
bufferSize = Math.max(AudioTrack.getMinBufferSize(sampleRate,
channelConfig,
AudioFormat.ENCODING_PCM_16BIT),
FRAME_SIZE * 2);
// Round to next frame
bufferSize = (((bufferSize + (FRAME_SIZE - 1)) / FRAME_SIZE) * FRAME_SIZE);
LimeLog.info("Audio track buffer size: "+bufferSize);
track = new AudioTrack(AudioManager.STREAM_MUSIC,
sampleRate,
channelConfig,
AudioFormat.ENCODING_PCM_16BIT,
bufferSize,
AudioTrack.MODE_STREAM);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
AudioTrack.Builder trackBuilder = new AudioTrack.Builder()
.setAudioFormat(format)
.setAudioAttributes(attributesBuilder.build())
.setTransferMode(AudioTrack.MODE_STREAM)
.setBufferSizeInBytes(bufferSize);
track.play();
return true;
}
// Use PERFORMANCE_MODE_LOW_LATENCY on O and later
if (lowLatency) {
trackBuilder.setPerformanceMode(AudioTrack.PERFORMANCE_MODE_LOW_LATENCY);
}
@Override
public void playDecodedAudio(byte[] audioData, int offset, int length) {
track.write(audioData, offset, length);
}
return trackBuilder.build();
}
else {
return new AudioTrack(attributesBuilder.build(),
format,
bufferSize,
AudioTrack.MODE_STREAM,
AudioManager.AUDIO_SESSION_ID_GENERATE);
}
}
}
@Override
public void streamClosing() {
if (track != null) {
track.release();
}
}
@Override
public int setup(int audioConfiguration) {
int channelConfig;
int bytesPerFrame;
@Override
public int getCapabilities() {
return 0;
}
switch (audioConfiguration)
{
case MoonBridge.AUDIO_CONFIGURATION_STEREO:
channelConfig = AudioFormat.CHANNEL_OUT_STEREO;
bytesPerFrame = 2 * 240 * 2;
break;
case MoonBridge.AUDIO_CONFIGURATION_51_SURROUND:
channelConfig = AudioFormat.CHANNEL_OUT_5POINT1;
bytesPerFrame = 6 * 240 * 2;
break;
default:
LimeLog.severe("Decoder returned unhandled channel count");
return -1;
}
// We're not supposed to request less than the minimum
// buffer size for our buffer, but it appears that we can
// do this on many devices and it lowers audio latency.
// We'll try the small buffer size first and if it fails,
// use the recommended larger buffer size.
for (int i = 0; i < 4; i++) {
boolean lowLatency;
int bufferSize;
// We will try:
// 1) Small buffer, low latency mode
// 2) Large buffer, low latency mode
// 3) Small buffer, standard mode
// 4) Large buffer, standard mode
switch (i) {
case 0:
case 1:
lowLatency = true;
break;
case 2:
case 3:
lowLatency = false;
break;
default:
// Unreachable
throw new IllegalStateException();
}
switch (i) {
case 0:
case 2:
bufferSize = bytesPerFrame * 2;
break;
case 1:
case 3:
// Try the larger buffer size
bufferSize = Math.max(AudioTrack.getMinBufferSize(48000,
channelConfig,
AudioFormat.ENCODING_PCM_16BIT),
bytesPerFrame * 2);
// Round to next frame
bufferSize = (((bufferSize + (bytesPerFrame - 1)) / bytesPerFrame) * bytesPerFrame);
break;
default:
// Unreachable
throw new IllegalStateException();
}
// Skip low latency options if hardware sample rate isn't 48000Hz
if (AudioTrack.getNativeOutputSampleRate(AudioManager.STREAM_MUSIC) != 48000 && lowLatency) {
continue;
}
try {
track = createAudioTrack(channelConfig, bufferSize, lowLatency);
track.play();
// Successfully created working AudioTrack. We're done here.
LimeLog.info("Audio track configuration: "+bufferSize+" "+lowLatency);
break;
} catch (Exception e) {
// Try to release the AudioTrack if we got far enough
e.printStackTrace();
try {
if (track != null) {
track.release();
track = null;
}
} catch (Exception ignored) {}
}
}
if (track == null) {
// Couldn't create any audio track for playback
return -2;
}
return 0;
}
@Override
public void playDecodedAudio(byte[] audioData) {
track.write(audioData, 0, audioData.length);
}
@Override
public void start() {}
@Override
public void stop() {}
@Override
public void cleanup() {
// Immediately drop all pending data
track.pause();
track.flush();
track.release();
}
}

View File

@@ -13,8 +13,8 @@ import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.Provider;
import java.security.SecureRandom;
import java.security.Security;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
@@ -45,239 +45,228 @@ import com.limelight.nvstream.http.LimelightCryptoProvider;
public class AndroidCryptoProvider implements LimelightCryptoProvider {
private File certFile;
private File keyFile;
private X509Certificate cert;
private RSAPrivateKey key;
private byte[] pemCertBytes;
private static final Object globalCryptoLock = new Object();
static {
// Install the Bouncy Castle provider
Security.addProvider(new BouncyCastleProvider());
}
public AndroidCryptoProvider(Context c) {
String dataPath = c.getFilesDir().getAbsolutePath();
certFile = new File(dataPath + File.separator + "client.crt");
keyFile = new File(dataPath + File.separator + "client.key");
}
private byte[] loadFileToBytes(File f) {
if (!f.exists()) {
return null;
}
try {
FileInputStream fin = new FileInputStream(f);
byte[] fileData = new byte[(int) f.length()];
if (fin.read(fileData) != f.length()) {
private final File certFile;
private final File keyFile;
private X509Certificate cert;
private RSAPrivateKey key;
private byte[] pemCertBytes;
private static final Object globalCryptoLock = new Object();
private static final Provider bcProvider = new BouncyCastleProvider();
public AndroidCryptoProvider(Context c) {
String dataPath = c.getFilesDir().getAbsolutePath();
certFile = new File(dataPath + File.separator + "client.crt");
keyFile = new File(dataPath + File.separator + "client.key");
}
private byte[] loadFileToBytes(File f) {
if (!f.exists()) {
return null;
}
try {
FileInputStream fin = new FileInputStream(f);
byte[] fileData = new byte[(int) f.length()];
if (fin.read(fileData) != f.length()) {
// Failed to read
fileData = null;
}
fin.close();
return fileData;
} catch (IOException e) {
return null;
}
}
private boolean loadCertKeyPair() {
byte[] certBytes = loadFileToBytes(certFile);
byte[] keyBytes = loadFileToBytes(keyFile);
// If either file was missing, we definitely can't succeed
if (certBytes == null || keyBytes == null) {
LimeLog.info("Missing cert or key; need to generate a new one");
return false;
}
try {
CertificateFactory certFactory = CertificateFactory.getInstance("X.509", "BC");
cert = (X509Certificate) certFactory.generateCertificate(new ByteArrayInputStream(certBytes));
pemCertBytes = certBytes;
KeyFactory keyFactory = KeyFactory.getInstance("RSA", "BC");
key = (RSAPrivateKey) keyFactory.generatePrivate(new PKCS8EncodedKeySpec(keyBytes));
} catch (CertificateException e) {
// May happen if the cert is corrupt
LimeLog.warning("Corrupted certificate");
return false;
} catch (NoSuchAlgorithmException e) {
// Should never happen
e.printStackTrace();
return false;
} catch (InvalidKeySpecException e) {
// May happen if the key is corrupt
LimeLog.warning("Corrupted key");
return false;
} catch (NoSuchProviderException e) {
// Should never happen
e.printStackTrace();
return false;
}
return true;
}
@SuppressLint("TrulyRandom")
private boolean generateCertKeyPair() {
byte[] snBytes = new byte[8];
new SecureRandom().nextBytes(snBytes);
KeyPair keyPair;
try {
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA", "BC");
keyPairGenerator.initialize(2048);
keyPair = keyPairGenerator.generateKeyPair();
} catch (NoSuchAlgorithmException e1) {
// Should never happen
e1.printStackTrace();
return false;
} catch (NoSuchProviderException e) {
// Should never happen
e.printStackTrace();
return false;
}
Date now = new Date();
// Expires in 20 years
Calendar calendar = Calendar.getInstance();
calendar.setTime(now);
calendar.add(Calendar.YEAR, 20);
Date expirationDate = calendar.getTime();
BigInteger serial = new BigInteger(snBytes).abs();
X500NameBuilder nameBuilder = new X500NameBuilder(BCStyle.INSTANCE);
nameBuilder.addRDN(BCStyle.CN, "NVIDIA GameStream Client");
X500Name name = nameBuilder.build();
X509v3CertificateBuilder certBuilder = new X509v3CertificateBuilder(name, serial, now, expirationDate, Locale.ENGLISH, name,
SubjectPublicKeyInfo.getInstance(keyPair.getPublic().getEncoded()));
try {
ContentSigner sigGen = new JcaContentSignerBuilder("SHA1withRSA").setProvider(BouncyCastleProvider.PROVIDER_NAME).build(keyPair.getPrivate());
cert = new JcaX509CertificateConverter().setProvider(BouncyCastleProvider.PROVIDER_NAME).getCertificate(certBuilder.build(sigGen));
key = (RSAPrivateKey) keyPair.getPrivate();
} catch (Exception e) {
// Nothing should go wrong here
e.printStackTrace();
return false;
}
LimeLog.info("Generated a new key pair");
// Save the resulting pair
saveCertKeyPair();
return true;
}
private void saveCertKeyPair() {
try {
FileOutputStream certOut = new FileOutputStream(certFile);
FileOutputStream keyOut = new FileOutputStream(keyFile);
// Write the certificate in OpenSSL PEM format (important for the server)
StringWriter strWriter = new StringWriter();
JcaPEMWriter pemWriter = new JcaPEMWriter(strWriter);
pemWriter.writeObject(cert);
pemWriter.close();
// Line endings MUST be UNIX for the PC to accept the cert properly
OutputStreamWriter certWriter = new OutputStreamWriter(certOut);
String pemStr = strWriter.getBuffer().toString();
for (int i = 0; i < pemStr.length(); i++) {
char c = pemStr.charAt(i);
if (c != '\r')
certWriter.append(c);
}
certWriter.close();
// Write the private out in PKCS8 format
keyOut.write(key.getEncoded());
certOut.close();
keyOut.close();
LimeLog.info("Saved generated key pair to disk");
} catch (IOException e) {
// This isn't good because it means we'll have
// to re-pair next time
e.printStackTrace();
}
}
public X509Certificate getClientCertificate() {
// Use a lock here to ensure only one guy will be generating or loading
// the certificate and key at a time
synchronized (globalCryptoLock) {
// Return a loaded cert if we have one
if (cert != null) {
return cert;
}
// No loaded cert yet, let's see if we have one on disk
if (loadCertKeyPair()) {
// Got one
return cert;
}
// Try to generate a new key pair
if (!generateCertKeyPair()) {
// Failed
return null;
}
// Load the generated pair
loadCertKeyPair();
return cert;
}
}
fin.close();
return fileData;
} catch (IOException e) {
return null;
}
}
public RSAPrivateKey getClientPrivateKey() {
// Use a lock here to ensure only one guy will be generating or loading
// the certificate and key at a time
synchronized (globalCryptoLock) {
// Return a loaded key if we have one
if (key != null) {
return key;
}
// No loaded key yet, let's see if we have one on disk
if (loadCertKeyPair()) {
// Got one
return key;
}
// Try to generate a new key pair
if (!generateCertKeyPair()) {
// Failed
return null;
}
// Load the generated pair
loadCertKeyPair();
return key;
}
}
public byte[] getPemEncodedClientCertificate() {
synchronized (globalCryptoLock) {
// Call our helper function to do the cert loading/generation for us
getClientCertificate();
// Return a cached value if we have it
return pemCertBytes;
}
}
private boolean loadCertKeyPair() {
byte[] certBytes = loadFileToBytes(certFile);
byte[] keyBytes = loadFileToBytes(keyFile);
@Override
public String encodeBase64String(byte[] data) {
return Base64.encodeToString(data, Base64.NO_WRAP);
}
// If either file was missing, we definitely can't succeed
if (certBytes == null || keyBytes == null) {
LimeLog.info("Missing cert or key; need to generate a new one");
return false;
}
try {
CertificateFactory certFactory = CertificateFactory.getInstance("X.509", bcProvider);
cert = (X509Certificate) certFactory.generateCertificate(new ByteArrayInputStream(certBytes));
pemCertBytes = certBytes;
KeyFactory keyFactory = KeyFactory.getInstance("RSA", bcProvider);
key = (RSAPrivateKey) keyFactory.generatePrivate(new PKCS8EncodedKeySpec(keyBytes));
} catch (CertificateException e) {
// May happen if the cert is corrupt
LimeLog.warning("Corrupted certificate");
return false;
} catch (NoSuchAlgorithmException e) {
// Should never happen
e.printStackTrace();
return false;
} catch (InvalidKeySpecException e) {
// May happen if the key is corrupt
LimeLog.warning("Corrupted key");
return false;
}
return true;
}
@SuppressLint("TrulyRandom")
private boolean generateCertKeyPair() {
byte[] snBytes = new byte[8];
new SecureRandom().nextBytes(snBytes);
KeyPair keyPair;
try {
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA", bcProvider);
keyPairGenerator.initialize(2048);
keyPair = keyPairGenerator.generateKeyPair();
} catch (NoSuchAlgorithmException e1) {
// Should never happen
e1.printStackTrace();
return false;
}
Date now = new Date();
// Expires in 20 years
Calendar calendar = Calendar.getInstance();
calendar.setTime(now);
calendar.add(Calendar.YEAR, 20);
Date expirationDate = calendar.getTime();
BigInteger serial = new BigInteger(snBytes).abs();
X500NameBuilder nameBuilder = new X500NameBuilder(BCStyle.INSTANCE);
nameBuilder.addRDN(BCStyle.CN, "NVIDIA GameStream Client");
X500Name name = nameBuilder.build();
X509v3CertificateBuilder certBuilder = new X509v3CertificateBuilder(name, serial, now, expirationDate, Locale.ENGLISH, name,
SubjectPublicKeyInfo.getInstance(keyPair.getPublic().getEncoded()));
try {
ContentSigner sigGen = new JcaContentSignerBuilder("SHA1withRSA").setProvider(bcProvider).build(keyPair.getPrivate());
cert = new JcaX509CertificateConverter().setProvider(bcProvider).getCertificate(certBuilder.build(sigGen));
key = (RSAPrivateKey) keyPair.getPrivate();
} catch (Exception e) {
// Nothing should go wrong here
e.printStackTrace();
return false;
}
LimeLog.info("Generated a new key pair");
// Save the resulting pair
saveCertKeyPair();
return true;
}
private void saveCertKeyPair() {
try {
FileOutputStream certOut = new FileOutputStream(certFile);
FileOutputStream keyOut = new FileOutputStream(keyFile);
// Write the certificate in OpenSSL PEM format (important for the server)
StringWriter strWriter = new StringWriter();
JcaPEMWriter pemWriter = new JcaPEMWriter(strWriter);
pemWriter.writeObject(cert);
pemWriter.close();
// Line endings MUST be UNIX for the PC to accept the cert properly
OutputStreamWriter certWriter = new OutputStreamWriter(certOut);
String pemStr = strWriter.getBuffer().toString();
for (int i = 0; i < pemStr.length(); i++) {
char c = pemStr.charAt(i);
if (c != '\r')
certWriter.append(c);
}
certWriter.close();
// Write the private out in PKCS8 format
keyOut.write(key.getEncoded());
certOut.close();
keyOut.close();
LimeLog.info("Saved generated key pair to disk");
} catch (IOException e) {
// This isn't good because it means we'll have
// to re-pair next time
e.printStackTrace();
}
}
public X509Certificate getClientCertificate() {
// Use a lock here to ensure only one guy will be generating or loading
// the certificate and key at a time
synchronized (globalCryptoLock) {
// Return a loaded cert if we have one
if (cert != null) {
return cert;
}
// No loaded cert yet, let's see if we have one on disk
if (loadCertKeyPair()) {
// Got one
return cert;
}
// Try to generate a new key pair
if (!generateCertKeyPair()) {
// Failed
return null;
}
// Load the generated pair
loadCertKeyPair();
return cert;
}
}
public RSAPrivateKey getClientPrivateKey() {
// Use a lock here to ensure only one guy will be generating or loading
// the certificate and key at a time
synchronized (globalCryptoLock) {
// Return a loaded key if we have one
if (key != null) {
return key;
}
// No loaded key yet, let's see if we have one on disk
if (loadCertKeyPair()) {
// Got one
return key;
}
// Try to generate a new key pair
if (!generateCertKeyPair()) {
// Failed
return null;
}
// Load the generated pair
loadCertKeyPair();
return key;
}
}
public byte[] getPemEncodedClientCertificate() {
synchronized (globalCryptoLock) {
// Call our helper function to do the cert loading/generation for us
getClientCertificate();
// Return a cached value if we have it
return pemCertBytes;
}
}
@Override
public String encodeBase64String(byte[] data) {
return Base64.encodeToString(data, Base64.NO_WRAP);
}
}

View File

@@ -2,41 +2,31 @@ package com.limelight.binding.input;
import android.view.KeyEvent;
import com.limelight.nvstream.NvConnection;
import com.limelight.nvstream.input.KeycodeTranslator;
/**
* Class to translate a Android key code into the codes GFE is expecting
* @author Diego Waxemberg
* @author Cameron Gutman
*/
public class KeyboardTranslator extends KeycodeTranslator {
public class KeyboardTranslator {
/**
* GFE's prefix for every key code
*/
public static final short KEY_PREFIX = (short) 0x80;
private static final short KEY_PREFIX = (short) 0x80;
public static final int VK_0 = 48;
public static final int VK_9 = 57;
public static final int VK_A = 65;
public static final int VK_Z = 90;
public static final int VK_ALT = 18;
public static final int VK_NUMPAD0 = 96;
public static final int VK_BACK_SLASH = 92;
public static final int VK_CAPS_LOCK = 20;
public static final int VK_BACK_SLASH = 92;
public static final int VK_CAPS_LOCK = 20;
public static final int VK_CLEAR = 12;
public static final int VK_COMMA = 44;
public static final int VK_CONTROL = 17;
public static final int VK_BACK_SPACE = 8;
public static final int VK_EQUALS = 61;
public static final int VK_ESCAPE = 27;
public static final int VK_F1 = 112;
public static final int VK_PERIOD = 46;
public static final int VK_INSERT = 155;
public static final int VK_OPEN_BRACKET = 91;
public static final int VK_WINDOWS = 524;
public static final int VK_MINUS = 45;
public static final int VK_END = 35;
public static final int VK_HOME = 36;
public static final int VK_NUM_LOCK = 144;
@@ -46,7 +36,6 @@ public class KeyboardTranslator extends KeycodeTranslator {
public static final int VK_CLOSE_BRACKET = 93;
public static final int VK_SCROLL_LOCK = 145;
public static final int VK_SEMICOLON = 59;
public static final int VK_SHIFT = 16;
public static final int VK_SLASH = 47;
public static final int VK_SPACE = 32;
public static final int VK_PRINTSCREEN = 154;
@@ -59,27 +48,17 @@ public class KeyboardTranslator extends KeycodeTranslator {
public static final int VK_QUOTE = 222;
public static final int VK_PAUSE = 19;
/**
* Constructs a new translator for the specified connection
* @param conn the connection to which the translated codes are sent
*/
public KeyboardTranslator(NvConnection conn) {
super(conn);
}
/**
* Translates the given keycode and returns the GFE keycode
* @param keycode the code to be translated
* @return a GFE keycode for the given keycode
*/
@Override
public short translate(int keycode) {
public static short translate(int keycode) {
int translated;
/* There seems to be no clean mapping between Android key codes
* and what Nvidia sends over the wire. If someone finds one,
* I'll happily delete this code :)
*/
// This is a poor man's mapping between Android key codes
// and Windows VK_* codes. For all defined VK_ codes, see:
// https://msdn.microsoft.com/en-us/library/windows/desktop/dd375731(v=vs.85).aspx
if (keycode >= KeyEvent.KEYCODE_0 &&
keycode <= KeyEvent.KEYCODE_9) {
translated = (keycode - KeyEvent.KEYCODE_0) + VK_0;
@@ -99,8 +78,11 @@ public class KeyboardTranslator extends KeycodeTranslator {
else {
switch (keycode) {
case KeyEvent.KEYCODE_ALT_LEFT:
translated = 0xA4;
break;
case KeyEvent.KEYCODE_ALT_RIGHT:
translated = VK_ALT;
translated = 0xA5;
break;
case KeyEvent.KEYCODE_BACKSLASH:
@@ -120,8 +102,11 @@ public class KeyboardTranslator extends KeycodeTranslator {
break;
case KeyEvent.KEYCODE_CTRL_LEFT:
translated = 0xA2;
break;
case KeyEvent.KEYCODE_CTRL_RIGHT:
translated = VK_CONTROL;
translated = 0xA3;
break;
case KeyEvent.KEYCODE_DEL:
@@ -141,23 +126,25 @@ public class KeyboardTranslator extends KeycodeTranslator {
break;
case KeyEvent.KEYCODE_FORWARD_DEL:
// Nvidia maps period to delete
translated = VK_PERIOD;
translated = 0x2e;
break;
case KeyEvent.KEYCODE_INSERT:
translated = -1;
translated = 0x2d;
break;
case KeyEvent.KEYCODE_LEFT_BRACKET:
translated = 0xdb;
break;
case KeyEvent.KEYCODE_META_LEFT:
case KeyEvent.KEYCODE_META_RIGHT:
translated = VK_WINDOWS;
translated = 0x5b;
break;
case KeyEvent.KEYCODE_META_RIGHT:
translated = 0x5c;
break;
case KeyEvent.KEYCODE_MINUS:
translated = 0xbd;
break;
@@ -199,8 +186,11 @@ public class KeyboardTranslator extends KeycodeTranslator {
break;
case KeyEvent.KEYCODE_SHIFT_LEFT:
translated = 0xA0;
break;
case KeyEvent.KEYCODE_SHIFT_RIGHT:
translated = VK_SHIFT;
translated = 0xA1;
break;
case KeyEvent.KEYCODE_SLASH:
@@ -247,6 +237,26 @@ public class KeyboardTranslator extends KeycodeTranslator {
case KeyEvent.KEYCODE_BREAK:
translated = VK_PAUSE;
break;
case KeyEvent.KEYCODE_NUMPAD_DIVIDE:
translated = 0x6F;
break;
case KeyEvent.KEYCODE_NUMPAD_MULTIPLY:
translated = 0x6A;
break;
case KeyEvent.KEYCODE_NUMPAD_SUBTRACT:
translated = 0x6D;
break;
case KeyEvent.KEYCODE_NUMPAD_ADD:
translated = 0x6B;
break;
case KeyEvent.KEYCODE_NUMPAD_DOT:
translated = 0x6E;
break;
default:
System.out.println("No key for "+keycode);

View File

@@ -1,93 +1,241 @@
package com.limelight.binding.input;
import android.view.View;
import com.limelight.nvstream.NvConnection;
import com.limelight.nvstream.input.MouseButtonPacket;
import java.util.Timer;
import java.util.TimerTask;
public class TouchContext {
private int lastTouchX = 0;
private int lastTouchY = 0;
private int originalTouchX = 0;
private int originalTouchY = 0;
private long originalTouchTime = 0;
private NvConnection conn;
private int actionIndex;
private static final int TAP_MOVEMENT_THRESHOLD = 10;
private static final int TAP_TIME_THRESHOLD = 250;
public TouchContext(NvConnection conn, int actionIndex)
{
this.conn = conn;
this.actionIndex = actionIndex;
}
private boolean isTap()
{
int xDelta = Math.abs(lastTouchX - originalTouchX);
int yDelta = Math.abs(lastTouchY - originalTouchY);
long timeDelta = System.currentTimeMillis() - originalTouchTime;
return xDelta <= TAP_MOVEMENT_THRESHOLD &&
yDelta <= TAP_MOVEMENT_THRESHOLD &&
timeDelta <= TAP_TIME_THRESHOLD;
}
private byte getMouseButtonIndex()
{
if (actionIndex == 1) {
return MouseButtonPacket.BUTTON_RIGHT;
}
else {
return MouseButtonPacket.BUTTON_LEFT;
}
}
public boolean touchDownEvent(int eventX, int eventY)
{
originalTouchX = lastTouchX = eventX;
originalTouchY = lastTouchY = eventY;
originalTouchTime = System.currentTimeMillis();
return true;
}
public void touchUpEvent(int eventX, int eventY)
{
if (isTap())
{
byte buttonIndex = getMouseButtonIndex();
// Lower the mouse button
conn.sendMouseButtonDown(buttonIndex);
// We need to sleep a bit here because some games
// do input detection by polling
try {
Thread.sleep(100);
} catch (InterruptedException ignored) {}
// Raise the mouse button
conn.sendMouseButtonUp(buttonIndex);
}
}
public boolean touchMoveEvent(int eventX, int eventY)
{
if (eventX != lastTouchX || eventY != lastTouchY)
{
// We only send moves for the primary touch point
if (actionIndex == 0) {
conn.sendMouseMove((short)(eventX - lastTouchX),
(short)(eventY - lastTouchY));
}
lastTouchX = eventX;
lastTouchY = eventY;
return true;
}
return false;
}
private int lastTouchX = 0;
private int lastTouchY = 0;
private int originalTouchX = 0;
private int originalTouchY = 0;
private long originalTouchTime = 0;
private boolean cancelled;
private boolean confirmedMove;
private boolean confirmedDrag;
private Timer dragTimer;
private double distanceMoved;
private double xFactor, yFactor;
private final NvConnection conn;
private final int actionIndex;
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,
int referenceWidth, int referenceHeight, View view)
{
this.conn = conn;
this.actionIndex = actionIndex;
this.referenceWidth = referenceWidth;
this.referenceHeight = referenceHeight;
this.targetView = view;
}
public int getActionIndex()
{
return actionIndex;
}
private boolean isWithinTapBounds(int touchX, int touchY)
{
int xDelta = Math.abs(touchX - originalTouchX);
int yDelta = Math.abs(touchY - originalTouchY);
return xDelta <= TAP_MOVEMENT_THRESHOLD &&
yDelta <= TAP_MOVEMENT_THRESHOLD;
}
private boolean isTap()
{
long timeDelta = System.currentTimeMillis() - originalTouchTime;
return isWithinTapBounds(lastTouchX, lastTouchY) && timeDelta <= TAP_TIME_THRESHOLD;
}
private byte getMouseButtonIndex()
{
if (actionIndex == 1) {
return MouseButtonPacket.BUTTON_RIGHT;
}
else {
return MouseButtonPacket.BUTTON_LEFT;
}
}
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();
cancelled = confirmedDrag = confirmedMove = false;
distanceMoved = 0;
if (actionIndex == 0) {
// Start the timer for engaging a drag
startDragTimer();
}
return true;
}
public void touchUpEvent(int eventX, int eventY)
{
if (cancelled) {
return;
}
// Cancel the drag timer
cancelDragTimer();
byte buttonIndex = getMouseButtonIndex();
if (confirmedDrag) {
// Raise the button after a drag
conn.sendMouseButtonUp(buttonIndex);
}
else if (isTap())
{
// Lower the mouse button
conn.sendMouseButtonDown(buttonIndex);
// We need to sleep a bit here because some games
// do input detection by polling
try {
Thread.sleep(100);
} catch (InterruptedException ignored) {}
// Raise the mouse button
conn.sendMouseButtonUp(buttonIndex);
}
}
private synchronized void startDragTimer() {
dragTimer = new Timer(true);
dragTimer.schedule(new TimerTask() {
@Override
public void run() {
synchronized (TouchContext.this) {
// Check if someone already set move
if (confirmedMove) {
return;
}
// Check if someone cancelled us
if (dragTimer == null) {
return;
}
// Uncancellable now
dragTimer = null;
// We haven't been cancelled before the timer expired so begin dragging
confirmedDrag = true;
conn.sendMouseButtonDown(getMouseButtonIndex());
}
}
}, DRAG_TIME_THRESHOLD);
}
private synchronized void cancelDragTimer() {
if (dragTimer != null) {
dragTimer.cancel();
dragTimer = null;
}
}
private synchronized void checkForConfirmedMove(int eventX, int eventY) {
// If we've already confirmed something, get out now
if (confirmedMove || confirmedDrag) {
return;
}
// If it leaves the tap bounds before the drag time expires, it's a move.
if (!isWithinTapBounds(eventX, eventY)) {
confirmedMove = true;
cancelDragTimer();
return;
}
// Check if we've exceeded the maximum distance moved
distanceMoved += Math.sqrt(Math.pow(eventX - lastTouchX, 2) + Math.pow(eventY - lastTouchY, 2));
if (distanceMoved >= TAP_DISTANCE_THRESHOLD) {
confirmedMove = true;
cancelDragTimer();
return;
}
}
public boolean touchMoveEvent(int eventX, int eventY)
{
if (eventX != lastTouchX || eventY != lastTouchY)
{
// We only send moves and drags for the primary touch point
if (actionIndex == 0) {
checkForConfirmedMove(eventX, eventY);
int deltaX = eventX - lastTouchX;
int deltaY = eventY - lastTouchY;
// Scale the deltas based on the factors passed to our constructor
deltaX = (int)Math.round((double)Math.abs(deltaX) * xFactor);
deltaY = (int)Math.round((double)Math.abs(deltaY) * yFactor);
// Fix up the signs
if (eventX < lastTouchX) {
deltaX = -deltaX;
}
if (eventY < lastTouchY) {
deltaY = -deltaY;
}
// If the scaling factor ended up rounding deltas to zero, wait until they are
// non-zero to update lastTouch that way devices that report small touch events often
// will work correctly
if (deltaX != 0) {
lastTouchX = eventX;
}
if (deltaY != 0) {
lastTouchY = eventY;
}
conn.sendMouseMove((short)deltaX, (short)deltaY);
}
else {
lastTouchX = eventX;
lastTouchY = eventY;
}
}
return true;
}
public void cancelTouch() {
cancelled = true;
// Cancel the drag timer
cancelDragTimer();
// If it was a confirmed drag, we'll need to raise the button now
if (confirmedDrag) {
conn.sendMouseButtonUp(getMouseButtonIndex());
}
}
public boolean isCancelled() {
return cancelled;
}
}

View File

@@ -0,0 +1,61 @@
package com.limelight.binding.input.capture;
import android.annotation.TargetApi;
import android.os.Build;
import android.view.InputDevice;
import android.view.MotionEvent;
import android.view.View;
@TargetApi(Build.VERSION_CODES.O)
public class AndroidNativePointerCaptureProvider extends InputCaptureProvider {
private View targetView;
public AndroidNativePointerCaptureProvider(View targetView) {
this.targetView = targetView;
}
public static boolean isCaptureProviderSupported() {
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.O;
}
@Override
public void enableCapture() {
super.enableCapture();
targetView.requestPointerCapture();
}
@Override
public void disableCapture() {
super.disableCapture();
targetView.releasePointerCapture();
}
@Override
public boolean isCapturingActive() {
return targetView.hasPointerCapture();
}
@Override
public boolean eventHasRelativeMouseAxes(MotionEvent event) {
return event.getSource() == InputDevice.SOURCE_MOUSE_RELATIVE;
}
@Override
public float getRelativeAxisX(MotionEvent event) {
float x = event.getX();
for (int i = 0; i < event.getHistorySize(); i++) {
x += event.getHistoricalX(i);
}
return x;
}
@Override
public float getRelativeAxisY(MotionEvent event) {
float y = event.getY();
for (int i = 0; i < event.getHistorySize(); i++) {
y += event.getHistoricalY(i);
}
return y;
}
}

View File

@@ -0,0 +1,61 @@
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 AndroidPointerIconCaptureProvider extends InputCaptureProvider {
private ViewGroup rootViewGroup;
private Context context;
public AndroidPointerIconCaptureProvider(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() {
super.enableCapture();
setPointerIconOnAllViews(PointerIcon.getSystemIcon(context, PointerIcon.TYPE_NULL));
}
@Override
public void disableCapture() {
super.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);
}
}

View File

@@ -0,0 +1,38 @@
package com.limelight.binding.input.capture;
import android.app.Activity;
import com.limelight.LimeLog;
import com.limelight.LimelightBuildProps;
import com.limelight.R;
import com.limelight.binding.input.evdev.EvdevCaptureProviderShim;
import com.limelight.binding.input.evdev.EvdevListener;
public class InputCaptureManager {
public static InputCaptureProvider getInputCaptureProvider(Activity activity, EvdevListener rootListener) {
if (AndroidNativePointerCaptureProvider.isCaptureProviderSupported()) {
LimeLog.info("Using Android O+ native mouse capture");
return new AndroidNativePointerCaptureProvider(activity.findViewById(R.id.surfaceView));
}
// LineageOS implemented broken NVIDIA capture extensions, so avoid using them on root builds.
// See https://github.com/LineageOS/android_frameworks_base/commit/d304f478a023430f4712dbdc3ee69d9ad02cebd3
else if (!LimelightBuildProps.ROOT_BUILD && ShieldCaptureProvider.isCaptureProviderSupported()) {
LimeLog.info("Using NVIDIA mouse capture extension");
return new ShieldCaptureProvider(activity);
}
else if (EvdevCaptureProviderShim.isCaptureProviderSupported()) {
LimeLog.info("Using Evdev mouse capture");
return EvdevCaptureProviderShim.createEvdevCaptureProvider(activity, rootListener);
}
else if (AndroidPointerIconCaptureProvider.isCaptureProviderSupported()) {
// Android N's native capture can't capture over system UI elements
// so we want to only use it if there's no other option.
LimeLog.info("Using Android N+ pointer hiding");
return new AndroidPointerIconCaptureProvider(activity);
}
else {
LimeLog.info("Mouse capture not available");
return new NullCaptureProvider();
}
}
}

View File

@@ -0,0 +1,36 @@
package com.limelight.binding.input.capture;
import android.view.MotionEvent;
public abstract class InputCaptureProvider {
protected boolean isCapturing;
public void enableCapture() {
isCapturing = true;
}
public void disableCapture() {
isCapturing = false;
}
public void destroy() {}
public boolean isCapturingEnabled() {
return isCapturing;
}
public boolean isCapturingActive() {
return isCapturing;
}
public boolean eventHasRelativeMouseAxes(MotionEvent event) {
return false;
}
public float getRelativeAxisX(MotionEvent event) {
return 0;
}
public float getRelativeAxisY(MotionEvent event) {
return 0;
}
}

View File

@@ -0,0 +1,4 @@
package com.limelight.binding.input.capture;
public class NullCaptureProvider extends InputCaptureProvider {}

View File

@@ -0,0 +1,91 @@
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() {
super.enableCapture();
setCursorVisibility(false);
}
@Override
public void disableCapture() {
super.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);
}
}

View File

@@ -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);
}
}

View File

@@ -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;
}
// Report that we're added _before_ starting the input thread
notifyDeviceAdded();
// Start listening for controller input
inputThread = createInputThread();
inputThread.start();
return true;
}
public void stop() {
if (stopped) {
return;
}
stopped = true;
// Stop the input thread
if (inputThread != null) {
inputThread.interrupt();
inputThread = null;
}
// Close the USB connection
connection.close();
// Report the device removed
notifyDeviceRemoved();
}
protected abstract boolean handleRead(ByteBuffer buffer);
protected abstract boolean doInit();
}

View File

@@ -0,0 +1,11 @@
package com.limelight.binding.input.driver;
public interface UsbDriverListener {
void reportControllerState(int controllerId, short buttonFlags,
float leftStickX, float leftStickY,
float rightStickX, float rightStickY,
float leftTrigger, float rightTrigger);
void deviceRemoved(int controllerId);
void deviceAdded(int controllerId);
}

View File

@@ -0,0 +1,245 @@
package com.limelight.binding.input.driver;
import android.app.PendingIntent;
import android.app.Service;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
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.Handler;
import android.os.IBinder;
import android.view.InputDevice;
import android.widget.Toast;
import com.limelight.LimeLog;
import com.limelight.R;
import com.limelight.preferences.PreferenceConfiguration;
import java.util.ArrayList;
public class UsbDriverService extends Service implements UsbDriverListener {
private static final String ACTION_USB_PERMISSION =
"com.limelight.USB_PERMISSION";
private UsbManager usbManager;
private PreferenceConfiguration prefConfig;
private final UsbEventReceiver receiver = new UsbEventReceiver();
private final UsbDriverBinder binder = new UsbDriverBinder();
private final ArrayList<AbstractController> controllers = new ArrayList<>();
private UsbDriverListener listener;
private int nextDeviceId;
@Override
public void reportControllerState(int controllerId, short buttonFlags, float leftStickX, float leftStickY, float rightStickX, float rightStickY, float leftTrigger, float rightTrigger) {
// Call through to the client's listener
if (listener != null) {
listener.reportControllerState(controllerId, buttonFlags, leftStickX, leftStickY, rightStickX, rightStickY, leftTrigger, rightTrigger);
}
}
@Override
public void deviceRemoved(int controllerId) {
// Remove the the controller from our list (if not removed already)
for (AbstractController controller : controllers) {
if (controller.getControllerId() == controllerId) {
controllers.remove(controller);
break;
}
}
// Call through to the client's listener
if (listener != null) {
listener.deviceRemoved(controllerId);
}
}
@Override
public void deviceAdded(int controllerId) {
// Call through to the client's listener
if (listener != null) {
listener.deviceAdded(controllerId);
}
}
public class UsbEventReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
// Initial attachment broadcast
if (action.equals(UsbManager.ACTION_USB_DEVICE_ATTACHED)) {
final UsbDevice device = intent.getParcelableExtra(UsbManager.EXTRA_DEVICE);
// shouldClaimDevice() looks at the kernel's enumerated input
// devices to make its decision about whether to prompt to take
// control of the device. The kernel bringing up the input stack
// may race with this callback and cause us to prompt when the
// kernel is capable of running the device. Let's post a delayed
// message to process this state change to allow the kernel
// some time to bring up the stack.
new Handler().postDelayed(new Runnable() {
@Override
public void run() {
// Continue the state machine
handleUsbDeviceState(device);
}
}, 1000);
}
// Subsequent permission dialog completion intent
else if (action.equals(ACTION_USB_PERMISSION)) {
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)) {
handleUsbDeviceState(device);
}
}
}
}
public class UsbDriverBinder extends Binder {
public void setListener(UsbDriverListener listener) {
UsbDriverService.this.listener = listener;
// Report all controllerMap that already exist
if (listener != null) {
for (AbstractController controller : controllers) {
listener.deviceAdded(controller.getControllerId());
}
}
}
}
private void handleUsbDeviceState(UsbDevice device) {
// Are we able to operate it?
if (shouldClaimDevice(device, prefConfig.bindAllUsb)) {
// Do we have permission yet?
if (!usbManager.hasPermission(device)) {
// Let's ask for permission
try {
// This function is not documented as throwing any exceptions (denying access
// is indicated by calling the PendingIntent with a false result). However,
// Samsung Knox has some policies which block this request, but rather than
// just returning a false result or returning 0 enumerated devices,
// they throw an undocumented SecurityException from this call, crashing
// the whole app. :(
usbManager.requestPermission(device, PendingIntent.getBroadcast(UsbDriverService.this, 0, new Intent(ACTION_USB_PERMISSION), 0));
} catch (SecurityException e) {
Toast.makeText(this, this.getText(R.string.error_usb_prohibited), Toast.LENGTH_LONG).show();
}
return;
}
// Open the device
UsbDeviceConnection connection = usbManager.openDevice(device);
if (connection == null) {
LimeLog.warning("Unable to open USB device: "+device.getDeviceName());
return;
}
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;
}
// Add this controller to the list
controllers.add(controller);
}
}
private static 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 == null) {
// Device was removed while looping
continue;
}
if (inputDev.getVendorId() == device.getVendorId() &&
inputDev.getProductId() == device.getProductId()) {
return true;
}
}
return false;
}
else {
return true;
}
}
public static boolean shouldClaimDevice(UsbDevice device, boolean claimAllAvailable) {
// 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) || claimAllAvailable) && Xbox360Controller.canClaimDevice(device));
}
@Override
public void onCreate() {
this.usbManager = (UsbManager) getSystemService(Context.USB_SERVICE);
this.prefConfig = PreferenceConfiguration.readPreferences(this);
// Register for USB attach broadcasts and permission completions
IntentFilter filter = new IntentFilter();
filter.addAction(UsbManager.ACTION_USB_DEVICE_ATTACHED);
filter.addAction(ACTION_USB_PERMISSION);
registerReceiver(receiver, filter);
// Enumerate existing devices
for (UsbDevice dev : usbManager.getDeviceList().values()) {
if (shouldClaimDevice(dev, prefConfig.bindAllUsb)) {
// Start the process of claiming this device
handleUsbDeviceState(dev);
}
}
}
@Override
public void onDestroy() {
// Stop the attachment receiver
unregisterReceiver(receiver);
// Remove listeners
listener = null;
// Stop all controllers
while (controllers.size() > 0) {
// Stop and remove the controller
controllers.remove(0).stop();
}
}
@Override
public IBinder onBind(Intent intent) {
return binder;
}
}

View File

@@ -0,0 +1,140 @@
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
0x056e, // Elecom
0x06a3, // Saitek
0x0738, // Mad Catz
0x07ff, // Mad Catz
0x0e6f, // Unknown
0x0f0d, // Hori
0x11c9, // Nacon
0x12ab, // Unknown
0x1430, // RedOctane
0x146b, // BigBen
0x1532, // Razer Sabertooth
0x15e4, // Numark
0x162e, // Joytech
0x1689, // Razer Onza
0x1bad, // Harmonix
0x24c6, // PowerA
};
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;
}
}

View File

@@ -0,0 +1,123 @@
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 XboxOneController extends AbstractXboxController {
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
0x1532, // Razer Wildcat
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) {
super(device, connection, deviceId, listener);
}
private void processButtons(ByteBuffer buffer) {
byte b = buffer.get();
setButtonFlag(ControllerPacket.PLAY_FLAG, b & 0x04);
setButtonFlag(ControllerPacket.BACK_FLAG, b & 0x08);
setButtonFlag(ControllerPacket.A_FLAG, b & 0x10);
setButtonFlag(ControllerPacket.B_FLAG, b & 0x20);
setButtonFlag(ControllerPacket.X_FLAG, b & 0x40);
setButtonFlag(ControllerPacket.Y_FLAG, b & 0x80);
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);
setButtonFlag(ControllerPacket.LB_FLAG, b & 0x10);
setButtonFlag(ControllerPacket.RB_FLAG, b & 0x20);
setButtonFlag(ControllerPacket.LS_CLK_FLAG, b & 0x40);
setButtonFlag(ControllerPacket.RS_CLK_FLAG, b & 0x80);
leftTrigger = buffer.getShort() / 1023.0f;
rightTrigger = buffer.getShort() / 1023.0f;
leftStickX = buffer.getShort() / 32767.0f;
leftStickY = ~buffer.getShort() / 32767.0f;
rightStickX = buffer.getShort() / 32767.0f;
rightStickY = ~buffer.getShort() / 32767.0f;
}
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);
return true;
case 0x07:
// 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);
return true;
}
return false;
}
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;
}
}
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) {
LimeLog.warning("Initialization transfer failed: "+res);
return false;
}
return true;
}
}

View File

@@ -0,0 +1,24 @@
package com.limelight.binding.input.evdev;
import android.app.Activity;
import com.limelight.LimelightBuildProps;
import com.limelight.binding.input.capture.InputCaptureProvider;
public class EvdevCaptureProviderShim {
public static boolean isCaptureProviderSupported() {
return LimelightBuildProps.ROOT_BUILD;
}
// We need to construct our capture provider using reflection because it isn't included in non-root builds
public static InputCaptureProvider createEvdevCaptureProvider(Activity activity, EvdevListener listener) {
try {
Class providerClass = Class.forName("com.limelight.binding.input.evdev.EvdevCaptureProvider");
return (InputCaptureProvider) providerClass.getConstructors()[0].newInstance(activity, listener);
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException(e);
}
}
}

View File

@@ -1,41 +0,0 @@
package com.limelight.binding.input.evdev;
public class EvdevEvent {
public static final int EVDEV_MIN_EVENT_SIZE = 16;
public static final int EVDEV_MAX_EVENT_SIZE = 24;
/* Event types */
public static final short EV_SYN = 0x00;
public static final short EV_KEY = 0x01;
public static final short EV_REL = 0x02;
public static final short EV_MSC = 0x04;
/* Relative axes */
public static final short REL_X = 0x00;
public static final short REL_Y = 0x01;
public static final short REL_WHEEL = 0x08;
/* Buttons */
public static final short BTN_LEFT = 0x110;
public static final short BTN_RIGHT = 0x111;
public static final short BTN_MIDDLE = 0x112;
public static final short BTN_SIDE = 0x113;
public static final short BTN_EXTRA = 0x114;
public static final short BTN_FORWARD = 0x115;
public static final short BTN_BACK = 0x116;
public static final short BTN_TASK = 0x117;
public static final short BTN_GAMEPAD = 0x130;
/* Keys */
public static final short KEY_Q = 16;
public short type;
public short code;
public int value;
public EvdevEvent(short type, short code, int value) {
this.type = type;
this.code = code;
this.value = value;
}
}

View File

@@ -1,167 +0,0 @@
package com.limelight.binding.input.evdev;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import com.limelight.LimeLog;
public class EvdevHandler {
private String absolutePath;
private EvdevListener listener;
private boolean shutdown = false;
private int fd = -1;
private Thread handlerThread = new Thread() {
@Override
public void run() {
// All the finally blocks here make this code look like a mess
// but it's important that we get this right to avoid causing
// system-wide input problems.
// Open the /dev/input/eventX file
fd = EvdevReader.open(absolutePath);
if (fd == -1) {
LimeLog.warning("Unable to open "+absolutePath);
return;
}
try {
// Check if it's a mouse or keyboard, but not a gamepad
if ((!EvdevReader.isMouse(fd) && !EvdevReader.isAlphaKeyboard(fd)) ||
EvdevReader.isGamepad(fd)) {
// We only handle keyboards and mice
return;
}
// Grab it for ourselves
if (!EvdevReader.grab(fd)) {
LimeLog.warning("Unable to grab "+absolutePath);
return;
}
LimeLog.info("Grabbed device for raw keyboard/mouse input: "+absolutePath);
ByteBuffer buffer = ByteBuffer.allocate(EvdevEvent.EVDEV_MAX_EVENT_SIZE).order(ByteOrder.nativeOrder());
try {
int deltaX = 0;
int deltaY = 0;
byte deltaScroll = 0;
while (!isInterrupted() && !shutdown) {
EvdevEvent event = EvdevReader.read(fd, buffer);
if (event == null) {
return;
}
switch (event.type)
{
case EvdevEvent.EV_SYN:
if (deltaX != 0 || deltaY != 0) {
listener.mouseMove(deltaX, deltaY);
deltaX = deltaY = 0;
}
if (deltaScroll != 0) {
listener.mouseScroll(deltaScroll);
deltaScroll = 0;
}
break;
case EvdevEvent.EV_REL:
switch (event.code)
{
case EvdevEvent.REL_X:
deltaX = event.value;
break;
case EvdevEvent.REL_Y:
deltaY = event.value;
break;
case EvdevEvent.REL_WHEEL:
deltaScroll = (byte) event.value;
break;
}
break;
case EvdevEvent.EV_KEY:
switch (event.code)
{
case EvdevEvent.BTN_LEFT:
listener.mouseButtonEvent(EvdevListener.BUTTON_LEFT,
event.value != 0);
break;
case EvdevEvent.BTN_MIDDLE:
listener.mouseButtonEvent(EvdevListener.BUTTON_MIDDLE,
event.value != 0);
break;
case EvdevEvent.BTN_RIGHT:
listener.mouseButtonEvent(EvdevListener.BUTTON_RIGHT,
event.value != 0);
break;
case EvdevEvent.BTN_SIDE:
case EvdevEvent.BTN_EXTRA:
case EvdevEvent.BTN_FORWARD:
case EvdevEvent.BTN_BACK:
case EvdevEvent.BTN_TASK:
// Other unhandled mouse buttons
break;
default:
// We got some unrecognized button. This means
// someone is trying to use the other device in this
// "combination" input device. We'll try to handle
// it via keyboard, but we're not going to disconnect
// if we can't
short keyCode = EvdevTranslator.translateEvdevKeyCode(event.code);
if (keyCode != 0) {
listener.keyboardEvent(event.value != 0, keyCode);
}
break;
}
break;
case EvdevEvent.EV_MSC:
break;
}
}
} finally {
// Release our grab
EvdevReader.ungrab(fd);
}
} finally {
// Close the file
EvdevReader.close(fd);
}
}
};
public EvdevHandler(String absolutePath, EvdevListener listener) {
this.absolutePath = absolutePath;
this.listener = listener;
}
public void start() {
handlerThread.start();
}
public void stop() {
// Close the fd. It doesn't matter if this races
// with the handler thread. We'll close this out from
// under the thread to wake it up
if (fd != -1) {
EvdevReader.close(fd);
}
shutdown = true;
handlerThread.interrupt();
try {
handlerThread.join();
} catch (InterruptedException ignored) {}
}
public void notifyDeleted() {
stop();
}
}

View File

@@ -1,12 +1,14 @@
package com.limelight.binding.input.evdev;
public interface EvdevListener {
public static final int BUTTON_LEFT = 1;
public static final int BUTTON_MIDDLE = 2;
public static final int BUTTON_RIGHT = 3;
public void mouseMove(int deltaX, int deltaY);
public void mouseButtonEvent(int buttonId, boolean down);
public void mouseScroll(byte amount);
public void keyboardEvent(boolean buttonDown, short keyCode);
int BUTTON_LEFT = 1;
int BUTTON_MIDDLE = 2;
int BUTTON_RIGHT = 3;
int BUTTON_X1 = 4;
int BUTTON_X2 = 5;
void mouseMove(int deltaX, int deltaY);
void mouseButtonEvent(int buttonId, boolean down);
void mouseScroll(byte amount);
void keyboardEvent(boolean buttonDown, short keyCode);
}

View File

@@ -1,103 +0,0 @@
package com.limelight.binding.input.evdev;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.ByteBuffer;
import java.util.Locale;
import com.limelight.LimeLog;
public class EvdevReader {
static {
System.loadLibrary("evdev_reader");
}
// Requires root to chmod /dev/input/eventX
public static boolean setPermissions(String[] files, int octalPermissions) {
ProcessBuilder builder = new ProcessBuilder("su");
try {
Process p = builder.start();
OutputStream stdin = p.getOutputStream();
for (String file : files) {
stdin.write(String.format((Locale)null, "chmod %o %s\n", octalPermissions, file).getBytes("UTF-8"));
}
stdin.write("exit\n".getBytes("UTF-8"));
stdin.flush();
p.waitFor();
p.destroy();
return true;
} catch (IOException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
}
return false;
}
// Returns the fd to be passed to other function or -1 on error
public static native int open(String fileName);
// Prevent other apps (including Android itself) from using the device while "grabbed"
public static native boolean grab(int fd);
public static native boolean ungrab(int fd);
// Used for checking device capabilities
public static native boolean hasRelAxis(int fd, short axis);
public static native boolean hasAbsAxis(int fd, short axis);
public static native boolean hasKey(int fd, short key);
public static boolean isMouse(int fd) {
// This is the same check that Android does in EventHub.cpp
return hasRelAxis(fd, EvdevEvent.REL_X) &&
hasRelAxis(fd, EvdevEvent.REL_Y) &&
hasKey(fd, EvdevEvent.BTN_LEFT);
}
public static boolean isAlphaKeyboard(int fd) {
// This is the same check that Android does in EventHub.cpp
return hasKey(fd, EvdevEvent.KEY_Q);
}
public static boolean isGamepad(int fd) {
return hasKey(fd, EvdevEvent.BTN_GAMEPAD);
}
// Returns the bytes read or -1 on error
private static native int read(int fd, byte[] buffer);
// Takes a byte buffer to use to read the output into.
// This buffer MUST be in native byte order and at least
// EVDEV_MAX_EVENT_SIZE bytes long.
public static EvdevEvent read(int fd, ByteBuffer buffer) {
int bytesRead = read(fd, buffer.array());
if (bytesRead < 0) {
LimeLog.warning("Failed to read: "+bytesRead);
return null;
}
else if (bytesRead < EvdevEvent.EVDEV_MIN_EVENT_SIZE) {
LimeLog.warning("Short read: "+bytesRead);
return null;
}
buffer.limit(bytesRead);
buffer.rewind();
// Throw away the time stamp
if (bytesRead == EvdevEvent.EVDEV_MAX_EVENT_SIZE) {
buffer.getLong();
buffer.getLong();
} else {
buffer.getInt();
buffer.getInt();
}
return new EvdevEvent(buffer.getShort(), buffer.getShort(), buffer.getInt());
}
// Closes the fd from open()
public static native int close(int fd);
}

View File

@@ -1,139 +0,0 @@
package com.limelight.binding.input.evdev;
import android.view.KeyEvent;
public class EvdevTranslator {
public static final short EVDEV_KEY_CODES[] = {
0, //KeyEvent.VK_RESERVED
KeyEvent.KEYCODE_ESCAPE,
KeyEvent.KEYCODE_1,
KeyEvent.KEYCODE_2,
KeyEvent.KEYCODE_3,
KeyEvent.KEYCODE_4,
KeyEvent.KEYCODE_5,
KeyEvent.KEYCODE_6,
KeyEvent.KEYCODE_7,
KeyEvent.KEYCODE_8,
KeyEvent.KEYCODE_9,
KeyEvent.KEYCODE_0,
KeyEvent.KEYCODE_MINUS,
KeyEvent.KEYCODE_EQUALS,
KeyEvent.KEYCODE_DEL,
KeyEvent.KEYCODE_TAB,
KeyEvent.KEYCODE_Q,
KeyEvent.KEYCODE_W,
KeyEvent.KEYCODE_E,
KeyEvent.KEYCODE_R,
KeyEvent.KEYCODE_T,
KeyEvent.KEYCODE_Y,
KeyEvent.KEYCODE_U,
KeyEvent.KEYCODE_I,
KeyEvent.KEYCODE_O,
KeyEvent.KEYCODE_P,
KeyEvent.KEYCODE_LEFT_BRACKET,
KeyEvent.KEYCODE_RIGHT_BRACKET,
KeyEvent.KEYCODE_ENTER,
KeyEvent.KEYCODE_CTRL_LEFT,
KeyEvent.KEYCODE_A,
KeyEvent.KEYCODE_S,
KeyEvent.KEYCODE_D,
KeyEvent.KEYCODE_F,
KeyEvent.KEYCODE_G,
KeyEvent.KEYCODE_H,
KeyEvent.KEYCODE_J,
KeyEvent.KEYCODE_K,
KeyEvent.KEYCODE_L,
KeyEvent.KEYCODE_SEMICOLON,
KeyEvent.KEYCODE_APOSTROPHE,
KeyEvent.KEYCODE_GRAVE,
KeyEvent.KEYCODE_SHIFT_LEFT,
KeyEvent.KEYCODE_BACKSLASH,
KeyEvent.KEYCODE_Z,
KeyEvent.KEYCODE_X,
KeyEvent.KEYCODE_C,
KeyEvent.KEYCODE_V,
KeyEvent.KEYCODE_B,
KeyEvent.KEYCODE_N,
KeyEvent.KEYCODE_M,
KeyEvent.KEYCODE_COMMA,
KeyEvent.KEYCODE_PERIOD,
KeyEvent.KEYCODE_SLASH,
KeyEvent.KEYCODE_SHIFT_RIGHT,
KeyEvent.KEYCODE_NUMPAD_MULTIPLY,
KeyEvent.KEYCODE_ALT_LEFT,
KeyEvent.KEYCODE_SPACE,
KeyEvent.KEYCODE_CAPS_LOCK,
KeyEvent.KEYCODE_F1,
KeyEvent.KEYCODE_F2,
KeyEvent.KEYCODE_F3,
KeyEvent.KEYCODE_F4,
KeyEvent.KEYCODE_F5,
KeyEvent.KEYCODE_F6,
KeyEvent.KEYCODE_F7,
KeyEvent.KEYCODE_F8,
KeyEvent.KEYCODE_F9,
KeyEvent.KEYCODE_F10,
KeyEvent.KEYCODE_NUM_LOCK,
KeyEvent.KEYCODE_SCROLL_LOCK,
KeyEvent.KEYCODE_NUMPAD_7,
KeyEvent.KEYCODE_NUMPAD_8,
KeyEvent.KEYCODE_NUMPAD_9,
KeyEvent.KEYCODE_NUMPAD_SUBTRACT,
KeyEvent.KEYCODE_NUMPAD_4,
KeyEvent.KEYCODE_NUMPAD_5,
KeyEvent.KEYCODE_NUMPAD_6,
KeyEvent.KEYCODE_NUMPAD_ADD,
KeyEvent.KEYCODE_NUMPAD_1,
KeyEvent.KEYCODE_NUMPAD_2,
KeyEvent.KEYCODE_NUMPAD_3,
KeyEvent.KEYCODE_NUMPAD_0,
KeyEvent.KEYCODE_NUMPAD_DOT,
0,
0, //KeyEvent.VK_ZENKAKUHANKAKU,
0, //KeyEvent.VK_102ND,
KeyEvent.KEYCODE_F11,
KeyEvent.KEYCODE_F12,
0, //KeyEvent.VK_RO,
0, //KeyEvent.VK_KATAKANA,
0, //KeyEvent.VK_HIRAGANA,
0, //KeyEvent.VK_HENKAN,
0, //KeyEvent.VK_KATAKANAHIRAGANA,
0, //KeyEvent.VK_MUHENKAN,
0, //KeyEvent.VK_KPJPCOMMA,
KeyEvent.KEYCODE_NUMPAD_ENTER,
KeyEvent.KEYCODE_CTRL_RIGHT,
KeyEvent.KEYCODE_NUMPAD_DIVIDE,
KeyEvent.KEYCODE_SYSRQ,
KeyEvent.KEYCODE_ALT_RIGHT,
0, //KeyEvent.VK_LINEFEED,
KeyEvent.KEYCODE_HOME,
KeyEvent.KEYCODE_DPAD_UP,
KeyEvent.KEYCODE_PAGE_UP,
KeyEvent.KEYCODE_DPAD_LEFT,
KeyEvent.KEYCODE_DPAD_RIGHT,
KeyEvent.KEYCODE_MOVE_END,
KeyEvent.KEYCODE_DPAD_DOWN,
KeyEvent.KEYCODE_PAGE_DOWN,
KeyEvent.KEYCODE_INSERT,
KeyEvent.KEYCODE_FORWARD_DEL,
0, //KeyEvent.VK_MACRO,
0, //KeyEvent.VK_MUTE,
0, //KeyEvent.VK_VOLUMEDOWN,
0, //KeyEvent.VK_VOLUMEUP,
0, //KeyEvent.VK_POWER, /* SC System Power Down */
KeyEvent.KEYCODE_NUMPAD_EQUALS,
0, //KeyEvent.VK_KPPLUSMINUS,
KeyEvent.KEYCODE_BREAK,
0, //KeyEvent.VK_SCALE, /* AL Compiz Scale (Expose) */
};
public static short translateEvdevKeyCode(short evdevKeyCode) {
if (evdevKeyCode < EVDEV_KEY_CODES.length) {
return EVDEV_KEY_CODES[evdevKeyCode];
}
return 0;
}
}

View File

@@ -1,172 +0,0 @@
package com.limelight.binding.input.evdev;
import java.io.File;
import java.util.HashMap;
import java.util.Map;
import com.limelight.LimeLog;
import android.os.FileObserver;
@SuppressWarnings("ALL")
public class EvdevWatcher {
private static final String PATH = "/dev/input";
private static final String REQUIRED_FILE_PREFIX = "event";
private final HashMap<String, EvdevHandler> handlers = new HashMap<String, EvdevHandler>();
private boolean shutdown = false;
private boolean init = false;
private boolean ungrabbed = false;
private EvdevListener listener;
private Thread startThread;
private FileObserver observer = new FileObserver(PATH, FileObserver.CREATE | FileObserver.DELETE) {
@Override
public void onEvent(int event, String fileName) {
if (fileName == null) {
return;
}
if (!fileName.startsWith(REQUIRED_FILE_PREFIX)) {
return;
}
synchronized (handlers) {
if (shutdown) {
return;
}
if ((event & FileObserver.CREATE) != 0) {
LimeLog.info("Starting evdev handler for "+fileName);
if (!init) {
// If this a real new device, update permissions again so we can read it
EvdevReader.setPermissions(new String[]{PATH + "/" + fileName}, 0666);
}
EvdevHandler handler = new EvdevHandler(PATH + "/" + fileName, listener);
// If we're ungrabbed now, don't start the handler
if (!ungrabbed) {
handler.start();
}
handlers.put(fileName, handler);
}
if ((event & FileObserver.DELETE) != 0) {
LimeLog.info("Halting evdev handler for "+fileName);
EvdevHandler handler = handlers.remove(fileName);
if (handler != null) {
handler.notifyDeleted();
}
}
}
}
};
public EvdevWatcher(EvdevListener listener) {
this.listener = listener;
}
private File[] rundownWithPermissionsChange(int newPermissions) {
// Rundown existing files
File devInputDir = new File(PATH);
File[] files = devInputDir.listFiles();
if (files == null) {
return new File[0];
}
// Set desired permissions
String[] filePaths = new String[files.length];
for (int i = 0; i < files.length; i++) {
filePaths[i] = files[i].getAbsolutePath();
}
EvdevReader.setPermissions(filePaths, newPermissions);
return files;
}
public void ungrabAll() {
synchronized (handlers) {
// Note that we're ungrabbed for now
ungrabbed = true;
// Stop all handlers
for (EvdevHandler handler : handlers.values()) {
handler.stop();
}
}
}
public void regrabAll() {
synchronized (handlers) {
// We're regrabbing everything now
ungrabbed = false;
for (Map.Entry<String, EvdevHandler> entry : handlers.entrySet()) {
// We need to recreate each entry since we can't reuse a stopped one
entry.setValue(new EvdevHandler(PATH + "/" + entry.getKey(), listener));
entry.getValue().start();
}
}
}
public void start() {
startThread = new Thread() {
@Override
public void run() {
// List all files and allow us access
File[] files = rundownWithPermissionsChange(0666);
init = true;
for (File f : files) {
observer.onEvent(FileObserver.CREATE, f.getName());
}
// Done with initial onEvent calls
init = false;
// Start watching for new files
observer.startWatching();
synchronized (startThread) {
// Wait to be awoken again by shutdown()
try {
startThread.wait();
} catch (InterruptedException e) {}
}
// Giveup eventX permissions
rundownWithPermissionsChange(066);
}
};
startThread.start();
}
public void shutdown() {
// Let start thread cleanup on it's own sweet time
synchronized (startThread) {
startThread.notify();
}
// Stop the observer
observer.stopWatching();
synchronized (handlers) {
// Stop creating new handlers
shutdown = true;
// If we've already ungrabbed, there's nothing else to do
if (ungrabbed) {
return;
}
// Stop all handlers
for (EvdevHandler handler : handlers.values()) {
handler.stop();
}
}
}
}

View File

@@ -0,0 +1,350 @@
/**
* 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, int elementId) {
super(controller, context, elementId);
// 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, 100);
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;
}
}

View File

@@ -0,0 +1,243 @@
/**
* 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 elementId, int layer, Context context) {
super(controller, context, elementId);
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();
}
// We may be called for a release without a prior click
if (timerLongClick != null) {
timerLongClick.cancel();
}
if (longClickTimerTask != null) {
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;
}
}

View File

@@ -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, EID_DPAD);
}
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);
}
}

View File

@@ -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, EID_LS);
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();
}
});
}
}

View File

@@ -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, EID_LT, 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();
}
});
}
}

View File

@@ -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, EID_RS);
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();
}
});
}
}

View File

@@ -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, EID_RT, 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();
}
});
}
}

View File

@@ -0,0 +1,192 @@
/**
* 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;
import java.util.Timer;
import java.util.TimerTask;
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;
private Timer retransmitTimer;
ControllerMode currentMode = ControllerMode.Active;
ControllerInputContext inputContext = new ControllerInputContext();
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.setAlpha(0.25f);
buttonConfigure.setFocusable(false);
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;
VirtualControllerConfigurationLoader.saveProfile(VirtualController.this, context);
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 hide() {
retransmitTimer.cancel();
relative_layout.setVisibility(View.INVISIBLE);
}
public void show() {
relative_layout.setVisibility(View.VISIBLE);
// HACK: GFE sometimes discards gamepad packets when they are received
// very shortly after another. This can be critical if an axis zeroing packet
// is lost and causes an analog stick to get stuck. To avoid this, we send
// a gamepad input packet every 100 ms to ensure any loss can be recovered.
retransmitTimer = new Timer("OSC timer", true);
retransmitTimer.schedule(new TimerTask() {
@Override
public void run() {
sendControllerInputContext();
}
}, 100, 100);
}
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.06f);
RelativeLayout.LayoutParams params = new RelativeLayout.LayoutParams(buttonSize, buttonSize);
params.addRule(RelativeLayout.ALIGN_PARENT_LEFT, RelativeLayout.TRUE);
params.addRule(RelativeLayout.ALIGN_PARENT_TOP, RelativeLayout.TRUE);
params.leftMargin = 15;
params.topMargin = 15;
relative_layout.addView(buttonConfigure, params);
// Start with the default layout
VirtualControllerConfigurationLoader.createDefaultLayout(this, context);
// Apply user preferences onto the default layout
VirtualControllerConfigurationLoader.loadFromPreferences(this, context);
}
public ControllerMode getControllerMode() {
return currentMode;
}
public ControllerInputContext getControllerInputContext() {
return inputContext;
}
void sendControllerInputContext() {
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);
if (connection != null) {
connection.sendControllerInput(
inputContext.inputMap,
inputContext.leftTrigger,
inputContext.rightTrigger,
inputContext.leftStickX,
inputContext.leftStickY,
inputContext.rightStickX,
inputContext.rightStickY
);
}
} catch (Exception e) {
e.printStackTrace();
}
}
}

View File

@@ -0,0 +1,328 @@
/**
* Created by Karim Mreisi.
*/
package com.limelight.binding.input.virtual_controller;
import android.app.Activity;
import android.content.Context;
import android.content.SharedPreferences;
import android.util.DisplayMetrics;
import com.limelight.nvstream.input.ControllerPacket;
import com.limelight.preferences.PreferenceConfiguration;
import org.json.JSONException;
import org.json.JSONObject;
public class VirtualControllerConfigurationLoader {
public static final String OSC_PREFERENCE = "OSC";
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 elementId,
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, elementId, 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();
PreferenceConfiguration config = PreferenceConfiguration.readPreferences(context);
// 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.
if (!config.onlyL3R3)
{
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(
VirtualControllerElement.EID_A,
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(
VirtualControllerElement.EID_B,
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(
VirtualControllerElement.EID_X,
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(
VirtualControllerElement.EID_Y,
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(
VirtualControllerElement.EID_LB,
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(
VirtualControllerElement.EID_RB,
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(
VirtualControllerElement.EID_BACK,
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(
VirtualControllerElement.EID_START,
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)
);
}
else {
controller.addElement(createDigitalButton(
VirtualControllerElement.EID_LSB,
ControllerPacket.LS_CLK_FLAG, 0, 1, "L3", -1, controller, context),
getPercent(2, screen.widthPixels),
getPercent(80, screen.heightPixels),
getPercent(BUTTON_WIDTH, screen.widthPixels),
getPercent(BUTTON_HEIGHT, screen.heightPixels)
);
controller.addElement(createDigitalButton(
VirtualControllerElement.EID_RSB,
ControllerPacket.RS_CLK_FLAG, 0, 1, "R3", -1, controller, context),
getPercent(89, screen.widthPixels),
getPercent(80, screen.heightPixels),
getPercent(BUTTON_WIDTH, screen.widthPixels),
getPercent(BUTTON_HEIGHT, screen.heightPixels)
);
}
}
public static void saveProfile(final VirtualController controller,
final Context context) {
SharedPreferences.Editor prefEditor = context.getSharedPreferences(OSC_PREFERENCE, Activity.MODE_PRIVATE).edit();
for (VirtualControllerElement element : controller.getElements()) {
String prefKey = ""+element.elementId;
try {
prefEditor.putString(prefKey, element.getConfiguration().toString());
} catch (JSONException e) {
e.printStackTrace();
}
}
prefEditor.apply();
}
public static void loadFromPreferences(final VirtualController controller, final Context context) {
SharedPreferences pref = context.getSharedPreferences(OSC_PREFERENCE, Activity.MODE_PRIVATE);
for (VirtualControllerElement element : controller.getElements()) {
String prefKey = ""+element.elementId;
String jsonConfig = pref.getString(prefKey, null);
if (jsonConfig != null) {
try {
element.loadConfiguration(new JSONObject(jsonConfig));
} catch (JSONException e) {
e.printStackTrace();
// Remove the corrupt element from the preferences
pref.edit().remove(prefKey).apply();
}
}
}
}
}

View File

@@ -0,0 +1,326 @@
/**
* 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;
import org.json.JSONException;
import org.json.JSONObject;
public abstract class VirtualControllerElement extends View {
protected static boolean _PRINT_DEBUG_INFORMATION = false;
public static final int EID_DPAD = 1;
public static final int EID_LT = 2;
public static final int EID_RT = 3;
public static final int EID_LB = 4;
public static final int EID_RB = 5;
public static final int EID_A = 6;
public static final int EID_B = 7;
public static final int EID_X = 8;
public static final int EID_Y = 9;
public static final int EID_BACK = 10;
public static final int EID_START = 11;
public static final int EID_LS = 12;
public static final int EID_RS = 13;
public static final int EID_LSB = 14;
public static final int EID_RSB = 15;
protected VirtualController virtualController;
protected final int elementId;
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, int elementId) {
super(context);
this.virtualController = controller;
this.elementId = elementId;
}
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() throws JSONException {
JSONObject configuration = new JSONObject();
RelativeLayout.LayoutParams layoutParams = (RelativeLayout.LayoutParams) getLayoutParams();
configuration.put("LEFT", layoutParams.leftMargin);
configuration.put("TOP", layoutParams.topMargin);
configuration.put("WIDTH", layoutParams.width);
configuration.put("HEIGHT", layoutParams.height);
return configuration;
}
public void loadConfiguration(JSONObject configuration) throws JSONException {
RelativeLayout.LayoutParams layoutParams = (RelativeLayout.LayoutParams) getLayoutParams();
layoutParams.leftMargin = configuration.getInt("LEFT");
layoutParams.topMargin = configuration.getInt("TOP");
layoutParams.width = configuration.getInt("WIDTH");
layoutParams.height = configuration.getInt("HEIGHT");
requestLayout();
}
}

View File

@@ -1,263 +0,0 @@
package com.limelight.binding.video;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.concurrent.locks.LockSupport;
import android.graphics.PixelFormat;
import android.os.Build;
import android.view.SurfaceHolder;
import com.limelight.LimeLog;
import com.limelight.nvstream.av.ByteBufferDescriptor;
import com.limelight.nvstream.av.DecodeUnit;
import com.limelight.nvstream.av.video.VideoDecoderRenderer;
import com.limelight.nvstream.av.video.VideoDepacketizer;
import com.limelight.nvstream.av.video.cpu.AvcDecoder;
@SuppressWarnings("EmptyCatchBlock")
public class AndroidCpuDecoderRenderer implements VideoDecoderRenderer {
private Thread rendererThread;
private int targetFps;
private static final int DECODER_BUFFER_SIZE = 92*1024;
private ByteBuffer decoderBuffer;
// Only sleep if the difference is above this value
private static final int WAIT_CEILING_MS = 8;
private static final int LOW_PERF = 1;
private static final int MED_PERF = 2;
private static final int HIGH_PERF = 3;
private int totalFrames;
private long totalTimeMs;
private int cpuCount = Runtime.getRuntime().availableProcessors();
@SuppressWarnings("unused")
private int findOptimalPerformanceLevel() {
StringBuilder cpuInfo = new StringBuilder();
BufferedReader br = null;
try {
br = new BufferedReader(new FileReader(new File("/proc/cpuinfo")));
for (;;) {
int ch = br.read();
if (ch == -1)
break;
cpuInfo.append((char)ch);
}
// Here we're doing very simple heuristics based on CPU model
String cpuInfoStr = cpuInfo.toString();
// We order them from greatest to least for proper detection
// of devices with multiple sets of cores (like Exynos 5 Octa)
// TODO Make this better (only even kind of works on ARM)
if (Build.FINGERPRINT.contains("generic")) {
// Emulator
return LOW_PERF;
}
else if (cpuInfoStr.contains("0xc0f")) {
// Cortex-A15
return MED_PERF;
}
else if (cpuInfoStr.contains("0xc09")) {
// Cortex-A9
return LOW_PERF;
}
else if (cpuInfoStr.contains("0xc07")) {
// Cortex-A7
return LOW_PERF;
}
else {
// Didn't have anything we're looking for
return MED_PERF;
}
} catch (IOException e) {
} finally {
if (br != null) {
try {
br.close();
} catch (IOException e) {}
}
}
// Couldn't read cpuinfo, so assume medium
return MED_PERF;
}
@Override
public boolean setup(int width, int height, int redrawRate, Object renderTarget, int drFlags) {
this.targetFps = redrawRate;
int perfLevel = LOW_PERF; //findOptimalPerformanceLevel();
int threadCount;
int avcFlags = 0;
switch (perfLevel) {
case HIGH_PERF:
// Single threaded low latency decode is ideal but hard to acheive
avcFlags = AvcDecoder.LOW_LATENCY_DECODE;
threadCount = 1;
break;
case LOW_PERF:
// Disable the loop filter for performance reasons
avcFlags = AvcDecoder.DISABLE_LOOP_FILTER |
AvcDecoder.FAST_BILINEAR_FILTERING |
AvcDecoder.FAST_DECODE;
// Use plenty of threads to try to utilize the CPU as best we can
threadCount = cpuCount - 1;
break;
default:
case MED_PERF:
avcFlags = AvcDecoder.BILINEAR_FILTERING |
AvcDecoder.FAST_DECODE;
// Only use 2 threads to minimize frame processing latency
threadCount = 2;
break;
}
// If the user wants quality, we'll remove the low IQ flags
if ((drFlags & VideoDecoderRenderer.FLAG_PREFER_QUALITY) != 0) {
// Make sure the loop filter is enabled
avcFlags &= ~AvcDecoder.DISABLE_LOOP_FILTER;
// Disable the non-compliant speed optimizations
avcFlags &= ~AvcDecoder.FAST_DECODE;
LimeLog.info("Using high quality decoding");
}
SurfaceHolder sh = (SurfaceHolder)renderTarget;
sh.setFormat(PixelFormat.RGBX_8888);
int err = AvcDecoder.init(width, height, avcFlags, threadCount);
if (err != 0) {
throw new IllegalStateException("AVC decoder initialization failure: "+err);
}
AvcDecoder.setRenderTarget(sh.getSurface());
decoderBuffer = ByteBuffer.allocate(DECODER_BUFFER_SIZE + AvcDecoder.getInputPaddingSize());
LimeLog.info("Using software decoding (performance level: "+perfLevel+")");
return true;
}
@Override
public boolean start(final VideoDepacketizer depacketizer) {
rendererThread = new Thread() {
@Override
public void run() {
long nextFrameTime = System.currentTimeMillis();
DecodeUnit du;
while (!isInterrupted())
{
du = depacketizer.pollNextDecodeUnit();
if (du != null) {
submitDecodeUnit(du);
depacketizer.freeDecodeUnit(du);
}
long diff = nextFrameTime - System.currentTimeMillis();
if (diff > WAIT_CEILING_MS) {
LockSupport.parkNanos(1);
continue;
}
nextFrameTime = computePresentationTimeMs(targetFps);
AvcDecoder.redraw();
}
}
};
rendererThread.setName("Video - Renderer (CPU)");
rendererThread.setPriority(Thread.MAX_PRIORITY);
rendererThread.start();
return true;
}
private long computePresentationTimeMs(int frameRate) {
return System.currentTimeMillis() + (1000 / frameRate);
}
@Override
public void stop() {
rendererThread.interrupt();
try {
rendererThread.join();
} catch (InterruptedException e) { }
}
@Override
public void release() {
AvcDecoder.destroy();
}
private boolean submitDecodeUnit(DecodeUnit decodeUnit) {
byte[] data;
// Use the reserved decoder buffer if this decode unit will fit
if (decodeUnit.getDataLength() <= DECODER_BUFFER_SIZE) {
decoderBuffer.clear();
for (ByteBufferDescriptor bbd : decodeUnit.getBufferList()) {
decoderBuffer.put(bbd.data, bbd.offset, bbd.length);
}
data = decoderBuffer.array();
}
else {
data = new byte[decodeUnit.getDataLength()+AvcDecoder.getInputPaddingSize()];
int offset = 0;
for (ByteBufferDescriptor bbd : decodeUnit.getBufferList()) {
System.arraycopy(bbd.data, bbd.offset, data, offset, bbd.length);
offset += bbd.length;
}
}
boolean success = (AvcDecoder.decode(data, 0, decodeUnit.getDataLength()) == 0);
if (success) {
long timeAfterDecode = System.currentTimeMillis();
// Add delta time to the totals (excluding probable outliers)
long delta = timeAfterDecode - decodeUnit.getReceiveTimestamp();
if (delta >= 0 && delta < 300) {
totalTimeMs += delta;
totalFrames++;
}
}
return success;
}
@Override
public int getCapabilities() {
return 0;
}
@Override
public int getAverageDecoderLatency() {
return 0;
}
@Override
public int getAverageEndToEndLatency() {
if (totalFrames == 0) {
return 0;
}
return (int)(totalTimeMs / totalFrames);
}
}

View File

@@ -1,77 +0,0 @@
package com.limelight.binding.video;
import com.limelight.nvstream.av.video.VideoDecoderRenderer;
import com.limelight.nvstream.av.video.VideoDepacketizer;
public class ConfigurableDecoderRenderer implements VideoDecoderRenderer {
private VideoDecoderRenderer decoderRenderer;
@Override
public void release() {
if (decoderRenderer != null) {
decoderRenderer.release();
}
}
@Override
public boolean setup(int width, int height, int redrawRate, Object renderTarget, int drFlags) {
if (decoderRenderer == null) {
throw new IllegalStateException("ConfigurableDecoderRenderer not initialized");
}
return decoderRenderer.setup(width, height, redrawRate, renderTarget, drFlags);
}
public void initializeWithFlags(int drFlags) {
if ((drFlags & VideoDecoderRenderer.FLAG_FORCE_HARDWARE_DECODING) != 0 ||
((drFlags & VideoDecoderRenderer.FLAG_FORCE_SOFTWARE_DECODING) == 0 &&
MediaCodecHelper.findProbableSafeDecoder() != null)) {
decoderRenderer = new MediaCodecDecoderRenderer();
}
else {
decoderRenderer = new AndroidCpuDecoderRenderer();
}
}
public boolean isHardwareAccelerated() {
if (decoderRenderer == null) {
throw new IllegalStateException("ConfigurableDecoderRenderer not initialized");
}
return (decoderRenderer instanceof MediaCodecDecoderRenderer);
}
@Override
public boolean start(VideoDepacketizer depacketizer) {
return decoderRenderer.start(depacketizer);
}
@Override
public void stop() {
decoderRenderer.stop();
}
@Override
public int getCapabilities() {
return decoderRenderer.getCapabilities();
}
@Override
public int getAverageDecoderLatency() {
if (decoderRenderer != null) {
return decoderRenderer.getAverageDecoderLatency();
}
else {
return 0;
}
}
@Override
public int getAverageEndToEndLatency() {
if (decoderRenderer != null) {
return decoderRenderer.getAverageEndToEndLatency();
}
else {
return 0;
}
}
}

View File

@@ -0,0 +1,5 @@
package com.limelight.binding.video;
public interface CrashListener {
void notifyCrash(Exception e);
}

View File

@@ -7,9 +7,13 @@ import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import android.annotation.SuppressLint;
import android.annotation.TargetApi;
import android.app.ActivityManager;
import android.content.Context;
import android.content.pm.ConfigurationInfo;
import android.media.MediaCodecInfo;
import android.media.MediaCodecList;
import android.media.MediaCodecInfo.CodecCapabilities;
@@ -20,38 +24,267 @@ import com.limelight.LimeLog;
public class MediaCodecHelper {
public static final List<String> preferredDecoders;
private static final List<String> preferredDecoders;
private static final List<String> blacklistedDecoderPrefixes;
private static final List<String> spsFixupBitstreamFixupDecoderPrefixes;
private static final List<String> blacklistedAdaptivePlaybackPrefixes;
private static final List<String> deprioritizedHevcDecoders;
private static final List<String> baselineProfileHackPrefixes;
private static final List<String> directSubmitPrefixes;
private static final List<String> constrainedHighProfilePrefixes;
private static final List<String> whitelistedHevcDecoders;
private static final List<String> refFrameInvalidationAvcPrefixes;
private static final List<String> refFrameInvalidationHevcPrefixes;
private static final List<String> blacklisted49FpsDecoderPrefixes;
private static boolean isLowEndSnapdragon = false;
private static boolean initialized = false;
static {
directSubmitPrefixes = new LinkedList<>();
// These decoders have low enough input buffer latency that they
// can be directly invoked from the receive thread
directSubmitPrefixes.add("omx.qcom");
directSubmitPrefixes.add("omx.sec");
directSubmitPrefixes.add("omx.exynos");
directSubmitPrefixes.add("omx.intel");
directSubmitPrefixes.add("omx.brcm");
directSubmitPrefixes.add("omx.TI");
directSubmitPrefixes.add("omx.arc");
directSubmitPrefixes.add("omx.nvidia");
}
static {
refFrameInvalidationAvcPrefixes = new LinkedList<>();
refFrameInvalidationHevcPrefixes = new LinkedList<>();
// Qualcomm and NVIDIA may be added at runtime
}
public static final List<String> blacklistedDecoderPrefixes;
public static final List<String> spsFixupBitstreamFixupDecoderPrefixes;
public static final List<String> whitelistedAdaptiveResolutionPrefixes;
static {
preferredDecoders = new LinkedList<String>();
preferredDecoders = new LinkedList<>();
}
static {
blacklistedDecoderPrefixes = new LinkedList<String>();
// Software decoders that don't support H264 high profile
blacklistedDecoderPrefixes.add("omx.google");
blacklistedDecoderPrefixes.add("AVCDecoder");
blacklistedDecoderPrefixes = new LinkedList<>();
// Blacklist software decoders that don't support H264 high profile,
// but exclude the official AOSP and CrOS emulator from this restriction.
if (!Build.HARDWARE.equals("ranchu") && !Build.HARDWARE.equals("cheets")) {
blacklistedDecoderPrefixes.add("omx.google");
blacklistedDecoderPrefixes.add("AVCDecoder");
}
// Never use ffmpeg decoders since they're software decoders
blacklistedDecoderPrefixes.add("OMX.ffmpeg");
// Force these decoders disabled because:
// 1) They are software decoders, so the performance is terrible
// 2) They crash with our HEVC stream anyway (at least prior to CSD batching)
blacklistedDecoderPrefixes.add("OMX.qcom.video.decoder.hevcswvdec");
blacklistedDecoderPrefixes.add("OMX.SEC.hevc.sw.dec");
}
static {
spsFixupBitstreamFixupDecoderPrefixes = new LinkedList<String>();
// If a decoder qualifies for reference frame invalidation,
// these entries will be ignored for those decoders.
spsFixupBitstreamFixupDecoderPrefixes = new LinkedList<>();
spsFixupBitstreamFixupDecoderPrefixes.add("omx.nvidia");
spsFixupBitstreamFixupDecoderPrefixes.add("omx.qcom");
spsFixupBitstreamFixupDecoderPrefixes.add("omx.mtk");
whitelistedAdaptiveResolutionPrefixes = new LinkedList<String>();
whitelistedAdaptiveResolutionPrefixes.add("omx.nvidia");
whitelistedAdaptiveResolutionPrefixes.add("omx.qcom");
whitelistedAdaptiveResolutionPrefixes.add("omx.sec");
whitelistedAdaptiveResolutionPrefixes.add("omx.TI");
spsFixupBitstreamFixupDecoderPrefixes.add("omx.brcm");
baselineProfileHackPrefixes = new LinkedList<>();
baselineProfileHackPrefixes.add("omx.intel");
blacklistedAdaptivePlaybackPrefixes = new LinkedList<>();
// The Intel decoder on Lollipop on Nexus Player would increase latency badly
// if adaptive playback was enabled so let's avoid it to be safe.
blacklistedAdaptivePlaybackPrefixes.add("omx.intel");
// The MediaTek decoder crashes at 1080p when adaptive playback is enabled
// on some Android TV devices with H.265 only.
blacklistedAdaptivePlaybackPrefixes.add("omx.mtk");
constrainedHighProfilePrefixes = new LinkedList<>();
constrainedHighProfilePrefixes.add("omx.intel");
}
static {
whitelistedHevcDecoders = new LinkedList<>();
// Allow software HEVC decoding in the official AOSP emulator
if (Build.HARDWARE.equals("ranchu") && Build.BRAND.equals("google")) {
whitelistedHevcDecoders.add("omx.google");
}
// Exynos seems to be the only HEVC decoder that works reliably
whitelistedHevcDecoders.add("omx.exynos");
// On Darcy (Shield 2017), HEVC runs fine with no fixups required.
// For some reason, other X1 implementations require bitstream fixups.
if (Build.DEVICE.equalsIgnoreCase("darcy")) {
whitelistedHevcDecoders.add("omx.nvidia");
}
else {
// TODO: This needs a similar fixup to the Tegra 3 otherwise it buffers 16 frames
}
// Sony ATVs have broken MediaTek codecs (decoder hangs after rendering the first frame).
// I know the Fire TV 2 and 3 works, so I'll just whitelist Amazon devices which seem
// to actually be tested.
if (Build.MANUFACTURER.equalsIgnoreCase("Amazon")) {
whitelistedHevcDecoders.add("omx.mtk");
whitelistedHevcDecoders.add("omx.amlogic");
}
// 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.
}
static {
deprioritizedHevcDecoders = new LinkedList<>();
// These are decoders that work but aren't used by default for various reasons.
// Qualcomm is currently the only decoders in this group.
}
static {
blacklisted49FpsDecoderPrefixes = new LinkedList<>();
// We see a bunch of crashes on MediaTek Android TVs running
// at 49 FPS (PAL 50 Hz - 1). Blacklist this frame rate for
// these devices and hope they fix it in Pie.
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) {
blacklisted49FpsDecoderPrefixes.add("omx.mtk");
}
}
private static boolean isPowerVR(String glRenderer) {
return glRenderer.toLowerCase().contains("powervr");
}
private static String getAdrenoVersionString(String glRenderer) {
glRenderer = glRenderer.toLowerCase().trim();
if (!glRenderer.contains("adreno")) {
return null;
}
Pattern modelNumberPattern = Pattern.compile("(.*)([0-9]{3})(.*)");
Matcher matcher = modelNumberPattern.matcher(glRenderer);
if (!matcher.matches()) {
return null;
}
String modelNumber = matcher.group(2);
LimeLog.info("Found Adreno GPU: "+modelNumber);
return modelNumber;
}
private static boolean isLowEndSnapdragonRenderer(String glRenderer) {
String modelNumber = getAdrenoVersionString(glRenderer);
if (modelNumber == null) {
// Not an Adreno GPU
return false;
}
// The current logic is to identify low-end SoCs based on a zero in the x0x place.
return modelNumber.charAt(1) == '0';
}
// This is a workaround for some broken devices that report
// only GLES 3.0 even though the GPU is an Adreno 4xx series part.
// An example of such a device is the Huawei Honor 5x with the
// Snapdragon 616 SoC (Adreno 405).
private static boolean isGLES31SnapdragonRenderer(String glRenderer) {
String modelNumber = getAdrenoVersionString(glRenderer);
if (modelNumber == null) {
// Not an Adreno GPU
return false;
}
// Snapdragon 4xx and higher support GLES 3.1
return modelNumber.charAt(0) >= '4';
}
public static void initialize(Context context, String glRenderer) {
if (initialized) {
return;
}
ActivityManager activityManager =
(ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
ConfigurationInfo configInfo = activityManager.getDeviceConfigurationInfo();
if (configInfo.reqGlEsVersion != ConfigurationInfo.GL_ES_VERSION_UNDEFINED) {
LimeLog.info("OpenGL ES version: "+configInfo.reqGlEsVersion);
isLowEndSnapdragon = isLowEndSnapdragonRenderer(glRenderer);
// Tegra K1 and later can do reference frame invalidation properly
if (configInfo.reqGlEsVersion >= 0x30000) {
LimeLog.info("Added omx.nvidia to AVC reference frame invalidation support list");
refFrameInvalidationAvcPrefixes.add("omx.nvidia");
LimeLog.info("Added omx.qcom to AVC reference frame invalidation support list");
refFrameInvalidationAvcPrefixes.add("omx.qcom");
// Prior to M, we were tricking the decoder into using baseline profile, which
// won't support RFI properly.
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
LimeLog.info("Added omx.intel to AVC reference frame invalidation support list");
refFrameInvalidationAvcPrefixes.add("omx.intel");
}
}
// Qualcomm's early HEVC decoders break hard on our HEVC stream. The best check to
// tell the good from the bad decoders are the generation of Adreno GPU included:
// 3xx - bad
// 4xx - good
//
// The "good" GPUs support GLES 3.1, but we can't just check that directly
// (see comment on isGLES31SnapdragonRenderer).
//
if (isGLES31SnapdragonRenderer(glRenderer)) {
// We prefer reference frame invalidation support (which is only doable on AVC on
// older Qualcomm chips) vs. enabling HEVC by default. The user can override using the settings
// to force HEVC on. If HDR or mobile data will be used, we'll override this and use
// HEVC anyway.
LimeLog.info("Added omx.qcom to deprioritized HEVC decoders based on GLES 3.1+ support");
deprioritizedHevcDecoders.add("omx.qcom");
}
else {
blacklistedDecoderPrefixes.add("OMX.qcom.video.decoder.hevc");
}
// Older MediaTek SoCs have issues with HEVC rendering but the newer chips with
// PowerVR GPUs have good HEVC support.
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && isPowerVR(glRenderer)) {
LimeLog.info("Added omx.mtk to HEVC decoders based on PowerVR GPU");
whitelistedHevcDecoders.add("omx.mtk");
// This SoC (MT8176 in GPD XD+) supports AVC RFI too, but the maxNumReferenceFrames setting
// required to make it work adds a huge amount of latency.
LimeLog.info("Added omx.mtk to RFI list for HEVC");
refFrameInvalidationHevcPrefixes.add("omx.mtk");
}
}
initialized = true;
}
private static boolean isDecoderInList(List<String> decoderList, String decoderName) {
if (!initialized) {
throw new IllegalStateException("MediaCodecHelper must be initialized before use");
}
for (String badPrefix : decoderList) {
if (decoderName.length() >= badPrefix.length()) {
String prefix = decoderName.substring(0, badPrefix.length());
@@ -63,16 +296,19 @@ public class MediaCodecHelper {
return false;
}
@TargetApi(Build.VERSION_CODES.KITKAT)
public static boolean decoderSupportsAdaptivePlayback(String decoderName, MediaCodecInfo decoderInfo) {
if (isDecoderInList(whitelistedAdaptiveResolutionPrefixes, decoderName)) {
LimeLog.info("Adaptive playback supported (whitelist)");
return true;
}
public static long getMonotonicMillis() {
return System.nanoTime() / 1000000L;
}
public static boolean decoderSupportsAdaptivePlayback(MediaCodecInfo decoderInfo) {
// Possibly enable adaptive playback on KitKat and above
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
if (isDecoderInList(blacklistedAdaptivePlaybackPrefixes, decoderInfo.getName())) {
LimeLog.info("Decoder blacklisted for adaptive playback");
return false;
}
try {
if (decoderInfo.getCapabilitiesForType("video/avc").
isFeatureSupported(CodecCapabilities.FEATURE_AdaptivePlayback))
@@ -88,15 +324,94 @@ public class MediaCodecHelper {
return false;
}
public static boolean decoderNeedsConstrainedHighProfile(String decoderName) {
return isDecoderInList(constrainedHighProfilePrefixes, decoderName);
}
public static boolean decoderCanDirectSubmit(String decoderName) {
return isDecoderInList(directSubmitPrefixes, decoderName) && !isExynos4Device();
}
public static boolean decoderNeedsSpsBitstreamRestrictions(String decoderName, MediaCodecInfo decoderInfo) {
public static boolean decoderNeedsSpsBitstreamRestrictions(String decoderName) {
return isDecoderInList(spsFixupBitstreamFixupDecoderPrefixes, decoderName);
}
public static boolean decoderNeedsBaselineSpsHack(String decoderName) {
return isDecoderInList(baselineProfileHackPrefixes, decoderName);
}
public static boolean decoderBlacklistedFor49Fps(String decoderName) {
return isDecoderInList(blacklisted49FpsDecoderPrefixes, decoderName);
}
public static boolean decoderSupportsRefFrameInvalidationAvc(String decoderName, int videoHeight) {
// Reference frame invalidation is broken on low-end Snapdragon SoCs at 1080p.
if (videoHeight > 720 && isLowEndSnapdragon) {
return false;
}
// This device seems to crash constantly at 720p, so try disabling
// RFI to see if we can get that under control.
if (Build.DEVICE.equals("b3") || Build.DEVICE.equals("b5")) {
return false;
}
return isDecoderInList(refFrameInvalidationAvcPrefixes, decoderName);
}
public static boolean decoderSupportsRefFrameInvalidationHevc(String decoderName) {
return isDecoderInList(refFrameInvalidationHevcPrefixes, decoderName);
}
public static boolean decoderIsWhitelistedForHevc(String decoderName, boolean meteredData) {
// TODO: Shield Tablet K1/LTE?
//
// NVIDIA does partial HEVC acceleration on the Shield Tablet. I don't know
// whether the performance is good enough to use for streaming, but they're
// using the same omx.nvidia.h265.decode name as the Shield TV which has a
// fully accelerated HEVC pipeline. AFAIK, the only K1 device with this
// partially accelerated HEVC decoder is the Shield Tablet, so I'll
// check for it here.
//
// TODO: Temporarily disabled with NVIDIA HEVC support
/*if (Build.DEVICE.equalsIgnoreCase("shieldtablet")) {
return false;
}*/
// Google didn't have official support for HEVC (or more importantly, a CTS test) until
// Lollipop. I've seen some MediaTek devices on 4.4 crash when attempting to use HEVC,
// so I'm restricting HEVC usage to Lollipop and higher.
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
return false;
}
//
// Software decoders are terrible and we never want to use them.
// We want to catch decoders like:
// OMX.qcom.video.decoder.hevcswvdec
// OMX.SEC.hevc.sw.dec
//
if (decoderName.contains("sw")) {
return false;
}
// Some devices have HEVC decoders that we prefer not to use
// typically because it can't support reference frame invalidation.
// However, we will use it for HDR and for streaming over mobile networks
// since it works fine otherwise.
if (meteredData && isDecoderInList(deprioritizedHevcDecoders, decoderName)) {
LimeLog.info("Selected deprioritized decoder");
return true;
}
return isDecoderInList(whitelistedHevcDecoders, decoderName);
}
@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);
@@ -133,10 +448,14 @@ public class MediaCodecHelper {
return str;
}
public static MediaCodecInfo findPreferredDecoder() {
private static MediaCodecInfo findPreferredDecoder() {
// This is a different algorithm than the other findXXXDecoder functions,
// because we want to evaluate the decoders in our list's order
// rather than MediaCodecList's order
if (!initialized) {
throw new IllegalStateException("MediaCodecHelper must be initialized before use");
}
for (String preferredDecoder : preferredDecoders) {
for (MediaCodecInfo codecInfo : getMediaCodecList()) {
@@ -156,7 +475,7 @@ public class MediaCodecHelper {
return null;
}
public static MediaCodecInfo findFirstDecoder() {
public static MediaCodecInfo findFirstDecoder(String mimeType) {
for (MediaCodecInfo codecInfo : getMediaCodecList()) {
// Skip encoders
if (codecInfo.isEncoder()) {
@@ -169,9 +488,9 @@ public class MediaCodecHelper {
continue;
}
// Find a decoder that supports H.264
// Find a decoder that supports the specified video format
for (String mime : codecInfo.getSupportedTypes()) {
if (mime.equalsIgnoreCase("video/avc")) {
if (mime.equalsIgnoreCase(mimeType)) {
LimeLog.info("First decoder choice is "+codecInfo.getName());
return codecInfo;
}
@@ -181,7 +500,7 @@ public class MediaCodecHelper {
return null;
}
public static MediaCodecInfo findProbableSafeDecoder() {
public static MediaCodecInfo findProbableSafeDecoder(String mimeType, int requiredProfile) {
// First look for a preferred decoder by name
MediaCodecInfo info = findPreferredDecoder();
if (info != null) {
@@ -191,12 +510,12 @@ public class MediaCodecHelper {
// Now look for decoders we know are safe
try {
// If this function completes, it will determine if the decoder is safe
return findKnownSafeDecoder();
return findKnownSafeDecoder(mimeType, requiredProfile);
} catch (Exception e) {
// Some buggy devices seem to throw exceptions
// from getCapabilitiesForType() so we'll just assume
// they're okay and go with the first one we find
return findFirstDecoder();
return findFirstDecoder(mimeType);
}
}
@@ -204,7 +523,7 @@ public class MediaCodecHelper {
// since some bad decoders can throw IllegalArgumentExceptions unexpectedly
// and we want to be sure all callers are handling this possibility
@SuppressWarnings("RedundantThrows")
public static MediaCodecInfo findKnownSafeDecoder() throws Exception {
private static MediaCodecInfo findKnownSafeDecoder(String mimeType, int requiredProfile) throws Exception {
for (MediaCodecInfo codecInfo : getMediaCodecList()) {
// Skip encoders
if (codecInfo.isEncoder()) {
@@ -217,21 +536,26 @@ public class MediaCodecHelper {
continue;
}
// Find a decoder that supports H.264 high profile
// Find a decoder that supports the requested video format
for (String mime : codecInfo.getSupportedTypes()) {
if (mime.equalsIgnoreCase("video/avc")) {
if (mime.equalsIgnoreCase(mimeType)) {
LimeLog.info("Examining decoder capabilities of "+codecInfo.getName());
CodecCapabilities caps = codecInfo.getCapabilitiesForType(mime);
for (CodecProfileLevel profile : caps.profileLevels) {
if (profile.profile == CodecProfileLevel.AVCProfileHigh) {
LimeLog.info("Decoder "+codecInfo.getName()+" supports high profile");
LimeLog.info("Selected decoder: "+codecInfo.getName());
return codecInfo;
if (requiredProfile != -1) {
for (CodecProfileLevel profile : caps.profileLevels) {
if (profile.profile == requiredProfile) {
LimeLog.info("Decoder " + codecInfo.getName() + " supports required profile");
return codecInfo;
}
}
LimeLog.info("Decoder " + codecInfo.getName() + " does NOT support required profile");
}
else {
return codecInfo;
}
LimeLog.info("Decoder "+codecInfo.getName()+" does NOT support high profile");
}
}
}
@@ -249,7 +573,7 @@ public class MediaCodecHelper {
break;
cpuInfo.append((char)ch);
}
return cpuInfo.toString();
} finally {
br.close();

View File

@@ -1,7 +1,5 @@
package com.limelight.computers;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
@@ -17,148 +15,123 @@ import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteException;
public class ComputerDatabaseManager {
private static final String COMPUTER_DB_NAME = "computers.db";
private static final String COMPUTER_TABLE_NAME = "Computers";
private static final String COMPUTER_NAME_COLUMN_NAME = "ComputerName";
private static final String COMPUTER_UUID_COLUMN_NAME = "UUID";
private static final String LOCAL_IP_COLUMN_NAME = "LocalIp";
private static final String REMOTE_IP_COLUMN_NAME = "RemoteIp";
private static final String MAC_COLUMN_NAME = "Mac";
private SQLiteDatabase computerDb;
public ComputerDatabaseManager(Context c) {
try {
// Create or open an existing DB
computerDb = c.openOrCreateDatabase(COMPUTER_DB_NAME, 0, null);
} catch (SQLiteException e) {
// Delete the DB and try again
c.deleteDatabase(COMPUTER_DB_NAME);
computerDb = c.openOrCreateDatabase(COMPUTER_DB_NAME, 0, null);
}
initializeDb();
}
public void close() {
computerDb.close();
}
private void initializeDb() {
// Create tables if they aren't already there
computerDb.execSQL(String.format((Locale)null, "CREATE TABLE IF NOT EXISTS %s(%s TEXT PRIMARY KEY," +
" %s TEXT NOT NULL, %s TEXT NOT NULL, %s TEXT NOT NULL, %s TEXT NOT NULL)",
COMPUTER_TABLE_NAME,
COMPUTER_NAME_COLUMN_NAME, COMPUTER_UUID_COLUMN_NAME, LOCAL_IP_COLUMN_NAME,
REMOTE_IP_COLUMN_NAME, MAC_COLUMN_NAME));
}
public void deleteComputer(String name) {
computerDb.delete(COMPUTER_TABLE_NAME, COMPUTER_NAME_COLUMN_NAME+"='"+name+"'", null);
}
public boolean updateComputer(ComputerDetails details) {
ContentValues values = new ContentValues();
values.put(COMPUTER_NAME_COLUMN_NAME, details.name);
values.put(COMPUTER_UUID_COLUMN_NAME, details.uuid.toString());
values.put(LOCAL_IP_COLUMN_NAME, details.localIp.getAddress());
values.put(REMOTE_IP_COLUMN_NAME, details.remoteIp.getAddress());
values.put(MAC_COLUMN_NAME, details.macAddress);
return -1 != computerDb.insertWithOnConflict(COMPUTER_TABLE_NAME, null, values, SQLiteDatabase.CONFLICT_REPLACE);
}
public List<ComputerDetails> getAllComputers() {
Cursor c = computerDb.rawQuery("SELECT * FROM "+COMPUTER_TABLE_NAME, null);
LinkedList<ComputerDetails> computerList = new LinkedList<ComputerDetails>();
while (c.moveToNext()) {
ComputerDetails details = new ComputerDetails();
details.name = c.getString(0);
String uuidStr = c.getString(1);
try {
details.uuid = UUID.fromString(uuidStr);
} catch (IllegalArgumentException e) {
// We'll delete this entry
LimeLog.severe("DB: Corrupted UUID for "+details.name);
}
try {
details.localIp = InetAddress.getByAddress(c.getBlob(2));
} catch (UnknownHostException e) {
// We'll delete this entry
LimeLog.severe("DB: Corrupted local IP for "+details.name);
}
try {
details.remoteIp = InetAddress.getByAddress(c.getBlob(3));
} catch (UnknownHostException e) {
// We'll delete this entry
LimeLog.severe("DB: Corrupted remote IP for "+details.name);
}
details.macAddress = c.getString(4);
// This signifies we don't have dynamic state (like pair state)
details.state = ComputerDetails.State.UNKNOWN;
// If a field is corrupt or missing, skip the database entry
if (details.uuid == null || details.localIp == null || details.remoteIp == null ||
details.macAddress == null) {
continue;
}
computerList.add(details);
}
c.close();
return computerList;
}
public ComputerDetails getComputerByName(String name) {
Cursor c = computerDb.rawQuery("SELECT * FROM "+COMPUTER_TABLE_NAME+" WHERE "+COMPUTER_NAME_COLUMN_NAME+"='"+name+"'", null);
ComputerDetails details = new ComputerDetails();
if (!c.moveToFirst()) {
// No matching computer
c.close();
return null;
}
private static final String COMPUTER_DB_NAME = "computers2.db";
private static final String COMPUTER_TABLE_NAME = "Computers";
private static final String COMPUTER_UUID_COLUMN_NAME = "UUID";
private static final String COMPUTER_NAME_COLUMN_NAME = "ComputerName";
private static final String LOCAL_ADDRESS_COLUMN_NAME = "LocalAddress";
private static final String REMOTE_ADDRESS_COLUMN_NAME = "RemoteAddress";
private static final String MANUAL_ADDRESS_COLUMN_NAME = "ManualAddress";
private static final String MAC_ADDRESS_COLUMN_NAME = "MacAddress";
details.name = c.getString(0);
String uuidStr = c.getString(1);
try {
details.uuid = UUID.fromString(uuidStr);
} catch (IllegalArgumentException e) {
// We'll delete this entry
LimeLog.severe("DB: Corrupted UUID for "+details.name);
}
try {
details.localIp = InetAddress.getByAddress(c.getBlob(2));
} catch (UnknownHostException e) {
// We'll delete this entry
LimeLog.severe("DB: Corrupted local IP for "+details.name);
}
try {
details.remoteIp = InetAddress.getByAddress(c.getBlob(3));
} catch (UnknownHostException e) {
// We'll delete this entry
LimeLog.severe("DB: Corrupted remote IP for "+details.name);
}
details.macAddress = c.getString(4);
c.close();
// If a field is corrupt or missing, delete the database entry
if (details.uuid == null || details.localIp == null || details.remoteIp == null ||
details.macAddress == null) {
deleteComputer(details.name);
return null;
}
return details;
}
private SQLiteDatabase computerDb;
public ComputerDatabaseManager(Context c) {
try {
// Create or open an existing DB
computerDb = c.openOrCreateDatabase(COMPUTER_DB_NAME, 0, null);
} catch (SQLiteException e) {
// Delete the DB and try again
c.deleteDatabase(COMPUTER_DB_NAME);
computerDb = c.openOrCreateDatabase(COMPUTER_DB_NAME, 0, null);
}
initializeDb(c);
}
public void close() {
computerDb.close();
}
private void initializeDb(Context c) {
// Create tables if they aren't already there
computerDb.execSQL(String.format((Locale)null,
"CREATE TABLE IF NOT EXISTS %s(%s TEXT PRIMARY KEY, %s TEXT NOT NULL, %s TEXT, %s TEXT, %s TEXT, %s TEXT)",
COMPUTER_TABLE_NAME,
COMPUTER_UUID_COLUMN_NAME, COMPUTER_NAME_COLUMN_NAME,
LOCAL_ADDRESS_COLUMN_NAME, REMOTE_ADDRESS_COLUMN_NAME, MANUAL_ADDRESS_COLUMN_NAME,
MAC_ADDRESS_COLUMN_NAME));
// Move all computers from the old DB (if any) to the new one
List<ComputerDetails> oldComputers = LegacyDatabaseReader.migrateAllComputers(c);
for (ComputerDetails computer : oldComputers) {
updateComputer(computer);
}
}
public void deleteComputer(String name) {
computerDb.delete(COMPUTER_TABLE_NAME, COMPUTER_NAME_COLUMN_NAME+"=?", new String[]{name});
}
public boolean updateComputer(ComputerDetails details) {
ContentValues values = new ContentValues();
values.put(COMPUTER_UUID_COLUMN_NAME, details.uuid.toString());
values.put(COMPUTER_NAME_COLUMN_NAME, details.name);
values.put(LOCAL_ADDRESS_COLUMN_NAME, details.localAddress);
values.put(REMOTE_ADDRESS_COLUMN_NAME, details.remoteAddress);
values.put(MANUAL_ADDRESS_COLUMN_NAME, details.manualAddress);
values.put(MAC_ADDRESS_COLUMN_NAME, details.macAddress);
return -1 != computerDb.insertWithOnConflict(COMPUTER_TABLE_NAME, null, values, SQLiteDatabase.CONFLICT_REPLACE);
}
private ComputerDetails getComputerFromCursor(Cursor c) {
ComputerDetails details = new ComputerDetails();
String uuidStr = c.getString(0);
try {
details.uuid = UUID.fromString(uuidStr);
} catch (IllegalArgumentException e) {
// We'll delete this entry
LimeLog.severe("DB: Corrupted UUID for "+details.name);
}
details.name = c.getString(1);
details.localAddress = c.getString(2);
details.remoteAddress = c.getString(3);
details.manualAddress = c.getString(4);
details.macAddress = c.getString(5);
// This signifies we don't have dynamic state (like pair state)
details.state = ComputerDetails.State.UNKNOWN;
return details;
}
public List<ComputerDetails> getAllComputers() {
Cursor c = computerDb.rawQuery("SELECT * FROM "+COMPUTER_TABLE_NAME, null);
LinkedList<ComputerDetails> computerList = new LinkedList<>();
while (c.moveToNext()) {
ComputerDetails details = getComputerFromCursor(c);
// If a critical field is corrupt or missing, skip the database entry
if (details.uuid == null) {
continue;
}
computerList.add(details);
}
c.close();
return computerList;
}
public ComputerDetails getComputerByUUID(UUID uuid) {
Cursor c = computerDb.query(COMPUTER_TABLE_NAME, null, COMPUTER_UUID_COLUMN_NAME+"=?", new String[]{ uuid.toString() }, null, null, null);
if (!c.moveToFirst()) {
// No matching computer
c.close();
return null;
}
ComputerDetails details = getComputerFromCursor(c);
c.close();
// If a critical field is corrupt or missing, delete the database entry
if (details.uuid == null) {
deleteComputer(details.name);
return null;
}
return details;
}
}

View File

@@ -3,5 +3,5 @@ package com.limelight.computers;
import com.limelight.nvstream.http.ComputerDetails;
public interface ComputerManagerListener {
public void notifyComputerUpdated(ComputerDetails details);
void notifyComputerUpdated(ComputerDetails details);
}

View File

@@ -12,75 +12,75 @@ import com.limelight.LimeLog;
import android.content.Context;
public class IdentityManager {
private static final String UNIQUE_ID_FILE_NAME = "uniqueid";
private static final int UID_SIZE_IN_BYTES = 8;
private String uniqueId;
public IdentityManager(Context c) {
uniqueId = loadUniqueId(c);
if (uniqueId == null) {
uniqueId = generateNewUniqueId(c);
}
LimeLog.info("UID is now: "+uniqueId);
}
public String getUniqueId() {
return uniqueId;
}
private static String loadUniqueId(Context c) {
// 2 Hex digits per byte
char[] uid = new char[UID_SIZE_IN_BYTES * 2];
InputStreamReader reader = null;
LimeLog.info("Reading UID from disk");
try {
reader = new InputStreamReader(c.openFileInput(UNIQUE_ID_FILE_NAME));
if (reader.read(uid) != UID_SIZE_IN_BYTES * 2)
{
LimeLog.severe("UID file data is truncated");
return null;
}
return new String(uid);
} catch (FileNotFoundException e) {
LimeLog.info("No UID file found");
return null;
} catch (IOException e) {
LimeLog.severe("Error while reading UID file");
e.printStackTrace();
return null;
} finally {
if (reader != null) {
try {
reader.close();
} catch (IOException ignored) {}
}
}
}
private static String generateNewUniqueId(Context c) {
// Generate a new UID hex string
LimeLog.info("Generating new UID");
String uidStr = String.format((Locale)null, "%016x", new Random().nextLong());
OutputStreamWriter writer = null;
try {
writer = new OutputStreamWriter(c.openFileOutput(UNIQUE_ID_FILE_NAME, 0));
writer.write(uidStr);
LimeLog.info("UID written to disk");
} catch (IOException e) {
LimeLog.severe("Error while writing UID file");
e.printStackTrace();
} finally {
if (writer != null) {
try {
writer.close();
} catch (IOException ignored) {}
}
}
// We can return a UID even if I/O fails
return uidStr;
}
private static final String UNIQUE_ID_FILE_NAME = "uniqueid";
private static final int UID_SIZE_IN_BYTES = 8;
private String uniqueId;
public IdentityManager(Context c) {
uniqueId = loadUniqueId(c);
if (uniqueId == null) {
uniqueId = generateNewUniqueId(c);
}
LimeLog.info("UID is now: "+uniqueId);
}
public String getUniqueId() {
return uniqueId;
}
private static String loadUniqueId(Context c) {
// 2 Hex digits per byte
char[] uid = new char[UID_SIZE_IN_BYTES * 2];
InputStreamReader reader = null;
LimeLog.info("Reading UID from disk");
try {
reader = new InputStreamReader(c.openFileInput(UNIQUE_ID_FILE_NAME));
if (reader.read(uid) != UID_SIZE_IN_BYTES * 2)
{
LimeLog.severe("UID file data is truncated");
return null;
}
return new String(uid);
} catch (FileNotFoundException e) {
LimeLog.info("No UID file found");
return null;
} catch (IOException e) {
LimeLog.severe("Error while reading UID file");
e.printStackTrace();
return null;
} finally {
if (reader != null) {
try {
reader.close();
} catch (IOException ignored) {}
}
}
}
private static String generateNewUniqueId(Context c) {
// Generate a new UID hex string
LimeLog.info("Generating new UID");
String uidStr = String.format((Locale)null, "%016x", new Random().nextLong());
OutputStreamWriter writer = null;
try {
writer = new OutputStreamWriter(c.openFileOutput(UNIQUE_ID_FILE_NAME, 0));
writer.write(uidStr);
LimeLog.info("UID written to disk");
} catch (IOException e) {
LimeLog.severe("Error while writing UID file");
e.printStackTrace();
} finally {
if (writer != null) {
try {
writer.close();
} catch (IOException ignored) {}
}
}
// We can return a UID even if I/O fails
return uidStr;
}
}

View File

@@ -0,0 +1,113 @@
package com.limelight.computers;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteException;
import com.limelight.LimeLog;
import com.limelight.nvstream.http.ComputerDetails;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.LinkedList;
import java.util.List;
import java.util.UUID;
public class LegacyDatabaseReader {
private static final String COMPUTER_DB_NAME = "computers.db";
private static final String COMPUTER_TABLE_NAME = "Computers";
private static final String ADDRESS_PREFIX = "ADDRESS_PREFIX__";
private static ComputerDetails getComputerFromCursor(Cursor c) {
ComputerDetails details = new ComputerDetails();
details.name = c.getString(0);
String uuidStr = c.getString(1);
try {
details.uuid = UUID.fromString(uuidStr);
} catch (IllegalArgumentException e) {
// We'll delete this entry
LimeLog.severe("DB: Corrupted UUID for " + details.name);
}
// An earlier schema defined addresses as byte blobs. We'll
// gracefully migrate those to strings so we can store DNS names
// too. To disambiguate, we'll need to prefix them with a string
// greater than the allowable IP address length.
try {
details.localAddress = InetAddress.getByAddress(c.getBlob(2)).getHostAddress();
LimeLog.warning("DB: Legacy local address for " + details.name);
} catch (UnknownHostException e) {
// This is probably a hostname/address with the prefix string
String stringData = c.getString(2);
if (stringData.startsWith(ADDRESS_PREFIX)) {
details.localAddress = c.getString(2).substring(ADDRESS_PREFIX.length());
} else {
LimeLog.severe("DB: Corrupted local address for " + details.name);
}
}
try {
details.remoteAddress = InetAddress.getByAddress(c.getBlob(3)).getHostAddress();
LimeLog.warning("DB: Legacy remote address for " + details.name);
} catch (UnknownHostException e) {
// This is probably a hostname/address with the prefix string
String stringData = c.getString(3);
if (stringData.startsWith(ADDRESS_PREFIX)) {
details.remoteAddress = c.getString(3).substring(ADDRESS_PREFIX.length());
} else {
LimeLog.severe("DB: Corrupted remote address for " + details.name);
}
}
// On older versions of Moonlight, this is typically where manual addresses got stored,
// so let's initialize it just to be safe.
details.manualAddress = details.remoteAddress;
details.macAddress = c.getString(4);
// This signifies we don't have dynamic state (like pair state)
details.state = ComputerDetails.State.UNKNOWN;
return details;
}
private static List<ComputerDetails> getAllComputers(SQLiteDatabase db) {
Cursor c = db.rawQuery("SELECT * FROM " + COMPUTER_TABLE_NAME, null);
LinkedList<ComputerDetails> computerList = new LinkedList<>();
while (c.moveToNext()) {
ComputerDetails details = getComputerFromCursor(c);
// If a critical field is corrupt or missing, skip the database entry
if (details.uuid == null) {
continue;
}
computerList.add(details);
}
c.close();
return computerList;
}
public static List<ComputerDetails> migrateAllComputers(Context c) {
SQLiteDatabase computerDb = null;
try {
// Open the existing database
computerDb = SQLiteDatabase.openDatabase(c.getDatabasePath(COMPUTER_DB_NAME).getPath(), null, SQLiteDatabase.OPEN_READONLY);
return getAllComputers(computerDb);
} catch (SQLiteException e) {
return new LinkedList<ComputerDetails>();
} finally {
// Close and delete the old DB
if (computerDb != null) {
computerDb.close();
}
c.deleteDatabase(COMPUTER_DB_NAME);
}
}
}

View File

@@ -15,76 +15,76 @@ import android.os.Binder;
import android.os.IBinder;
public class DiscoveryService extends Service {
private MdnsDiscoveryAgent discoveryAgent;
private MdnsDiscoveryListener boundListener;
private MulticastLock multicastLock;
public class DiscoveryBinder extends Binder {
public void setListener(MdnsDiscoveryListener listener) {
boundListener = listener;
}
public void startDiscovery(int queryIntervalMs) {
multicastLock.acquire();
discoveryAgent.startDiscovery(queryIntervalMs);
}
public void stopDiscovery() {
discoveryAgent.stopDiscovery();
multicastLock.release();
}
public List<MdnsComputer> getComputerSet() {
return discoveryAgent.getComputerSet();
}
}
@Override
public void onCreate() {
WifiManager wifiMgr = (WifiManager) getSystemService(Context.WIFI_SERVICE);
multicastLock = wifiMgr.createMulticastLock("Limelight mDNS");
multicastLock.setReferenceCounted(false);
discoveryAgent = new MdnsDiscoveryAgent(new MdnsDiscoveryListener() {
@Override
public void notifyComputerAdded(MdnsComputer computer) {
if (boundListener != null) {
boundListener.notifyComputerAdded(computer);
}
}
@Override
public void notifyComputerRemoved(MdnsComputer computer) {
if (boundListener != null) {
boundListener.notifyComputerRemoved(computer);
}
}
private MdnsDiscoveryAgent discoveryAgent;
private MdnsDiscoveryListener boundListener;
private MulticastLock multicastLock;
@Override
public void notifyDiscoveryFailure(Exception e) {
if (boundListener != null) {
boundListener.notifyDiscoveryFailure(e);
}
}
});
}
private DiscoveryBinder binder = new DiscoveryBinder();
@Override
public IBinder onBind(Intent intent) {
return binder;
}
@Override
public boolean onUnbind(Intent intent) {
// Stop any discovery session
discoveryAgent.stopDiscovery();
multicastLock.release();
// Unbind the listener
boundListener = null;
return false;
}
public class DiscoveryBinder extends Binder {
public void setListener(MdnsDiscoveryListener listener) {
boundListener = listener;
}
public void startDiscovery(int queryIntervalMs) {
multicastLock.acquire();
discoveryAgent.startDiscovery(queryIntervalMs);
}
public void stopDiscovery() {
discoveryAgent.stopDiscovery();
multicastLock.release();
}
public List<MdnsComputer> getComputerSet() {
return discoveryAgent.getComputerSet();
}
}
@Override
public void onCreate() {
WifiManager wifiMgr = (WifiManager) getApplicationContext().getSystemService(Context.WIFI_SERVICE);
multicastLock = wifiMgr.createMulticastLock("Limelight mDNS");
multicastLock.setReferenceCounted(false);
discoveryAgent = new MdnsDiscoveryAgent(new MdnsDiscoveryListener() {
@Override
public void notifyComputerAdded(MdnsComputer computer) {
if (boundListener != null) {
boundListener.notifyComputerAdded(computer);
}
}
@Override
public void notifyComputerRemoved(MdnsComputer computer) {
if (boundListener != null) {
boundListener.notifyComputerRemoved(computer);
}
}
@Override
public void notifyDiscoveryFailure(Exception e) {
if (boundListener != null) {
boundListener.notifyDiscoveryFailure(e);
}
}
});
}
private final DiscoveryBinder binder = new DiscoveryBinder();
@Override
public IBinder onBind(Intent intent) {
return binder;
}
@Override
public boolean onUnbind(Intent intent) {
// Stop any discovery session
discoveryAgent.stopDiscovery();
multicastLock.release();
// Unbind the listener
boundListener = null;
return false;
}
}

View File

@@ -0,0 +1,109 @@
package com.limelight.grid;
import android.app.Activity;
import android.widget.ImageView;
import android.widget.ProgressBar;
import android.widget.TextView;
import com.limelight.AppView;
import com.limelight.LimeLog;
import com.limelight.R;
import com.limelight.grid.assets.CachedAppAssetLoader;
import com.limelight.grid.assets.DiskAssetLoader;
import com.limelight.grid.assets.MemoryAssetLoader;
import com.limelight.grid.assets.NetworkAssetLoader;
import com.limelight.nvstream.http.ComputerDetails;
import java.util.Collections;
import java.util.Comparator;
@SuppressWarnings("unchecked")
public class AppGridAdapter extends GenericGridAdapter<AppView.AppObject> {
private static final int ART_WIDTH_PX = 300;
private static final int SMALL_WIDTH_DP = 100;
private static final int LARGE_WIDTH_DP = 150;
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));
int dpi = activity.getResources().getDisplayMetrics().densityDpi;
int dp;
if (small) {
dp = SMALL_WIDTH_DP;
}
else {
dp = LARGE_WIDTH_DP;
}
double scalingDivisor = ART_WIDTH_PX / (dp * (dpi / 160.0));
if (scalingDivisor < 1.0) {
// We don't want to make them bigger before draw-time
scalingDivisor = 1.0;
}
LimeLog.info("Art scaling divisor: " + scalingDivisor);
this.loader = new CachedAppAssetLoader(computer, scalingDivisor,
new NetworkAssetLoader(context, uniqueId),
new MemoryAssetLoader(),
new DiskAssetLoader(context));
}
public void cancelQueuedOperations() {
loader.cancelForegroundLoads();
loader.cancelBackgroundLoads();
loader.freeCacheMemory();
}
private void sortList() {
Collections.sort(itemList, new Comparator<AppView.AppObject>() {
@Override
public int compare(AppView.AppObject lhs, AppView.AppObject rhs) {
return lhs.app.getAppName().toLowerCase().compareTo(rhs.app.getAppName().toLowerCase());
}
});
}
public void addApp(AppView.AppObject app) {
// Queue a request to fetch this bitmap into cache
loader.queueCacheLoad(app.app);
// Add the app to our sorted list
itemList.add(app);
sortList();
}
public void removeApp(AppView.AppObject app) {
itemList.remove(app);
}
@Override
public boolean populateImageView(ImageView imgView, ProgressBar prgView, AppView.AppObject obj) {
// Let the cached asset loader handle it
loader.populateImageView(obj.app, imgView, prgView);
return true;
}
@Override
public boolean populateTextView(TextView txtView, AppView.AppObject obj) {
// Select the text view so it starts marquee mode
txtView.setSelected(true);
// Return false to use the app's toString method
return false;
}
@Override
public boolean populateOverlayView(ImageView overlayView, AppView.AppObject obj) {
if (obj.isRunning) {
// Show the play button overlay
overlayView.setImageResource(R.drawable.ic_play);
return true;
}
// No overlay
return false;
}
}

View File

@@ -0,0 +1,82 @@
package com.limelight.grid;
import android.content.Context;
import android.view.LayoutInflater;
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;
import java.util.ArrayList;
public abstract class GenericGridAdapter<T> extends BaseAdapter {
protected final Context context;
protected final int layoutId;
protected final ArrayList<T> itemList = new ArrayList<>();
protected final LayoutInflater inflater;
public GenericGridAdapter(Context context, int layoutId) {
this.context = context;
this.layoutId = layoutId;
this.inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
}
public void clear() {
itemList.clear();
}
@Override
public int getCount() {
return itemList.size();
}
@Override
public Object getItem(int i) {
return itemList.get(i);
}
@Override
public long getItemId(int i) {
return i;
}
public abstract boolean populateImageView(ImageView imgView, ProgressBar prgView, T obj);
public abstract boolean populateTextView(TextView txtView, T obj);
public abstract boolean populateOverlayView(ImageView overlayView, T obj);
@Override
public View getView(int i, View convertView, ViewGroup viewGroup) {
if (convertView == null) {
convertView = inflater.inflate(layoutId, viewGroup, false);
}
ImageView imgView = convertView.findViewById(R.id.grid_image);
ImageView overlayView = convertView.findViewById(R.id.grid_overlay);
TextView txtView = convertView.findViewById(R.id.grid_text);
ProgressBar prgView = convertView.findViewById(R.id.grid_spinner);
if (imgView != null) {
if (!populateImageView(imgView, prgView, itemList.get(i))) {
imgView.setImageBitmap(null);
}
}
if (!populateTextView(txtView, itemList.get(i))) {
txtView.setText(itemList.get(i).toString());
}
if (overlayView != null) {
if (!populateOverlayView(overlayView, itemList.get(i))) {
overlayView.setVisibility(View.INVISIBLE);
}
else {
overlayView.setVisibility(View.VISIBLE);
}
}
return convertView;
}
}

View File

@@ -0,0 +1,82 @@
package com.limelight.grid;
import android.content.Context;
import android.view.View;
import android.widget.ImageView;
import android.widget.ProgressBar;
import android.widget.TextView;
import com.limelight.PcView;
import com.limelight.R;
import com.limelight.nvstream.http.ComputerDetails;
import java.util.Collections;
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));
}
public void addComputer(PcView.ComputerObject computer) {
itemList.add(computer);
sortList();
}
private void sortList() {
Collections.sort(itemList, new Comparator<PcView.ComputerObject>() {
@Override
public int compare(PcView.ComputerObject lhs, PcView.ComputerObject rhs) {
return lhs.details.name.toLowerCase().compareTo(rhs.details.name.toLowerCase());
}
});
}
public boolean removeComputer(PcView.ComputerObject computer) {
return itemList.remove(computer);
}
@Override
public boolean populateImageView(ImageView imgView, ProgressBar prgView, PcView.ComputerObject obj) {
if (obj.details.state == ComputerDetails.State.ONLINE) {
imgView.setAlpha(1.0f);
}
else {
imgView.setAlpha(0.4f);
}
if (obj.details.state == ComputerDetails.State.UNKNOWN) {
prgView.setVisibility(View.VISIBLE);
}
else {
prgView.setVisibility(View.INVISIBLE);
}
imgView.setImageResource(R.drawable.ic_computer);
return true;
}
@Override
public boolean populateTextView(TextView txtView, PcView.ComputerObject obj) {
if (obj.details.state == ComputerDetails.State.ONLINE) {
txtView.setAlpha(1.0f);
}
else {
txtView.setAlpha(0.4f);
}
// Return false to use the computer's toString method
return false;
}
@Override
public boolean populateOverlayView(ImageView overlayView, PcView.ComputerObject obj) {
if (obj.details.state == ComputerDetails.State.OFFLINE) {
overlayView.setImageResource(R.drawable.ic_pc_offline);
overlayView.setAlpha(0.4f);
return true;
}
return false;
}
}

View File

@@ -0,0 +1,357 @@
package com.limelight.grid.assets;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.os.AsyncTask;
import android.view.View;
import android.widget.ImageView;
import android.widget.ProgressBar;
import com.limelight.nvstream.http.ComputerDetails;
import com.limelight.nvstream.http.NvApp;
import java.io.IOException;
import java.io.InputStream;
import java.lang.ref.WeakReference;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
public class CachedAppAssetLoader {
private static final int MAX_CONCURRENT_DISK_LOADS = 3;
private static final int MAX_CONCURRENT_NETWORK_LOADS = 3;
private static final int MAX_CONCURRENT_CACHE_LOADS = 1;
private static final int MAX_PENDING_CACHE_LOADS = 100;
private static final int MAX_PENDING_NETWORK_LOADS = 40;
private static final int MAX_PENDING_DISK_LOADS = 40;
private final ThreadPoolExecutor cacheExecutor = new ThreadPoolExecutor(
MAX_CONCURRENT_CACHE_LOADS, MAX_CONCURRENT_CACHE_LOADS,
Long.MAX_VALUE, TimeUnit.DAYS,
new LinkedBlockingQueue<Runnable>(MAX_PENDING_CACHE_LOADS),
new ThreadPoolExecutor.DiscardOldestPolicy());
private final ThreadPoolExecutor foregroundExecutor = new ThreadPoolExecutor(
MAX_CONCURRENT_DISK_LOADS, MAX_CONCURRENT_DISK_LOADS,
Long.MAX_VALUE, TimeUnit.DAYS,
new LinkedBlockingQueue<Runnable>(MAX_PENDING_DISK_LOADS),
new ThreadPoolExecutor.DiscardOldestPolicy());
private final ThreadPoolExecutor networkExecutor = new ThreadPoolExecutor(
MAX_CONCURRENT_NETWORK_LOADS, MAX_CONCURRENT_NETWORK_LOADS,
Long.MAX_VALUE, TimeUnit.DAYS,
new LinkedBlockingQueue<Runnable>(MAX_PENDING_NETWORK_LOADS),
new ThreadPoolExecutor.DiscardOldestPolicy());
private final ComputerDetails computer;
private final double scalingDivider;
private final NetworkAssetLoader networkLoader;
private final MemoryAssetLoader memoryLoader;
private final DiskAssetLoader diskLoader;
private final Bitmap placeholderBitmap;
public CachedAppAssetLoader(ComputerDetails computer, double scalingDivider,
NetworkAssetLoader networkLoader, MemoryAssetLoader memoryLoader,
DiskAssetLoader diskLoader) {
this.computer = computer;
this.scalingDivider = scalingDivider;
this.networkLoader = networkLoader;
this.memoryLoader = memoryLoader;
this.diskLoader = diskLoader;
this.placeholderBitmap = Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888);
}
public void cancelBackgroundLoads() {
Runnable r;
while ((r = cacheExecutor.getQueue().poll()) != null) {
cacheExecutor.remove(r);
}
}
public void cancelForegroundLoads() {
Runnable r;
while ((r = foregroundExecutor.getQueue().poll()) != null) {
foregroundExecutor.remove(r);
}
while ((r = networkExecutor.getQueue().poll()) != null) {
networkExecutor.remove(r);
}
}
public void freeCacheMemory() {
memoryLoader.clearCache();
}
private Bitmap doNetworkAssetLoad(LoaderTuple tuple, LoaderTask task) {
// Try 3 times
for (int i = 0; i < 3; i++) {
// Check again whether we've been cancelled or the image view is gone
if (task != null && (task.isCancelled() || task.imageViewRef.get() == null)) {
return null;
}
InputStream in = networkLoader.getBitmapStream(tuple);
if (in != null) {
// Write the stream straight to disk
diskLoader.populateCacheWithStream(tuple, in);
// Close the network input stream
try {
in.close();
} catch (IOException ignored) {}
// If there's a task associated with this load, we should return the bitmap
if (task != null) {
// If the cached bitmap is valid, return it. Otherwise, we'll try the load again
Bitmap bmp = diskLoader.loadBitmapFromCache(tuple, (int) scalingDivider);
if (bmp != null) {
return bmp;
}
}
else {
// Otherwise it's a background load and we return nothing
return null;
}
}
// Wait 1 second with a bit of fuzz
try {
Thread.sleep((int) (1000 + (Math.random() * 500)));
} catch (InterruptedException e) {
return null;
}
}
return null;
}
private class LoaderTask extends AsyncTask<LoaderTuple, Void, Bitmap> {
private final WeakReference<ImageView> imageViewRef;
private final WeakReference<ProgressBar> progressViewRef;
private final boolean diskOnly;
private LoaderTuple tuple;
public LoaderTask(ImageView imageView, ProgressBar prgView, boolean diskOnly) {
this.imageViewRef = new WeakReference<>(imageView);
this.progressViewRef = new WeakReference<>(prgView);
this.diskOnly = diskOnly;
}
@Override
protected Bitmap doInBackground(LoaderTuple... params) {
tuple = params[0];
// Check whether it has been cancelled or the views are gone
if (isCancelled() || imageViewRef.get() == null || progressViewRef.get() == null) {
return null;
}
Bitmap bmp = diskLoader.loadBitmapFromCache(tuple, (int) scalingDivider);
if (bmp == null) {
if (!diskOnly) {
// Try to load the asset from the network
bmp = doNetworkAssetLoad(tuple, this);
} else {
// Report progress to display the placeholder and spin
// off the network-capable task
publishProgress();
}
}
// Cache the bitmap
if (bmp != null) {
memoryLoader.populateCache(tuple, bmp);
}
return bmp;
}
@Override
protected void onProgressUpdate(Void... nothing) {
// Do nothing if cancelled
if (isCancelled()) {
return;
}
// If the current loader task for this view isn't us, do nothing
final ImageView imageView = imageViewRef.get();
final ProgressBar prgView = progressViewRef.get();
if (getLoaderTask(imageView) == this) {
// Now display the progress bar since we have to hit the network
if (prgView != null) {
prgView.setVisibility(View.VISIBLE);
}
// Set off another loader task on the network executor
LoaderTask task = new LoaderTask(imageView, prgView, false);
AsyncDrawable asyncDrawable = new AsyncDrawable(imageView.getResources(), placeholderBitmap, task);
imageView.setVisibility(View.VISIBLE);
imageView.setImageDrawable(asyncDrawable);
task.executeOnExecutor(networkExecutor, tuple);
}
}
@Override
protected void onPostExecute(Bitmap bitmap) {
// Do nothing if cancelled
if (isCancelled()) {
return;
}
final ImageView imageView = imageViewRef.get();
final ProgressBar prgView = progressViewRef.get();
if (getLoaderTask(imageView) == this) {
// Set the bitmap
if (bitmap != null) {
imageView.setImageBitmap(bitmap);
}
// Hide the progress bar
if (prgView != null) {
prgView.setVisibility(View.INVISIBLE);
}
// Show the view
imageView.setVisibility(View.VISIBLE);
}
}
}
static class AsyncDrawable extends BitmapDrawable {
private final WeakReference<LoaderTask> loaderTaskReference;
public AsyncDrawable(Resources res, Bitmap bitmap,
LoaderTask loaderTask) {
super(res, bitmap);
loaderTaskReference = new WeakReference<>(loaderTask);
}
public LoaderTask getLoaderTask() {
return loaderTaskReference.get();
}
}
private static LoaderTask getLoaderTask(ImageView imageView) {
if (imageView == null) {
return null;
}
final Drawable drawable = imageView.getDrawable();
// If our drawable is in play, get the loader task
if (drawable instanceof AsyncDrawable) {
final AsyncDrawable asyncDrawable = (AsyncDrawable) drawable;
return asyncDrawable.getLoaderTask();
}
return null;
}
private static boolean cancelPendingLoad(LoaderTuple tuple, ImageView imageView) {
final LoaderTask loaderTask = getLoaderTask(imageView);
// Check if any task was pending for this image view
if (loaderTask != null && !loaderTask.isCancelled()) {
final LoaderTuple taskTuple = loaderTask.tuple;
// Cancel the task if it's not already loading the same data
if (taskTuple == null || !taskTuple.equals(tuple)) {
loaderTask.cancel(true);
} else {
// It's already loading what we want
return false;
}
}
// Allow the load to proceed
return true;
}
public void queueCacheLoad(NvApp app) {
final LoaderTuple tuple = new LoaderTuple(computer, app);
if (memoryLoader.loadBitmapFromCache(tuple) != null) {
// It's in memory which means it must also be on disk
return;
}
// Queue a fetch in the cache executor
cacheExecutor.execute(new Runnable() {
@Override
public void run() {
// Check if the image is cached on disk
if (diskLoader.checkCacheExists(tuple)) {
return;
}
// Try to load the asset from the network and cache result on disk
doNetworkAssetLoad(tuple, null);
}
});
}
public boolean populateImageView(NvApp app, ImageView imgView, ProgressBar prgView) {
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, imgView)) {
return true;
}
// Hide the progress bar always on initial load
prgView.setVisibility(View.INVISIBLE);
// First, try the memory cache in the current context
Bitmap bmp = memoryLoader.loadBitmapFromCache(tuple);
if (bmp != null) {
// Show the bitmap immediately
imgView.setVisibility(View.VISIBLE);
imgView.setImageBitmap(bmp);
return true;
}
// If it's not in memory, create an async task to load it. This task will be attached
// via AsyncDrawable to this view.
final LoaderTask task = new LoaderTask(imgView, prgView, true);
final AsyncDrawable asyncDrawable = new AsyncDrawable(imgView.getResources(), placeholderBitmap, task);
imgView.setVisibility(View.INVISIBLE);
imgView.setImageDrawable(asyncDrawable);
// Run the task on our foreground executor
task.executeOnExecutor(foregroundExecutor, tuple);
return false;
}
public class LoaderTuple {
public final ComputerDetails computer;
public final NvApp app;
public LoaderTuple(ComputerDetails computer, NvApp app) {
this.computer = computer;
this.app = app;
}
@Override
public boolean equals(Object o) {
if (!(o instanceof LoaderTuple)) {
return false;
}
LoaderTuple other = (LoaderTuple) o;
return computer.uuid.equals(other.computer.uuid) && app.getAppId() == other.app.getAppId();
}
@Override
public String toString() {
return "("+computer.uuid+", "+app.getAppId()+")";
}
}
}

View File

@@ -0,0 +1,158 @@
package com.limelight.grid.assets;
import android.app.ActivityManager;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.ImageDecoder;
import android.os.Build;
import com.limelight.LimeLog;
import com.limelight.utils.CacheHelper;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
public class DiskAssetLoader {
// 5 MB
private static final long MAX_ASSET_SIZE = 5 * 1024 * 1024;
// Standard box art is 300x400
private static final int STANDARD_ASSET_WIDTH = 300;
private static final int STANDARD_ASSET_HEIGHT = 400;
private final boolean isLowRamDevice;
private final File cacheDir;
public DiskAssetLoader(Context context) {
this.cacheDir = context.getCacheDir();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
this.isLowRamDevice =
((ActivityManager)context.getSystemService(Context.ACTIVITY_SERVICE)).isLowRamDevice();
}
else {
// Use conservative low RAM behavior on very old devices
this.isLowRamDevice = true;
}
}
public boolean checkCacheExists(CachedAppAssetLoader.LoaderTuple tuple) {
return CacheHelper.cacheFileExists(cacheDir, "boxart", tuple.computer.uuid.toString(), tuple.app.getAppId() + ".png");
}
// https://developer.android.com/topic/performance/graphics/load-bitmap.html
public static int calculateInSampleSize(BitmapFactory.Options options, int reqWidth, int reqHeight) {
// Raw height and width of image
final int height = options.outHeight;
final int width = options.outWidth;
int inSampleSize = 1;
if (height > reqHeight || width > reqWidth) {
final int halfHeight = height / 2;
final int halfWidth = width / 2;
// Calculates the largest inSampleSize value that is a power of 2 and keeps both
// height and width larger than the requested height and width.
while ((halfHeight / inSampleSize) >= reqHeight && (halfWidth / inSampleSize) >= reqWidth) {
inSampleSize *= 2;
}
}
return inSampleSize;
}
public Bitmap loadBitmapFromCache(CachedAppAssetLoader.LoaderTuple tuple, int sampleSize) {
File file = CacheHelper.openPath(false, cacheDir, "boxart", tuple.computer.uuid.toString(), tuple.app.getAppId() + ".png");
// Don't bother with anything if it doesn't exist
if (!file.exists()) {
return null;
}
// Make sure the cached asset doesn't exceed the maximum size
if (file.length() > MAX_ASSET_SIZE) {
LimeLog.warning("Removing cached tuple exceeding size threshold: "+tuple);
file.delete();
return null;
}
Bitmap bmp;
// For OSes prior to P, we have to use the ugly BitmapFactory API
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) {
// Lookup bounds of the downloaded image
BitmapFactory.Options decodeOnlyOptions = new BitmapFactory.Options();
decodeOnlyOptions.inJustDecodeBounds = true;
BitmapFactory.decodeFile(file.getAbsolutePath(), decodeOnlyOptions);
if (decodeOnlyOptions.outWidth <= 0 || decodeOnlyOptions.outHeight <= 0) {
// Dimensions set to -1 on error. Return value always null.
return null;
}
LimeLog.info("Tuple "+tuple+" has cached art of size: "+decodeOnlyOptions.outWidth+"x"+decodeOnlyOptions.outHeight);
// Load the image scaled to the appropriate size
BitmapFactory.Options options = new BitmapFactory.Options();
options.inSampleSize = calculateInSampleSize(decodeOnlyOptions,
STANDARD_ASSET_WIDTH / sampleSize,
STANDARD_ASSET_HEIGHT / sampleSize);
if (isLowRamDevice) {
options.inPreferredConfig = Bitmap.Config.RGB_565;
options.inDither = true;
}
else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
options.inPreferredConfig = Bitmap.Config.HARDWARE;
}
bmp = BitmapFactory.decodeFile(file.getAbsolutePath(), options);
if (bmp != null) {
LimeLog.info("Tuple "+tuple+" decoded from disk cache with sample size: "+options.inSampleSize);
}
}
else {
// On P, we can get a bitmap back in one step with ImageDecoder
try {
bmp = ImageDecoder.decodeBitmap(ImageDecoder.createSource(file), new ImageDecoder.OnHeaderDecodedListener() {
@Override
public void onHeaderDecoded(ImageDecoder imageDecoder, ImageDecoder.ImageInfo imageInfo, ImageDecoder.Source source) {
imageDecoder.setTargetSize(STANDARD_ASSET_WIDTH, STANDARD_ASSET_HEIGHT);
if (isLowRamDevice) {
imageDecoder.setMemorySizePolicy(ImageDecoder.MEMORY_POLICY_LOW_RAM);
}
}
});
} catch (IOException e) {
e.printStackTrace();
return null;
}
}
return bmp;
}
public void populateCacheWithStream(CachedAppAssetLoader.LoaderTuple tuple, InputStream input) {
OutputStream out = null;
boolean success = false;
try {
out = CacheHelper.openCacheFileForOutput(cacheDir, "boxart", tuple.computer.uuid.toString(), tuple.app.getAppId() + ".png");
CacheHelper.writeInputStreamToOutputStream(input, out, MAX_ASSET_SIZE);
success = true;
} catch (IOException e) {
e.printStackTrace();
} finally {
if (out != null) {
try {
out.close();
} catch (IOException ignored) {}
}
if (!success) {
LimeLog.warning("Unable to populate cache with tuple: "+tuple);
CacheHelper.deleteCacheFile(cacheDir, "boxart", tuple.computer.uuid.toString(), tuple.app.getAppId() + ".png");
}
}
}
}

View File

@@ -0,0 +1,37 @@
package com.limelight.grid.assets;
import android.graphics.Bitmap;
import android.util.LruCache;
import com.limelight.LimeLog;
public class MemoryAssetLoader {
private static final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
private static final LruCache<String, Bitmap> memoryCache = new LruCache<String, Bitmap>(maxMemory / 16) {
@Override
protected int sizeOf(String key, Bitmap bitmap) {
// Sizeof returns kilobytes
return bitmap.getByteCount() / 1024;
}
};
private static String constructKey(CachedAppAssetLoader.LoaderTuple tuple) {
return tuple.computer.uuid.toString()+"-"+tuple.app.getAppId();
}
public Bitmap loadBitmapFromCache(CachedAppAssetLoader.LoaderTuple tuple) {
Bitmap bmp = memoryCache.get(constructKey(tuple));
if (bmp != null) {
LimeLog.info("Memory cache hit for tuple: "+tuple);
}
return bmp;
}
public void populateCache(CachedAppAssetLoader.LoaderTuple tuple, Bitmap bitmap) {
memoryCache.put(constructKey(tuple), bitmap);
}
public void clearCache() {
memoryCache.evictAll();
}
}

View File

@@ -0,0 +1,38 @@
package com.limelight.grid.assets;
import android.content.Context;
import com.limelight.LimeLog;
import com.limelight.binding.PlatformBinding;
import com.limelight.nvstream.http.NvHTTP;
import com.limelight.utils.ServerHelper;
import java.io.IOException;
import java.io.InputStream;
public class NetworkAssetLoader {
private final Context context;
private final String uniqueId;
public NetworkAssetLoader(Context context, String uniqueId) {
this.context = context;
this.uniqueId = uniqueId;
}
public InputStream getBitmapStream(CachedAppAssetLoader.LoaderTuple tuple) {
InputStream in = null;
try {
NvHTTP http = new NvHTTP(ServerHelper.getCurrentAddressFromComputer(tuple.computer), uniqueId, null, PlatformBinding.getCryptoProvider(context));
in = http.getBoxArt(tuple.app);
} catch (IOException ignored) {}
if (in != null) {
LimeLog.info("Network asset load complete: " + tuple);
}
else {
LimeLog.info("Network asset load failed: " + tuple);
}
return in;
}
}

View File

@@ -1,44 +0,0 @@
package com.limelight.nvstream.av.video.cpu;
public class AvcDecoder {
static {
// FFMPEG dependencies
System.loadLibrary("avutil-52");
System.loadLibrary("swresample-0");
System.loadLibrary("swscale-2");
System.loadLibrary("avcodec-55");
System.loadLibrary("avformat-55");
System.loadLibrary("nv_avc_dec");
}
/** Disables the deblocking filter at the cost of image quality */
public static final int DISABLE_LOOP_FILTER = 0x1;
/** Uses the low latency decode flag (disables multithreading) */
public static final int LOW_LATENCY_DECODE = 0x2;
/** Threads process each slice, rather than each frame */
public static final int SLICE_THREADING = 0x4;
/** Uses nonstandard speedup tricks */
public static final int FAST_DECODE = 0x8;
/** Uses bilinear filtering instead of bicubic */
public static final int BILINEAR_FILTERING = 0x10;
/** Uses a faster bilinear filtering with lower image quality */
public static final int FAST_BILINEAR_FILTERING = 0x20;
/** Disables color conversion (output is NV21) */
public static final int NO_COLOR_CONVERSION = 0x40;
public static native int init(int width, int height, int perflvl, int threadcount);
public static native void destroy();
// Rendering API when NO_COLOR_CONVERSION == 0
public static native boolean setRenderTarget(Object androidSurface);
public static native boolean getRgbFrameInt(int[] rgbFrame, int bufferSize);
public static native boolean getRgbFrame(byte[] rgbFrame, int bufferSize);
public static native boolean redraw();
// Rendering API when NO_COLOR_CONVERSION == 1
public static native boolean getRawFrame(byte[] yuvFrame, int bufferSize);
public static native int getInputPaddingSize();
public static native int decode(byte[] indata, int inoff, int inlen);
}

View File

@@ -1,150 +1,237 @@
package com.limelight.preferences;
import java.net.Inet4Address;
import java.net.InetAddress;
import java.net.InterfaceAddress;
import java.net.NetworkInterface;
import java.net.SocketException;
import java.net.UnknownHostException;
import java.util.Collections;
import java.util.concurrent.LinkedBlockingQueue;
import com.limelight.computers.ComputerManagerService;
import com.limelight.R;
import com.limelight.utils.Dialog;
import com.limelight.utils.SpinnerDialog;
import com.limelight.utils.UiHelper;
import android.app.Activity;
import android.app.Service;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.os.Bundle;
import android.os.IBinder;
import android.view.KeyEvent;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputMethodManager;
import android.widget.TextView;
import android.widget.Toast;
public class AddComputerManually extends Activity {
private Button addPcButton;
private TextView hostText;
private ComputerManagerService.ComputerManagerBinder managerBinder;
private LinkedBlockingQueue<String> computersToAdd = new LinkedBlockingQueue<String>();
private Thread addThread;
private ServiceConnection serviceConnection = new ServiceConnection() {
public void onServiceConnected(ComponentName className, final IBinder binder) {
managerBinder = ((ComputerManagerService.ComputerManagerBinder)binder);
startAddThread();
}
private TextView hostText;
private ComputerManagerService.ComputerManagerBinder managerBinder;
private final LinkedBlockingQueue<String> computersToAdd = new LinkedBlockingQueue<>();
private Thread addThread;
private final ServiceConnection serviceConnection = new ServiceConnection() {
public void onServiceConnected(ComponentName className, final IBinder binder) {
managerBinder = ((ComputerManagerService.ComputerManagerBinder)binder);
startAddThread();
}
public void onServiceDisconnected(ComponentName className) {
joinAddThread();
managerBinder = null;
}
};
private void doAddPc(String host) {
String msg;
boolean finish = false;
try {
InetAddress addr = InetAddress.getByName(host);
if (!managerBinder.addComputerBlocking(addr)){
msg = "Unable to connect to the specified computer. Make sure the required ports are allowed through the firewall.";
}
else {
msg = "Successfully added computer";
finish = true;
}
} catch (UnknownHostException e) {
msg = "Unable to resolve PC address. Make sure you didn't make a typo in the address.";
}
final boolean toastFinish = finish;
final String toastMsg = msg;
AddComputerManually.this.runOnUiThread(new Runnable() {
@Override
public void run() {
Toast.makeText(AddComputerManually.this, toastMsg, Toast.LENGTH_LONG).show();
if (toastFinish && !isFinishing()) {
// Close the activity
AddComputerManually.this.finish();
}
}
});
}
private void startAddThread() {
addThread = new Thread() {
@Override
public void run() {
while (!isInterrupted()) {
String computer;
try {
computer = computersToAdd.take();
} catch (InterruptedException e) {
return;
}
doAddPc(computer);
}
}
};
addThread.setName("UI - AddComputerManually");
addThread.start();
}
private void joinAddThread() {
if (addThread != null) {
addThread.interrupt();
try {
addThread.join();
} catch (InterruptedException ignored) {}
addThread = null;
}
}
public void onServiceDisconnected(ComponentName className) {
joinAddThread();
managerBinder = null;
}
};
@Override
protected void onStop() {
super.onStop();
Dialog.closeDialogs();
}
@Override
protected void onDestroy() {
super.onDestroy();
if (managerBinder != null) {
joinAddThread();
unbindService(serviceConnection);
}
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_add_computer_manually);
this.addPcButton = (Button) findViewById(R.id.addPc);
this.hostText = (TextView) findViewById(R.id.hostTextView);
// Bind to the ComputerManager service
bindService(new Intent(AddComputerManually.this,
ComputerManagerService.class), serviceConnection, Service.BIND_AUTO_CREATE);
addPcButton.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
if (hostText.getText().length() == 0) {
Toast.makeText(AddComputerManually.this, "You must enter an IP address", Toast.LENGTH_LONG).show();
return;
}
Toast.makeText(AddComputerManually.this, "Adding PC...", Toast.LENGTH_SHORT).show();
computersToAdd.add(hostText.getText().toString());
}
});
}
private boolean isWrongSubnetSiteLocalAddress(String address) {
try {
InetAddress targetAddress = InetAddress.getByName(address);
if (!(targetAddress instanceof Inet4Address) || !targetAddress.isSiteLocalAddress()) {
return false;
}
// We have a site-local address. Look for a matching local interface.
for (NetworkInterface iface : Collections.list(NetworkInterface.getNetworkInterfaces())) {
for (InterfaceAddress addr : iface.getInterfaceAddresses()) {
if (!(addr.getAddress() instanceof Inet4Address) || !addr.getAddress().isSiteLocalAddress()) {
// Skip non-site-local or non-IPv4 addresses
continue;
}
byte[] targetAddrBytes = targetAddress.getAddress();
byte[] ifaceAddrBytes = addr.getAddress().getAddress();
// Compare prefix to ensure it's the same
boolean addressMatches = true;
for (int i = 0; i < addr.getNetworkPrefixLength(); i++) {
if ((ifaceAddrBytes[i / 8] & (1 << (i % 8))) != (targetAddrBytes[i / 8] & (1 << (i % 8)))) {
addressMatches = false;
break;
}
}
if (addressMatches) {
return false;
}
}
}
// Couldn't find a matching interface
return true;
} catch (SocketException e) {
e.printStackTrace();
return false;
} catch (UnknownHostException e) {
return false;
}
}
private void doAddPc(String host) {
String msg;
boolean wrongSiteLocal = false;
boolean success;
SpinnerDialog dialog = SpinnerDialog.displayDialog(this, getResources().getString(R.string.title_add_pc),
getResources().getString(R.string.msg_add_pc), false);
success = managerBinder.addComputerBlocking(host, true);
if (!success){
wrongSiteLocal = isWrongSubnetSiteLocalAddress(host);
}
dialog.dismiss();
if (wrongSiteLocal) {
Dialog.displayDialog(this, getResources().getString(R.string.conn_error_title), getResources().getString(R.string.addpc_wrong_sitelocal), false);
}
else if (!success) {
Dialog.displayDialog(this, getResources().getString(R.string.conn_error_title), getResources().getString(R.string.addpc_fail), false);
}
else {
AddComputerManually.this.runOnUiThread(new Runnable() {
@Override
public void run() {
Toast.makeText(AddComputerManually.this, getResources().getString(R.string.addpc_success), Toast.LENGTH_LONG).show();
if (!isFinishing()) {
// Close the activity
AddComputerManually.this.finish();
}
}
});
}
}
private void startAddThread() {
addThread = new Thread() {
@Override
public void run() {
while (!isInterrupted()) {
String computer;
try {
computer = computersToAdd.take();
} catch (InterruptedException e) {
return;
}
doAddPc(computer);
}
}
};
addThread.setName("UI - AddComputerManually");
addThread.start();
}
private void joinAddThread() {
if (addThread != null) {
addThread.interrupt();
try {
addThread.join();
} catch (InterruptedException ignored) {}
addThread = null;
}
}
@Override
protected void onStop() {
super.onStop();
Dialog.closeDialogs();
SpinnerDialog.closeDialogs(this);
}
@Override
protected void onDestroy() {
super.onDestroy();
if (managerBinder != null) {
joinAddThread();
unbindService(serviceConnection);
}
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
UiHelper.setLocale(this);
setContentView(R.layout.activity_add_computer_manually);
UiHelper.notifyNewRootView(this);
this.hostText = findViewById(R.id.hostTextView);
hostText.setImeOptions(EditorInfo.IME_ACTION_DONE);
hostText.setOnEditorActionListener(new TextView.OnEditorActionListener() {
@Override
public boolean onEditorAction(TextView textView, int actionId, KeyEvent keyEvent) {
if (actionId == EditorInfo.IME_ACTION_DONE ||
(keyEvent != null &&
keyEvent.getAction() == KeyEvent.ACTION_DOWN &&
keyEvent.getKeyCode() == KeyEvent.KEYCODE_ENTER)) {
return handleDoneEvent();
}
else if (actionId == EditorInfo.IME_ACTION_PREVIOUS) {
// This is how the Fire TV dismisses the keyboard
InputMethodManager imm = (InputMethodManager)getSystemService(Context.INPUT_METHOD_SERVICE);
imm.hideSoftInputFromWindow(hostText.getWindowToken(), 0);
return false;
}
return false;
}
});
findViewById(R.id.addPcButton).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
handleDoneEvent();
}
});
// Bind to the ComputerManager service
bindService(new Intent(AddComputerManually.this,
ComputerManagerService.class), serviceConnection, Service.BIND_AUTO_CREATE);
}
// Returns true if the event should be eaten
private boolean handleDoneEvent() {
String hostAddress = hostText.getText().toString().trim();
if (hostAddress.length() == 0) {
Toast.makeText(AddComputerManually.this, getResources().getString(R.string.addpc_enter_ip), Toast.LENGTH_LONG).show();
return true;
}
computersToAdd.add(hostAddress);
return false;
}
}

View File

@@ -0,0 +1,41 @@
package com.limelight.preferences;
import android.annotation.TargetApi;
import android.content.Context;
import android.content.DialogInterface;
import android.content.SharedPreferences;
import android.os.Build;
import android.preference.DialogPreference;
import android.util.AttributeSet;
import android.widget.Toast;
import com.limelight.R;
import static com.limelight.binding.input.virtual_controller.VirtualControllerConfigurationLoader.OSC_PREFERENCE;
public class ConfirmDeleteOscPreference extends DialogPreference {
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
public ConfirmDeleteOscPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
public ConfirmDeleteOscPreference(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
public ConfirmDeleteOscPreference(Context context, AttributeSet attrs) {
super(context, attrs);
}
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
public ConfirmDeleteOscPreference(Context context) {
super(context);
}
public void onClick(DialogInterface dialog, int which) {
if (which == DialogInterface.BUTTON_POSITIVE) {
getContext().getSharedPreferences(OSC_PREFERENCE, Context.MODE_PRIVATE).edit().clear().apply();
Toast.makeText(getContext(), R.string.toast_reset_osc_success, Toast.LENGTH_SHORT).show();
}
}
}

View File

@@ -0,0 +1,37 @@
package com.limelight.preferences;
import android.content.Context;
import android.content.SharedPreferences;
public class GlPreferences {
private static final String PREF_NAME = "GlPreferences";
private static final String FINGERPRINT_PREF_STRING = "Fingerprint";
private static final String GL_RENDERER_PREF_STRING = "Renderer";
private SharedPreferences prefs;
public String glRenderer;
public String savedFingerprint;
private GlPreferences(SharedPreferences prefs) {
this.prefs = prefs;
}
public static GlPreferences readPreferences(Context context) {
SharedPreferences prefs = context.getSharedPreferences(PREF_NAME, 0);
GlPreferences glPrefs = new GlPreferences(prefs);
glPrefs.glRenderer = prefs.getString(GL_RENDERER_PREF_STRING, "");
glPrefs.savedFingerprint = prefs.getString(FINGERPRINT_PREF_STRING, "");
return glPrefs;
}
public boolean writePreferences() {
return prefs.edit()
.putString(GL_RENDERER_PREF_STRING, glRenderer)
.putString(FINGERPRINT_PREF_STRING, savedFingerprint)
.commit();
}
}

View File

@@ -2,139 +2,318 @@ package com.limelight.preferences;
import android.content.Context;
import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.os.Build;
import android.preference.PreferenceManager;
public class PreferenceConfiguration {
static final String RES_FPS_PREF_STRING = "list_resolution_fps";
private static final String DECODER_PREF_STRING = "list_decoders";
static final String BITRATE_PREF_STRING = "seekbar_bitrate";
private static final String LEGACY_RES_FPS_PREF_STRING = "list_resolution_fps";
static final String RESOLUTION_PREF_STRING = "list_resolution";
static final String FPS_PREF_STRING = "list_fps";
static final String BITRATE_PREF_STRING = "seekbar_bitrate_kbps";
private static final String BITRATE_PREF_OLD_STRING = "seekbar_bitrate";
private static final String STRETCH_PREF_STRING = "checkbox_stretch_video";
private static final String SOPS_PREF_STRING = "checkbox_enable_sops";
private static final String DISABLE_TOASTS_PREF_STRING = "checkbox_disable_warnings";
private static final String HOST_AUDIO_PREF_STRING = "checkbox_host_audio";
private static final String DEADZONE_PREF_STRING = "seekbar_deadzone";
private static final String LANGUAGE_PREF_STRING = "list_languages";
private static final String LIST_MODE_PREF_STRING = "checkbox_list_mode";
private static final String SMALL_ICONS_PREF_STRING = "checkbox_small_icon_mode";
private static final String MULTI_CONTROLLER_PREF_STRING = "checkbox_multi_controller";
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 String ONLY_L3_R3_PREF_STRING = "checkbox_only_show_L3R3";
private static final String DISABLE_FRAME_DROP_PREF_STRING = "checkbox_disable_frame_drop";
private static final String ENABLE_HDR_PREF_STRING = "checkbox_enable_hdr";
private static final String ENABLE_PIP_PREF_STRING = "checkbox_enable_pip";
private static final String BIND_ALL_USB_STRING = "checkbox_usb_bind_all";
private static final String MOUSE_EMULATION_STRING = "checkbox_mouse_emulation";
private static final String MOUSE_NAV_BUTTONS_STRING = "checkbox_mouse_nav_buttons";
static final String UNLOCK_FPS_STRING = "checkbox_unlock_fps";
private static final int BITRATE_DEFAULT_720_30 = 5;
private static final int BITRATE_DEFAULT_720_60 = 10;
private static final int BITRATE_DEFAULT_1080_30 = 10;
private static final int BITRATE_DEFAULT_1080_60 = 30;
private static final String DEFAULT_RES_FPS = "720p60";
private static final String DEFAULT_DECODER = "auto";
private static final int DEFAULT_BITRATE = BITRATE_DEFAULT_720_60;
static final String DEFAULT_RESOLUTION = "720p";
static final String DEFAULT_FPS = "60";
private static final boolean DEFAULT_STRETCH = false;
private static final boolean DEFAULT_SOPS = true;
private static final boolean DEFAULT_DISABLE_TOASTS = false;
private static final boolean DEFAULT_HOST_AUDIO = false;
private static final int DEFAULT_DEADZONE = 15;
public static final String DEFAULT_LANGUAGE = "default";
private static final boolean DEFAULT_LIST_MODE = false;
private static final boolean DEFAULT_MULTI_CONTROLLER = true;
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;
private static final boolean ONLY_L3_R3_DEFAULT = false;
private static final boolean DEFAULT_DISABLE_FRAME_DROP = false;
private static final boolean DEFAULT_ENABLE_HDR = false;
private static final boolean DEFAULT_ENABLE_PIP = false;
private static final boolean DEFAULT_BIND_ALL_USB = false;
private static final boolean DEFAULT_MOUSE_EMULATION = true;
private static final boolean DEFAULT_MOUSE_NAV_BUTTONS = false;
private static final boolean DEFAULT_UNLOCK_FPS = false;
public static final int FORCE_HARDWARE_DECODER = -1;
public static final int AUTOSELECT_DECODER = 0;
public static final int FORCE_SOFTWARE_DECODER = 1;
public static final int FORCE_H265_ON = -1;
public static final int AUTOSELECT_H265 = 0;
public static final int FORCE_H265_OFF = 1;
public int width, height, fps;
public int bitrate;
public int decoder;
public int videoFormat;
public int deadzonePercentage;
public boolean stretchVideo, enableSops, playHostAudio, disableWarnings;
public String language;
public boolean listMode, smallIconMode, multiController, enable51Surround, usbDriver;
public boolean onscreenController;
public boolean onlyL3R3;
public boolean disableFrameDrop;
public boolean enableHdr;
public boolean enablePip;
public boolean bindAllUsb;
public boolean mouseEmulation;
public boolean mouseNavButtons;
public boolean unlockFps;
public static int getDefaultBitrate(String resFpsString) {
if (resFpsString.equals("720p30")) {
return BITRATE_DEFAULT_720_30;
private static int getHeightFromResolutionString(String resString) {
if (resString.equalsIgnoreCase("360p")) {
return 360;
}
else if (resFpsString.equals("720p60")) {
return BITRATE_DEFAULT_720_60;
else if (resString.equalsIgnoreCase("720p")) {
return 720;
}
else if (resFpsString.equals("1080p30")) {
return BITRATE_DEFAULT_1080_30;
else if (resString.equalsIgnoreCase("1080p")) {
return 1080;
}
else if (resFpsString.equals("1080p60")) {
return BITRATE_DEFAULT_1080_60;
else if (resString.equalsIgnoreCase("1440p")) {
return 1440;
}
else if (resString.equalsIgnoreCase("4K")) {
return 2160;
}
else {
// Should never get here
return DEFAULT_BITRATE;
// Should be unreachable
return 720;
}
}
private static int getWidthFromResolutionString(String resString) {
return (getHeightFromResolutionString(resString) * 16) / 9;
}
private static String getResolutionString(int width, int height) {
switch (height) {
case 360:
return "360p";
default:
case 720:
return "720p";
case 1080:
return "1080p";
case 1440:
return "1440p";
case 2160:
return "4K";
}
}
public static int getDefaultBitrate(String resString, String fpsString) {
int width = getWidthFromResolutionString(resString);
int height = getHeightFromResolutionString(resString);
int fps = Integer.parseInt(fpsString);
// This table prefers 16:10 resolutions because they are
// only slightly more pixels than the 16:9 equivalents, so
// we don't want to bump those 16:10 resolutions up to the
// next 16:9 slot.
//
// This logic is shamelessly stolen from Moonlight Qt:
// https://github.com/moonlight-stream/moonlight-qt/blob/master/app/settings/streamingpreferences.cpp
if (width * height <= 640 * 360) {
return (int)(1000 * (fps / 30.0));
}
// This covers 1280x720 and 1280x800 too
else if (width * height <= 1366 * 768) {
return (int)(5000 * (fps / 30.0));
}
else if (width * height <= 1920 * 1200) {
return (int)(10000 * (fps / 30.0));
}
else if (width * height <= 2560 * 1600) {
return (int)(20000 * (fps / 30.0));
}
else /* if (width * height <= 3840 * 2160) */ {
return (int)(40000 * (fps / 30.0));
}
}
public static boolean getDefaultSmallMode(Context context) {
PackageManager manager = context.getPackageManager();
if (manager != null) {
// TVs shouldn't use small mode by default
if (manager.hasSystemFeature(PackageManager.FEATURE_TELEVISION)) {
return false;
}
// API 21 uses LEANBACK instead of TELEVISION
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP_MR1) {
if (manager.hasSystemFeature(PackageManager.FEATURE_LEANBACK)) {
return false;
}
}
}
// Use small mode on anything smaller than a 7" tablet
return context.getResources().getConfiguration().smallestScreenWidthDp < 500;
}
public static int getDefaultBitrate(Context context) {
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
return getDefaultBitrate(
prefs.getString(RESOLUTION_PREF_STRING, DEFAULT_RESOLUTION),
prefs.getString(FPS_PREF_STRING, DEFAULT_FPS));
}
String str = prefs.getString(RES_FPS_PREF_STRING, DEFAULT_RES_FPS);
if (str.equals("720p30")) {
return BITRATE_DEFAULT_720_30;
private static int getVideoFormatValue(Context context) {
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
String str = prefs.getString(VIDEO_FORMAT_PREF_STRING, DEFAULT_VIDEO_FORMAT);
if (str.equals("auto")) {
return AUTOSELECT_H265;
}
else if (str.equals("720p60")) {
return BITRATE_DEFAULT_720_60;
else if (str.equals("forceh265")) {
return FORCE_H265_ON;
}
else if (str.equals("1080p30")) {
return BITRATE_DEFAULT_1080_30;
}
else if (str.equals("1080p60")) {
return BITRATE_DEFAULT_1080_60;
else if (str.equals("neverh265")) {
return FORCE_H265_OFF;
}
else {
// Should never get here
return DEFAULT_BITRATE;
return AUTOSELECT_H265;
}
}
private static int getDecoderValue(Context context) {
public static void resetStreamingSettings(Context context) {
// We consider resolution, FPS, bitrate, HDR, and video format as "streaming settings" here
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
String str = prefs.getString(DECODER_PREF_STRING, DEFAULT_DECODER);
if (str.equals("auto")) {
return AUTOSELECT_DECODER;
}
else if (str.equals("software")) {
return FORCE_SOFTWARE_DECODER;
}
else if (str.equals("hardware")) {
return FORCE_HARDWARE_DECODER;
}
else {
// Should never get here
return AUTOSELECT_DECODER;
}
prefs.edit()
.remove(BITRATE_PREF_STRING)
.remove(BITRATE_PREF_OLD_STRING)
.remove(LEGACY_RES_FPS_PREF_STRING)
.remove(RESOLUTION_PREF_STRING)
.remove(FPS_PREF_STRING)
.remove(VIDEO_FORMAT_PREF_STRING)
.remove(ENABLE_HDR_PREF_STRING)
.remove(UNLOCK_FPS_STRING)
.apply();
}
public static PreferenceConfiguration readPreferences(Context context) {
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
PreferenceConfiguration config = new PreferenceConfiguration();
config.bitrate = prefs.getInt(BITRATE_PREF_STRING, getDefaultBitrate(context));
String str = prefs.getString(RES_FPS_PREF_STRING, DEFAULT_RES_FPS);
if (str.equals("720p30")) {
config.width = 1280;
config.height = 720;
config.fps = 30;
}
else if (str.equals("720p60")) {
config.width = 1280;
config.height = 720;
config.fps = 60;
}
else if (str.equals("1080p30")) {
config.width = 1920;
config.height = 1080;
config.fps = 30;
}
else if (str.equals("1080p60")) {
config.width = 1920;
config.height = 1080;
config.fps = 60;
// Migrate legacy preferences to the new locations
String str = prefs.getString(LEGACY_RES_FPS_PREF_STRING, null);
if (str != null) {
if (str.equals("360p30")) {
config.width = 640;
config.height = 360;
config.fps = 30;
}
else if (str.equals("360p60")) {
config.width = 640;
config.height = 360;
config.fps = 60;
}
else if (str.equals("720p30")) {
config.width = 1280;
config.height = 720;
config.fps = 30;
}
else if (str.equals("720p60")) {
config.width = 1280;
config.height = 720;
config.fps = 60;
}
else if (str.equals("1080p30")) {
config.width = 1920;
config.height = 1080;
config.fps = 30;
}
else if (str.equals("1080p60")) {
config.width = 1920;
config.height = 1080;
config.fps = 60;
}
else if (str.equals("4K30")) {
config.width = 3840;
config.height = 2160;
config.fps = 30;
}
else if (str.equals("4K60")) {
config.width = 3840;
config.height = 2160;
config.fps = 60;
}
else {
// Should never get here
config.width = 1280;
config.height = 720;
config.fps = 60;
}
prefs.edit()
.remove(LEGACY_RES_FPS_PREF_STRING)
.putString(RESOLUTION_PREF_STRING, getResolutionString(config.width, config.height))
.putString(FPS_PREF_STRING, ""+config.fps)
.apply();
}
else {
// Should never get here
config.width = 1280;
config.height = 720;
config.fps = 60;
// Use the new preference location
String resStr = prefs.getString(RESOLUTION_PREF_STRING, PreferenceConfiguration.DEFAULT_RESOLUTION);
config.width = PreferenceConfiguration.getWidthFromResolutionString(resStr);
config.height = PreferenceConfiguration.getHeightFromResolutionString(resStr);
config.fps = Integer.parseInt(prefs.getString(FPS_PREF_STRING, PreferenceConfiguration.DEFAULT_FPS));
}
config.decoder = getDecoderValue(context);
// This must happen after the preferences migration to ensure the preferences are populated
config.bitrate = prefs.getInt(BITRATE_PREF_STRING, prefs.getInt(BITRATE_PREF_OLD_STRING, 0) * 1000);
if (config.bitrate == 0) {
config.bitrate = getDefaultBitrate(context);
}
config.videoFormat = getVideoFormatValue(context);
config.deadzonePercentage = prefs.getInt(DEADZONE_PREF_STRING, DEFAULT_DEADZONE);
config.language = prefs.getString(LANGUAGE_PREF_STRING, DEFAULT_LANGUAGE);
// Checkbox preferences
config.disableWarnings = prefs.getBoolean(DISABLE_TOASTS_PREF_STRING, DEFAULT_DISABLE_TOASTS);
config.enableSops = prefs.getBoolean(SOPS_PREF_STRING, DEFAULT_SOPS);
config.stretchVideo = prefs.getBoolean(STRETCH_PREF_STRING, DEFAULT_STRETCH);
config.playHostAudio = prefs.getBoolean(HOST_AUDIO_PREF_STRING, DEFAULT_HOST_AUDIO);
config.listMode = prefs.getBoolean(LIST_MODE_PREF_STRING, DEFAULT_LIST_MODE);
config.smallIconMode = prefs.getBoolean(SMALL_ICONS_PREF_STRING, getDefaultSmallMode(context));
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);
config.onlyL3R3 = prefs.getBoolean(ONLY_L3_R3_PREF_STRING, ONLY_L3_R3_DEFAULT);
config.disableFrameDrop = prefs.getBoolean(DISABLE_FRAME_DROP_PREF_STRING, DEFAULT_DISABLE_FRAME_DROP);
config.enableHdr = prefs.getBoolean(ENABLE_HDR_PREF_STRING, DEFAULT_ENABLE_HDR);
config.enablePip = prefs.getBoolean(ENABLE_PIP_PREF_STRING, DEFAULT_ENABLE_PIP);
config.bindAllUsb = prefs.getBoolean(BIND_ALL_USB_STRING, DEFAULT_BIND_ALL_USB);
config.mouseEmulation = prefs.getBoolean(MOUSE_EMULATION_STRING, DEFAULT_MOUSE_EMULATION);
config.mouseNavButtons = prefs.getBoolean(MOUSE_NAV_BUTTONS_STRING, DEFAULT_MOUSE_NAV_BUTTONS);
config.unlockFps = prefs.getBoolean(UNLOCK_FPS_STRING, DEFAULT_UNLOCK_FPS);
return config;
}

View File

@@ -16,40 +16,48 @@ import android.widget.TextView;
// Based on a Stack Overflow example: http://stackoverflow.com/questions/1974193/slider-on-my-preferencescreen
public class SeekBarPreference extends DialogPreference
{
private static final String SCHEMA_URL = "http://schemas.android.com/apk/res/android";
private static final String ANDROID_SCHEMA_URL = "http://schemas.android.com/apk/res/android";
private static final String SEEKBAR_SCHEMA_URL = "http://schemas.moonlight-stream.com/apk/res/seekbar";
private SeekBar seekBar;
private TextView splashText, valueText;
private Context context;
private TextView valueText;
private final Context context;
private String dialogMessage, suffix;
private int defaultValue, maxValue, currentValue;
private final String dialogMessage;
private final String suffix;
private final int defaultValue;
private final int maxValue;
private final int minValue;
private final int stepSize;
private int currentValue;
public SeekBarPreference(Context context, AttributeSet attrs) {
super(context, attrs);
this.context = context;
// Read the message from XML
int dialogMessageId = attrs.getAttributeResourceValue(SCHEMA_URL, "dialogMessage", 0);
int dialogMessageId = attrs.getAttributeResourceValue(ANDROID_SCHEMA_URL, "dialogMessage", 0);
if (dialogMessageId == 0) {
dialogMessage = attrs.getAttributeValue(SCHEMA_URL, "dialogMessage");
dialogMessage = attrs.getAttributeValue(ANDROID_SCHEMA_URL, "dialogMessage");
}
else {
dialogMessage = context.getString(dialogMessageId);
}
// Get the suffix for the number displayed in the dialog
int suffixId = attrs.getAttributeResourceValue(SCHEMA_URL, "text", 0);
int suffixId = attrs.getAttributeResourceValue(ANDROID_SCHEMA_URL, "text", 0);
if (suffixId == 0) {
suffix = attrs.getAttributeValue(SCHEMA_URL, "text");
suffix = attrs.getAttributeValue(ANDROID_SCHEMA_URL, "text");
}
else {
suffix = context.getString(suffixId);
}
// Get default and max seekbar values
defaultValue = PreferenceConfiguration.getDefaultBitrate(context);
maxValue = attrs.getAttributeIntValue(SCHEMA_URL, "max", 100);
// Get default, min, and max seekbar values
defaultValue = attrs.getAttributeIntValue(ANDROID_SCHEMA_URL, "defaultValue", PreferenceConfiguration.getDefaultBitrate(context));
maxValue = attrs.getAttributeIntValue(ANDROID_SCHEMA_URL, "max", 100);
minValue = attrs.getAttributeIntValue(SEEKBAR_SCHEMA_URL, "min", 1);
stepSize = attrs.getAttributeIntValue(SEEKBAR_SCHEMA_URL, "step", 1);
}
@Override
@@ -60,7 +68,7 @@ public class SeekBarPreference extends DialogPreference
layout.setOrientation(LinearLayout.VERTICAL);
layout.setPadding(6, 6, 6, 6);
splashText = new TextView(context);
TextView splashText = new TextView(context);
splashText.setPadding(30, 10, 30, 10);
if (dialogMessage != null) {
splashText.setText(dialogMessage);
@@ -71,7 +79,7 @@ public class SeekBarPreference extends DialogPreference
valueText.setGravity(Gravity.CENTER_HORIZONTAL);
valueText.setTextSize(32);
params = new LinearLayout.LayoutParams(
LinearLayout.LayoutParams.FILL_PARENT,
LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.WRAP_CONTENT);
layout.addView(valueText, params);
@@ -79,8 +87,19 @@ public class SeekBarPreference extends DialogPreference
seekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
@Override
public void onProgressChanged(SeekBar seekBar, int value, boolean b) {
if (value < minValue) {
seekBar.setProgress(minValue);
return;
}
int roundedValue = ((value + (stepSize - 1))/stepSize)*stepSize;
if (roundedValue != value) {
seekBar.setProgress(roundedValue);
return;
}
String t = String.valueOf(value);
valueText.setText(suffix == null ? t : t.concat(" " + suffix));
valueText.setText(suffix == null ? t : t.concat(suffix.length() > 1 ? " "+suffix : suffix));
}
@Override
@@ -90,7 +109,7 @@ public class SeekBarPreference extends DialogPreference
public void onStopTrackingTouch(SeekBar seekBar) {}
});
layout.addView(seekBar, new LinearLayout.LayoutParams(LinearLayout.LayoutParams.FILL_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT));
layout.addView(seekBar, new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT));
if (shouldPersist()) {
currentValue = getPersistedInt(defaultValue);
@@ -121,13 +140,6 @@ public class SeekBarPreference extends DialogPreference
}
}
public void setMax(int max) {
this.maxValue = max;
}
public int getMax() {
return this.maxValue;
}
public void setProgress(int progress) {
this.currentValue = progress;
if (seekBar != null) {
@@ -149,10 +161,10 @@ public class SeekBarPreference extends DialogPreference
if (shouldPersist()) {
currentValue = seekBar.getProgress();
persistInt(seekBar.getProgress());
callChangeListener(Integer.valueOf(seekBar.getProgress()));
callChangeListener(seekBar.getProgress());
}
((AlertDialog) getDialog()).dismiss();
getDialog().dismiss();
}
});
}

View File

@@ -0,0 +1,21 @@
package com.limelight.preferences;
import android.content.Context;
import android.content.res.TypedArray;
import android.preference.CheckBoxPreference;
import android.util.AttributeSet;
public class SmallIconCheckboxPreference extends CheckBoxPreference {
public SmallIconCheckboxPreference(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
public SmallIconCheckboxPreference(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
protected Object onGetDefaultValue(TypedArray a, int index) {
return PreferenceConfiguration.getDefaultSmallMode(getContext());
}
}

View File

@@ -1,46 +1,381 @@
package com.limelight.preferences;
import android.content.Intent;
import android.content.SharedPreferences;
import android.media.MediaCodecInfo;
import android.os.Build;
import android.os.Bundle;
import android.app.Activity;
import android.os.Handler;
import android.preference.ListPreference;
import android.preference.Preference;
import android.preference.PreferenceCategory;
import android.preference.PreferenceFragment;
import android.preference.PreferenceManager;
import android.preference.PreferenceScreen;
import android.util.Range;
import android.view.Display;
import com.limelight.LimeLog;
import com.limelight.PcView;
import com.limelight.R;
import com.limelight.binding.video.MediaCodecHelper;
import com.limelight.utils.UiHelper;
public class StreamSettings extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
private PreferenceConfiguration previousPrefs;
setContentView(R.layout.activity_stream_settings);
void reloadSettings() {
getFragmentManager().beginTransaction().replace(
R.id.stream_settings, new SettingsFragment()
).commit();
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
previousPrefs = PreferenceConfiguration.readPreferences(this);
UiHelper.setLocale(this);
setContentView(R.layout.activity_stream_settings);
reloadSettings();
UiHelper.notifyNewRootView(this);
}
@Override
public void onBackPressed() {
finish();
// Check for changes that require a UI reload to take effect
PreferenceConfiguration newPrefs = PreferenceConfiguration.readPreferences(this);
if (newPrefs.listMode != previousPrefs.listMode ||
newPrefs.smallIconMode != previousPrefs.smallIconMode ||
!newPrefs.language.equals(previousPrefs.language)) {
// Restart the PC view to apply UI changes
Intent intent = new Intent(this, PcView.class);
intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK | Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(intent, null);
}
}
public static class SettingsFragment extends PreferenceFragment {
private void setValue(String preferenceKey, String value) {
ListPreference pref = (ListPreference) findPreference(preferenceKey);
pref.setValue(value);
}
private void removeValue(String preferenceKey, String value, Runnable onMatched) {
int matchingCount = 0;
ListPreference pref = (ListPreference) findPreference(preferenceKey);
// Count the number of matching entries we'll be removing
for (CharSequence seq : pref.getEntryValues()) {
if (seq.toString().equalsIgnoreCase(value)) {
matchingCount++;
}
}
// Create the new arrays
CharSequence[] entries = new CharSequence[pref.getEntries().length-matchingCount];
CharSequence[] entryValues = new CharSequence[pref.getEntryValues().length-matchingCount];
int outIndex = 0;
for (int i = 0; i < pref.getEntryValues().length; i++) {
if (pref.getEntryValues()[i].toString().equalsIgnoreCase(value)) {
// Skip matching values
continue;
}
entries[outIndex] = pref.getEntries()[i];
entryValues[outIndex] = pref.getEntryValues()[i];
outIndex++;
}
if (pref.getValue().equalsIgnoreCase(value)) {
onMatched.run();
}
// Update the preference with the new list
pref.setEntries(entries);
pref.setEntryValues(entryValues);
}
private void resetBitrateToDefault(SharedPreferences prefs, String res, String fps) {
if (res == null) {
res = prefs.getString(PreferenceConfiguration.RESOLUTION_PREF_STRING, PreferenceConfiguration.DEFAULT_RESOLUTION);
}
if (fps == null) {
fps = prefs.getString(PreferenceConfiguration.FPS_PREF_STRING, PreferenceConfiguration.DEFAULT_FPS);
}
prefs.edit()
.putInt(PreferenceConfiguration.BITRATE_PREF_STRING,
PreferenceConfiguration.getDefaultBitrate(res, fps))
.apply();
}
@Override
public void onCreate(Bundle savedInstanceState) {
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);
}
// Remove PiP mode on devices pre-Oreo
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
PreferenceCategory category =
(PreferenceCategory) findPreference("category_basic_settings");
category.removePreference(findPreference("checkbox_enable_pip"));
}
int maxSupportedFps = 0;
// Hide non-supported resolution/FPS combinations
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
Display display = getActivity().getWindowManager().getDefaultDisplay();
int maxSupportedResW = 0;
// Always allow resolutions that are smaller or equal to the active
// display resolution because decoders can report total non-sense to us.
// For example, a p201 device reports:
// AVC Decoder: OMX.amlogic.avc.decoder.awesome
// HEVC Decoder: OMX.amlogic.hevc.decoder.awesome
// AVC supported width range: 64 - 384
// HEVC supported width range: 64 - 544
for (Display.Mode candidate : display.getSupportedModes()) {
// Some devices report their dimensions in the portrait orientation
// where height > width. Normalize these to the conventional width > height
// arrangement before we process them.
int width = Math.max(candidate.getPhysicalWidth(), candidate.getPhysicalHeight());
int height = Math.min(candidate.getPhysicalWidth(), candidate.getPhysicalHeight());
if ((width >= 3840 || height >= 2160) && maxSupportedResW < 3840) {
maxSupportedResW = 3840;
}
else if ((width >= 2560 || height >= 1440) && maxSupportedResW < 2560) {
maxSupportedResW = 2560;
}
else if ((width >= 1920 || height >= 1080) && maxSupportedResW < 1920) {
maxSupportedResW = 1920;
}
if (candidate.getRefreshRate() > maxSupportedFps) {
maxSupportedFps = (int)candidate.getRefreshRate();
}
}
// This must be called to do runtime initialization before calling functions that evaluate
// decoder lists.
MediaCodecHelper.initialize(getContext(), GlPreferences.readPreferences(getContext()).glRenderer);
MediaCodecInfo avcDecoder = MediaCodecHelper.findProbableSafeDecoder("video/avc", -1);
MediaCodecInfo hevcDecoder = MediaCodecHelper.findProbableSafeDecoder("video/hevc", -1);
if (avcDecoder != null) {
Range<Integer> avcWidthRange = avcDecoder.getCapabilitiesForType("video/avc").getVideoCapabilities().getSupportedWidths();
LimeLog.info("AVC supported width range: "+avcWidthRange.getLower()+" - "+avcWidthRange.getUpper());
// If 720p is not reported as supported, ignore all results from this API
if (avcWidthRange.contains(1280)) {
if (avcWidthRange.contains(3840) && maxSupportedResW < 3840) {
maxSupportedResW = 3840;
}
else if (avcWidthRange.contains(1920) && maxSupportedResW < 1920) {
maxSupportedResW = 1920;
}
else if (maxSupportedResW < 1280) {
maxSupportedResW = 1280;
}
}
}
if (hevcDecoder != null) {
Range<Integer> hevcWidthRange = hevcDecoder.getCapabilitiesForType("video/hevc").getVideoCapabilities().getSupportedWidths();
LimeLog.info("HEVC supported width range: "+hevcWidthRange.getLower()+" - "+hevcWidthRange.getUpper());
// If 720p is not reported as supported, ignore all results from this API
if (hevcWidthRange.contains(1280)) {
if (hevcWidthRange.contains(3840) && maxSupportedResW < 3840) {
maxSupportedResW = 3840;
}
else if (hevcWidthRange.contains(1920) && maxSupportedResW < 1920) {
maxSupportedResW = 1920;
}
else if (maxSupportedResW < 1280) {
maxSupportedResW = 1280;
}
}
}
LimeLog.info("Maximum resolution slot: "+maxSupportedResW);
if (maxSupportedResW != 0) {
if (maxSupportedResW < 3840) {
// 4K is unsupported
removeValue(PreferenceConfiguration.RESOLUTION_PREF_STRING, "4K", new Runnable() {
@Override
public void run() {
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(SettingsFragment.this.getActivity());
setValue(PreferenceConfiguration.RESOLUTION_PREF_STRING, "1440p");
resetBitrateToDefault(prefs, null, null);
}
});
}
if (maxSupportedResW < 2560) {
// 1440p is unsupported
removeValue(PreferenceConfiguration.RESOLUTION_PREF_STRING, "1440p", new Runnable() {
@Override
public void run() {
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(SettingsFragment.this.getActivity());
setValue(PreferenceConfiguration.RESOLUTION_PREF_STRING, "1080p");
resetBitrateToDefault(prefs, null, null);
}
});
}
if (maxSupportedResW < 1920) {
// 1080p is unsupported
removeValue(PreferenceConfiguration.RESOLUTION_PREF_STRING, "1080p", new Runnable() {
@Override
public void run() {
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(SettingsFragment.this.getActivity());
setValue(PreferenceConfiguration.RESOLUTION_PREF_STRING, "720p");
resetBitrateToDefault(prefs, null, null);
}
});
}
// Never remove 720p
}
}
if (!PreferenceConfiguration.readPreferences(this.getActivity()).unlockFps) {
// We give some extra room in case the FPS is rounded down
if (maxSupportedFps < 118) {
removeValue(PreferenceConfiguration.FPS_PREF_STRING, "120", new Runnable() {
@Override
public void run() {
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(SettingsFragment.this.getActivity());
setValue(PreferenceConfiguration.FPS_PREF_STRING, "90");
resetBitrateToDefault(prefs, null, null);
}
});
}
if (maxSupportedFps < 88) {
// 1080p is unsupported
removeValue(PreferenceConfiguration.FPS_PREF_STRING, "90", new Runnable() {
@Override
public void run() {
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(SettingsFragment.this.getActivity());
setValue(PreferenceConfiguration.FPS_PREF_STRING, "60");
resetBitrateToDefault(prefs, null, null);
}
});
}
// Never remove 30 FPS or 60 FPS
}
// Android L introduces the drop duplicate behavior of releaseOutputBuffer()
// that the unlock FPS option relies on to not massively increase latency.
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
LimeLog.info("Excluding unlock FPS toggle based on OS");
PreferenceCategory category =
(PreferenceCategory) findPreference("category_basic_settings");
category.removePreference(findPreference("checkbox_unlock_fps"));
}
else {
findPreference(PreferenceConfiguration.UNLOCK_FPS_STRING).setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
@Override
public boolean onPreferenceChange(Preference preference, Object newValue) {
// HACK: We need to let the preference change succeed before reinitializing to ensure
// it's reflected in the new layout.
final Handler h = new Handler();
h.postDelayed(new Runnable() {
@Override
public void run() {
// Ensure the activity is still open when this timeout expires
StreamSettings settingsActivity = (StreamSettings)SettingsFragment.this.getActivity();
if (settingsActivity != null) {
settingsActivity.reloadSettings();
}
}
}, 500);
// Allow the original preference change to take place
return true;
}
});
}
// Remove HDR preference for devices below Nougat
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
LimeLog.info("Excluding HDR toggle based on OS");
PreferenceCategory category =
(PreferenceCategory) findPreference("category_advanced_settings");
category.removePreference(findPreference("checkbox_enable_hdr"));
}
else {
Display display = getActivity().getWindowManager().getDefaultDisplay();
Display.HdrCapabilities hdrCaps = display.getHdrCapabilities();
// We must now ensure our display is compatible with HDR10
boolean foundHdr10 = false;
if (hdrCaps != null) {
// getHdrCapabilities() returns null on Lenovo Lenovo Mirage Solo (vega), Android 8.0
for (int hdrType : hdrCaps.getSupportedHdrTypes()) {
if (hdrType == Display.HdrCapabilities.HDR_TYPE_HDR10) {
foundHdr10 = true;
}
}
}
if (!foundHdr10) {
LimeLog.info("Excluding HDR toggle based on display capabilities");
PreferenceCategory category =
(PreferenceCategory) findPreference("category_advanced_settings");
category.removePreference(findPreference("checkbox_enable_hdr"));
}
}
// Add a listener to the FPS and resolution preference
// so the bitrate can be auto-adjusted
Preference pref = findPreference(PreferenceConfiguration.RES_FPS_PREF_STRING);
pref.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
findPreference(PreferenceConfiguration.RESOLUTION_PREF_STRING).setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
@Override
public boolean onPreferenceChange(Preference preference, Object newValue) {
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(SettingsFragment.this.getActivity());
String valueStr = (String) newValue;
// Write the new bitrate value
prefs.edit()
.putInt(PreferenceConfiguration.BITRATE_PREF_STRING,
PreferenceConfiguration.getDefaultBitrate(valueStr))
.apply();
resetBitrateToDefault(prefs, valueStr, null);
// Allow the original preference change to take place
return true;
}
});
findPreference(PreferenceConfiguration.FPS_PREF_STRING).setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
@Override
public boolean onPreferenceChange(Preference preference, Object newValue) {
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(SettingsFragment.this.getActivity());
String valueStr = (String) newValue;
// Write the new bitrate value
resetBitrateToDefault(prefs, null, valueStr);
// Allow the original preference change to take place
return true;

View File

@@ -0,0 +1,35 @@
package com.limelight.ui;
import android.app.Activity;
import android.app.Fragment;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AbsListView;
import com.limelight.R;
public class AdapterFragment extends Fragment {
private AdapterFragmentCallbacks callbacks;
@Override
public void onAttach(Activity activity) {
super.onAttach(activity);
callbacks = (AdapterFragmentCallbacks) activity;
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
return inflater.inflate(callbacks.getAdapterFragmentLayoutId(), container, false);
}
@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
callbacks.receiveAbsListView((AbsListView) getView().findViewById(R.id.fragmentView));
}
}

View File

@@ -0,0 +1,8 @@
package com.limelight.ui;
import android.widget.AbsListView;
public interface AdapterFragmentCallbacks {
int getAdapterFragmentLayoutId();
void receiveAbsListView(AbsListView gridView);
}

View File

@@ -0,0 +1,5 @@
package com.limelight.ui;
public interface GameGestures {
void showKeyboard();
}

View File

@@ -0,0 +1,86 @@
package com.limelight.ui;
import android.annotation.TargetApi;
import android.content.Context;
import android.util.AttributeSet;
import android.view.KeyEvent;
import android.view.SurfaceView;
public class StreamView extends SurfaceView {
private double desiredAspectRatio;
private InputCallbacks inputCallbacks;
public void setDesiredAspectRatio(double aspectRatio) {
this.desiredAspectRatio = aspectRatio;
}
public void setInputCallbacks(InputCallbacks callbacks) {
this.inputCallbacks = callbacks;
}
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);
}
@Override
public boolean onKeyPreIme(int keyCode, KeyEvent event) {
// This callbacks allows us to override dumb IME behavior like when
// Samsung's default keyboard consumes Shift+Space.
if (inputCallbacks != null) {
if (event.getAction() == KeyEvent.ACTION_DOWN) {
if (inputCallbacks.handleKeyDown(event)) {
return true;
}
}
else if (event.getAction() == KeyEvent.ACTION_UP) {
if (inputCallbacks.handleKeyUp(event)) {
return true;
}
}
}
return super.onKeyPreIme(keyCode, event);
}
public interface InputCallbacks {
boolean handleKeyUp(KeyEvent event);
boolean handleKeyDown(KeyEvent event);
}
}

View File

@@ -0,0 +1,86 @@
package com.limelight.utils;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.Reader;
public class CacheHelper {
public static File openPath(boolean createPath, File root, String... path) {
File f = root;
for (int i = 0; i < path.length; i++) {
String component = path[i];
if (i == path.length - 1) {
// This is the file component so now we create parent directories
if (createPath) {
f.mkdirs();
}
}
f = new File(f, component);
}
return f;
}
public static long getFileSize(File root, String... path) {
return openPath(false, root, path).length();
}
public static boolean deleteCacheFile(File root, String... path) {
return openPath(false, root, path).delete();
}
public static boolean cacheFileExists(File root, String... path) {
return openPath(false, root, path).exists();
}
public static InputStream openCacheFileForInput(File root, String... path) throws FileNotFoundException {
return new BufferedInputStream(new FileInputStream(openPath(false, root, path)));
}
public static OutputStream openCacheFileForOutput(File root, String... path) throws FileNotFoundException {
return new BufferedOutputStream(new FileOutputStream(openPath(true, root, path)));
}
public static void writeInputStreamToOutputStream(InputStream in, OutputStream out, long maxLength) throws IOException {
byte[] buf = new byte[4096];
int bytesRead;
while ((bytesRead = in.read(buf)) != -1) {
maxLength -= bytesRead;
if (maxLength <= 0) {
throw new IOException("Stream exceeded max size");
}
out.write(buf, 0, bytesRead);
}
}
public static String readInputStreamToString(InputStream in) throws IOException {
Reader r = new InputStreamReader(in);
StringBuilder sb = new StringBuilder();
char[] buf = new char[256];
int bytesRead;
while ((bytesRead = r.read(buf)) != -1) {
sb.append(buf, 0, bytesRead);
}
try {
in.close();
} catch (IOException ignored) {}
return sb.toString();
}
public static void writeStringToOutputStream(OutputStream out, String str) throws IOException {
out.write(str.getBytes("UTF-8"));
}
}

View File

@@ -5,72 +5,109 @@ import java.util.ArrayList;
import android.app.Activity;
import android.app.AlertDialog;
import android.content.DialogInterface;
import android.widget.Button;
import com.limelight.R;
public class Dialog implements Runnable {
private String title, message;
private Activity activity;
private boolean endAfterDismiss;
private AlertDialog alert;
private static final ArrayList<Dialog> rundownDialogs = new ArrayList<Dialog>();
public Dialog(Activity activity, String title, String message, boolean endAfterDismiss)
{
this.activity = activity;
this.title = title;
this.message = message;
this.endAfterDismiss = endAfterDismiss;
}
public static void closeDialogs()
{
synchronized (rundownDialogs) {
for (Dialog d : rundownDialogs) {
if (d.alert.isShowing()) {
d.alert.dismiss();
}
}
rundownDialogs.clear();
}
}
public static void displayDialog(Activity activity, String title, String message, boolean endAfterDismiss)
{
activity.runOnUiThread(new Dialog(activity, title, message, endAfterDismiss));
}
@Override
public void run() {
// If we're dying, don't bother creating a dialog
if (activity.isFinishing())
return;
alert = new AlertDialog.Builder(activity).create();
private final String title;
private final String message;
private final Activity activity;
private final Runnable runOnDismiss;
alert.setTitle(title);
alert.setMessage(message);
alert.setCancelable(false);
alert.setCanceledOnTouchOutside(false);
private AlertDialog alert;
private static final ArrayList<Dialog> rundownDialogs = new ArrayList<>();
private Dialog(Activity activity, String title, String message, Runnable runOnDismiss)
{
this.activity = activity;
this.title = title;
this.message = message;
this.runOnDismiss = runOnDismiss;
}
public static void closeDialogs()
{
synchronized (rundownDialogs) {
for (Dialog d : rundownDialogs) {
if (d.alert.isShowing()) {
d.alert.dismiss();
}
}
rundownDialogs.clear();
}
}
public static void displayDialog(final Activity activity, String title, String message, final boolean endAfterDismiss)
{
activity.runOnUiThread(new Dialog(activity, title, message, new Runnable() {
@Override
public void run() {
if (endAfterDismiss) {
activity.finish();
}
}
}));
}
public static void displayDialog(Activity activity, String title, String message, Runnable runOnDismiss)
{
activity.runOnUiThread(new Dialog(activity, title, message, runOnDismiss));
}
@Override
public void run() {
// If we're dying, don't bother creating a dialog
if (activity.isFinishing())
return;
alert = new AlertDialog.Builder(activity).create();
alert.setTitle(title);
alert.setMessage(message);
alert.setCancelable(false);
alert.setCanceledOnTouchOutside(false);
alert.setButton(AlertDialog.BUTTON_NEUTRAL, "OK", new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int which) {
synchronized (rundownDialogs) {
rundownDialogs.remove(Dialog.this);
alert.dismiss();
}
if (endAfterDismiss) {
activity.finish();
}
}
});
synchronized (rundownDialogs) {
rundownDialogs.add(this);
alert.show();
}
}
alert.setButton(AlertDialog.BUTTON_POSITIVE, activity.getResources().getText(android.R.string.ok), new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int which) {
synchronized (rundownDialogs) {
rundownDialogs.remove(Dialog.this);
alert.dismiss();
}
runOnDismiss.run();
}
});
alert.setButton(AlertDialog.BUTTON_NEUTRAL, activity.getResources().getText(R.string.help), new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int which) {
synchronized (rundownDialogs) {
rundownDialogs.remove(Dialog.this);
alert.dismiss();
}
runOnDismiss.run();
HelpLauncher.launchTroubleshooting(activity);
}
});
alert.setOnShowListener(new DialogInterface.OnShowListener(){
@Override
public void onShow(DialogInterface dialog) {
// Set focus to the OK button by default
Button button = alert.getButton(AlertDialog.BUTTON_POSITIVE);
button.setFocusable(true);
button.setFocusableInTouchMode(true);
button.requestFocus();
}
});
synchronized (rundownDialogs) {
rundownDialogs.add(this);
alert.show();
}
}
}

View File

@@ -0,0 +1,66 @@
package com.limelight.utils;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.net.Uri;
import com.limelight.HelpActivity;
public class HelpLauncher {
private static boolean isKnownBrowser(Context context, Intent i) {
ResolveInfo resolvedActivity = context.getPackageManager().resolveActivity(i, PackageManager.MATCH_DEFAULT_ONLY);
if (resolvedActivity == null) {
// No browser
return false;
}
String name = resolvedActivity.activityInfo.name;
if (name == null) {
return false;
}
name = name.toLowerCase();
return name.contains("chrome") || name.contains("firefox");
}
private static void launchUrl(Context context, String url) {
// Try to launch the default browser
try {
Intent i = new Intent(Intent.ACTION_VIEW);
i.setData(Uri.parse(url));
// Several Android TV devices will lie and say they do have a browser
// even though the OS just shows an error dialog if we try to use it. We need to
// be a bit more clever on these devices and detect if the browser is a legitimate
// browser or just a fake error message activity.
if (!context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_LEANBACK) ||
isKnownBrowser(context, i)) {
context.startActivity(i);
return;
}
} catch (Exception e) {
// This is only supposed to throw ActivityNotFoundException but
// it can (at least) also throw SecurityException if a user's default
// browser is not exported. We'll catch everything to workaround this.
// Fall through
}
// This platform has no browser (possibly a leanback device)
// We'll launch our WebView activity
Intent i = new Intent(context, HelpActivity.class);
i.setData(Uri.parse(url));
context.startActivity(i);
}
public static void launchSetupGuide(Context context) {
launchUrl(context, "https://github.com/moonlight-stream/moonlight-docs/wiki/Setup-Guide");
}
public static void launchTroubleshooting(Context context) {
launchUrl(context, "https://github.com/moonlight-stream/moonlight-docs/wiki/Troubleshooting");
}
}

View File

@@ -0,0 +1,97 @@
package com.limelight.utils;
import android.app.Activity;
import android.content.Intent;
import android.widget.Toast;
import com.limelight.Game;
import com.limelight.R;
import com.limelight.binding.PlatformBinding;
import com.limelight.computers.ComputerManagerService;
import com.limelight.nvstream.http.ComputerDetails;
import com.limelight.nvstream.http.GfeHttpResponseException;
import com.limelight.nvstream.http.NvApp;
import com.limelight.nvstream.http.NvHTTP;
import java.io.FileNotFoundException;
import java.net.UnknownHostException;
public class ServerHelper {
public static String getCurrentAddressFromComputer(ComputerDetails computer) {
return computer.activeAddress;
}
public static Intent createStartIntent(Activity parent, NvApp app, ComputerDetails computer,
ComputerManagerService.ComputerManagerBinder managerBinder) {
Intent intent = new Intent(parent, Game.class);
intent.putExtra(Game.EXTRA_HOST, getCurrentAddressFromComputer(computer));
intent.putExtra(Game.EXTRA_APP_NAME, app.getAppName());
intent.putExtra(Game.EXTRA_APP_ID, app.getAppId());
intent.putExtra(Game.EXTRA_APP_HDR, app.isHdrSupported());
intent.putExtra(Game.EXTRA_UNIQUEID, managerBinder.getUniqueId());
intent.putExtra(Game.EXTRA_PC_UUID, computer.uuid.toString());
intent.putExtra(Game.EXTRA_PC_NAME, computer.name);
return intent;
}
public static void doStart(Activity parent, NvApp app, ComputerDetails computer,
ComputerManagerService.ComputerManagerBinder managerBinder) {
if (computer.state == ComputerDetails.State.OFFLINE ||
ServerHelper.getCurrentAddressFromComputer(computer) == null) {
Toast.makeText(parent, parent.getResources().getString(R.string.pair_pc_offline), Toast.LENGTH_SHORT).show();
return;
}
parent.startActivity(createStartIntent(parent, app, computer, managerBinder));
}
public static void doQuit(final Activity parent,
final String address,
final NvApp app,
final ComputerManagerService.ComputerManagerBinder managerBinder,
final Runnable onComplete) {
Toast.makeText(parent, parent.getResources().getString(R.string.applist_quit_app) + " " + app.getAppName() + "...", Toast.LENGTH_SHORT).show();
new Thread(new Runnable() {
@Override
public void run() {
NvHTTP httpConn;
String message;
try {
httpConn = new NvHTTP(address,
managerBinder.getUniqueId(), null, PlatformBinding.getCryptoProvider(parent));
if (httpConn.quitApp()) {
message = parent.getResources().getString(R.string.applist_quit_success) + " " + app.getAppName();
} else {
message = parent.getResources().getString(R.string.applist_quit_fail) + " " + app.getAppName();
}
} catch (GfeHttpResponseException e) {
if (e.getErrorCode() == 599) {
message = "This session wasn't started by this device," +
" so it cannot be quit. End streaming on the original " +
"device or the PC itself. (Error code: "+e.getErrorCode()+")";
}
else {
message = e.getMessage();
}
} catch (UnknownHostException e) {
message = parent.getResources().getString(R.string.error_unknown_host);
} catch (FileNotFoundException e) {
message = parent.getResources().getString(R.string.error_404);
} catch (Exception e) {
message = e.getMessage();
} finally {
if (onComplete != null) {
onComplete.run();
}
}
final String toastMessage = message;
parent.runOnUiThread(new Runnable() {
@Override
public void run() {
Toast.makeText(parent, toastMessage, Toast.LENGTH_LONG).show();
}
});
}
}).start();
}
}

View File

@@ -0,0 +1,171 @@
package com.limelight.utils;
import android.annotation.TargetApi;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ShortcutInfo;
import android.content.pm.ShortcutManager;
import android.graphics.Bitmap;
import android.graphics.drawable.Icon;
import android.os.Build;
import com.limelight.AppView;
import com.limelight.ShortcutTrampoline;
import com.limelight.R;
import com.limelight.nvstream.http.ComputerDetails;
import com.limelight.nvstream.http.NvApp;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
public class ShortcutHelper {
private final ShortcutManager sm;
private final Context context;
public ShortcutHelper(Context context) {
this.context = context;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) {
sm = context.getSystemService(ShortcutManager.class);
}
else {
sm = null;
}
}
@TargetApi(Build.VERSION_CODES.N_MR1)
private void reapShortcutsForDynamicAdd() {
List<ShortcutInfo> dynamicShortcuts = sm.getDynamicShortcuts();
while (dynamicShortcuts.size() >= sm.getMaxShortcutCountPerActivity()) {
ShortcutInfo maxRankShortcut = dynamicShortcuts.get(0);
for (ShortcutInfo scut : dynamicShortcuts) {
if (maxRankShortcut.getRank() < scut.getRank()) {
maxRankShortcut = scut;
}
}
sm.removeDynamicShortcuts(Collections.singletonList(maxRankShortcut.getId()));
}
}
@TargetApi(Build.VERSION_CODES.N_MR1)
private List<ShortcutInfo> getAllShortcuts() {
LinkedList<ShortcutInfo> list = new LinkedList<>();
list.addAll(sm.getDynamicShortcuts());
list.addAll(sm.getPinnedShortcuts());
return list;
}
@TargetApi(Build.VERSION_CODES.N_MR1)
private ShortcutInfo getInfoForId(String id) {
List<ShortcutInfo> shortcuts = getAllShortcuts();
for (ShortcutInfo info : shortcuts) {
if (info.getId().equals(id)) {
return info;
}
}
return null;
}
@TargetApi(Build.VERSION_CODES.N_MR1)
private boolean isExistingDynamicShortcut(String id) {
for (ShortcutInfo si : sm.getDynamicShortcuts()) {
if (si.getId().equals(id)) {
return true;
}
}
return false;
}
public void reportShortcutUsed(String id) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) {
if (getInfoForId(id) != null) {
sm.reportShortcutUsed(id);
}
}
}
public void createAppViewShortcut(String id, String computerName, String computerUuid, boolean forceAdd) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) {
Intent i = new Intent(context, ShortcutTrampoline.class);
i.putExtra(AppView.NAME_EXTRA, computerName);
i.putExtra(AppView.UUID_EXTRA, computerUuid);
i.setAction(Intent.ACTION_DEFAULT);
ShortcutInfo sinfo = new ShortcutInfo.Builder(context, id)
.setIntent(i)
.setShortLabel(computerName)
.setLongLabel(computerName)
.setIcon(Icon.createWithResource(context, R.mipmap.ic_pc_scut))
.build();
ShortcutInfo existingSinfo = getInfoForId(id);
if (existingSinfo != null) {
// Update in place
sm.updateShortcuts(Collections.singletonList(sinfo));
sm.enableShortcuts(Collections.singletonList(id));
}
// Reap shortcuts to make space for this if it's new
// NOTE: This CAN'T be an else on the above if, because it's
// possible that we have an existing shortcut but it's not a dynamic one.
if (!isExistingDynamicShortcut(id)) {
// To avoid a random carousel of shortcuts popping in and out based on polling status,
// we only add shortcuts if it's not at the limit or the user made a conscious action
// to interact with this PC.
if (forceAdd || sm.getDynamicShortcuts().size() < sm.getMaxShortcutCountPerActivity()) {
reapShortcutsForDynamicAdd();
sm.addDynamicShortcuts(Collections.singletonList(sinfo));
}
}
}
}
public void createAppViewShortcut(String id, ComputerDetails details, boolean forceAdd) {
createAppViewShortcut(id, details.name, details.uuid.toString(), forceAdd);
}
@TargetApi(Build.VERSION_CODES.O)
public boolean createPinnedGameShortcut(String id, Bitmap iconBits, String computerName, String computerUuid, String appName, String appId) {
if (sm.isRequestPinShortcutSupported()) {
Icon appIcon;
Intent i = new Intent(context, ShortcutTrampoline.class);
i.putExtra(AppView.NAME_EXTRA, computerName);
i.putExtra(AppView.UUID_EXTRA, computerUuid);
i.putExtra(ShortcutTrampoline.APP_ID_EXTRA, appId);
i.setAction(Intent.ACTION_DEFAULT);
if (iconBits != null) {
appIcon = Icon.createWithAdaptiveBitmap(iconBits);
} else {
appIcon = Icon.createWithResource(context, R.mipmap.ic_pc_scut);
}
ShortcutInfo sInfo = new ShortcutInfo.Builder(context, id)
.setIntent(i)
.setShortLabel(appName + " (" + computerName + ")")
.setIcon(appIcon)
.build();
return sm.requestPinShortcut(sInfo, null);
} else {
return false;
}
}
public boolean createPinnedGameShortcut(String id, Bitmap iconBits, ComputerDetails cDetails, NvApp app) {
return createPinnedGameShortcut(id, iconBits, cDetails.name, cDetails.uuid.toString(), app.getAppName(), Integer.valueOf(app.getAppId()).toString());
}
public void disableShortcut(String id, CharSequence reason) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) {
if (getInfoForId(id) != null) {
sm.disableShortcuts(Collections.singletonList(id), reason);
}
}
}
}

View File

@@ -9,111 +9,112 @@ import android.content.DialogInterface;
import android.content.DialogInterface.OnCancelListener;
public class SpinnerDialog implements Runnable,OnCancelListener {
private String title, message;
private Activity activity;
private ProgressDialog progress;
private boolean finish;
private static final ArrayList<SpinnerDialog> rundownDialogs = new ArrayList<SpinnerDialog>();
public SpinnerDialog(Activity activity, String title, String message, boolean finish)
{
this.activity = activity;
this.title = title;
this.message = message;
this.progress = null;
this.finish = finish;
}
public static SpinnerDialog displayDialog(Activity activity, String title, String message, boolean finish)
{
SpinnerDialog spinner = new SpinnerDialog(activity, title, message, finish);
activity.runOnUiThread(spinner);
return spinner;
}
public static void closeDialogs(Activity activity)
{
synchronized (rundownDialogs) {
Iterator<SpinnerDialog> i = rundownDialogs.iterator();
while (i.hasNext()) {
SpinnerDialog dialog = i.next();
if (dialog.activity == activity) {
i.remove();
if (dialog.progress.isShowing()) {
dialog.progress.dismiss();
}
}
}
}
}
public void dismiss()
{
// Running again with progress != null will destroy it
activity.runOnUiThread(this);
}
public void setMessage(final String message)
{
activity.runOnUiThread(new Runnable() {
@Override
public void run() {
progress.setMessage(message);
}
});
}
@Override
public void run() {
private final String title;
private final String message;
private final Activity activity;
private ProgressDialog progress;
private final boolean finish;
// If we're dying, don't bother doing anything
if (activity.isFinishing()) {
return;
}
if (progress == null)
{
progress = new ProgressDialog(activity);
progress.setTitle(title);
progress.setMessage(message);
progress.setProgressStyle(ProgressDialog.STYLE_SPINNER);
progress.setOnCancelListener(this);
// If we want to finish the activity when this is killed, make it cancellable
if (finish)
{
progress.setCancelable(true);
progress.setCanceledOnTouchOutside(false);
}
else
{
progress.setCancelable(false);
}
synchronized (rundownDialogs) {
rundownDialogs.add(this);
progress.show();
}
}
else
{
synchronized (rundownDialogs) {
if (rundownDialogs.remove(this) && progress.isShowing()) {
progress.dismiss();
}
}
}
}
private static final ArrayList<SpinnerDialog> rundownDialogs = new ArrayList<>();
@Override
public void onCancel(DialogInterface dialog) {
synchronized (rundownDialogs) {
rundownDialogs.remove(this);
}
// This will only be called if finish was true, so we don't need to check again
activity.finish();
}
private SpinnerDialog(Activity activity, String title, String message, boolean finish)
{
this.activity = activity;
this.title = title;
this.message = message;
this.progress = null;
this.finish = finish;
}
public static SpinnerDialog displayDialog(Activity activity, String title, String message, boolean finish)
{
SpinnerDialog spinner = new SpinnerDialog(activity, title, message, finish);
activity.runOnUiThread(spinner);
return spinner;
}
public static void closeDialogs(Activity activity)
{
synchronized (rundownDialogs) {
Iterator<SpinnerDialog> i = rundownDialogs.iterator();
while (i.hasNext()) {
SpinnerDialog dialog = i.next();
if (dialog.activity == activity) {
i.remove();
if (dialog.progress.isShowing()) {
dialog.progress.dismiss();
}
}
}
}
}
public void dismiss()
{
// Running again with progress != null will destroy it
activity.runOnUiThread(this);
}
public void setMessage(final String message)
{
activity.runOnUiThread(new Runnable() {
@Override
public void run() {
progress.setMessage(message);
}
});
}
@Override
public void run() {
// If we're dying, don't bother doing anything
if (activity.isFinishing()) {
return;
}
if (progress == null)
{
progress = new ProgressDialog(activity);
progress.setTitle(title);
progress.setMessage(message);
progress.setProgressStyle(ProgressDialog.STYLE_SPINNER);
progress.setOnCancelListener(this);
// If we want to finish the activity when this is killed, make it cancellable
if (finish)
{
progress.setCancelable(true);
progress.setCanceledOnTouchOutside(false);
}
else
{
progress.setCancelable(false);
}
synchronized (rundownDialogs) {
rundownDialogs.add(this);
progress.show();
}
}
else
{
synchronized (rundownDialogs) {
if (rundownDialogs.remove(this) && progress.isShowing()) {
progress.dismiss();
}
}
}
}
@Override
public void onCancel(DialogInterface dialog) {
synchronized (rundownDialogs) {
rundownDialogs.remove(this);
}
// This will only be called if finish was true, so we don't need to check again
activity.finish();
}
}

View File

@@ -0,0 +1,167 @@
package com.limelight.utils;
import android.app.Activity;
import android.app.AlertDialog;
import android.app.UiModeManager;
import android.content.Context;
import android.content.DialogInterface;
import android.content.SharedPreferences;
import android.content.res.Configuration;
import android.os.Build;
import android.view.View;
import android.view.WindowManager;
import com.limelight.R;
import com.limelight.nvstream.http.ComputerDetails;
import com.limelight.preferences.PreferenceConfiguration;
import java.util.Locale;
public class UiHelper {
// Values from https://developer.android.com/training/tv/start/layouts.html
private static final int TV_VERTICAL_PADDING_DP = 27;
private static final int TV_HORIZONTAL_PADDING_DP = 48;
public static void setLocale(Activity activity)
{
String locale = PreferenceConfiguration.readPreferences(activity).language;
if (!locale.equals(PreferenceConfiguration.DEFAULT_LANGUAGE)) {
Configuration config = new Configuration(activity.getResources().getConfiguration());
// Some locales include both language and country which must be separated
// before calling the Locale constructor.
if (locale.contains("-"))
{
config.locale = new Locale(locale.substring(0, locale.indexOf('-')),
locale.substring(locale.indexOf('-') + 1));
}
else
{
config.locale = new Locale(locale);
}
activity.getResources().updateConfiguration(config, activity.getResources().getDisplayMetrics());
}
}
public static void notifyNewRootView(Activity activity)
{
View rootView = activity.findViewById(android.R.id.content);
UiModeManager modeMgr = (UiModeManager) activity.getSystemService(Context.UI_MODE_SERVICE);
if (modeMgr.getCurrentModeType() == Configuration.UI_MODE_TYPE_TELEVISION)
{
// Increase view padding on TVs
float scale = activity.getResources().getDisplayMetrics().density;
int verticalPaddingPixels = (int) (TV_VERTICAL_PADDING_DP*scale + 0.5f);
int horizontalPaddingPixels = (int) (TV_HORIZONTAL_PADDING_DP*scale + 0.5f);
rootView.setPadding(horizontalPaddingPixels, verticalPaddingPixels,
horizontalPaddingPixels, verticalPaddingPixels);
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
// Allow this non-streaming activity to layout under notches.
//
// We should NOT do this for the Game activity unless
// the user specifically opts in, because it can obscure
// parts of the streaming surface.
activity.getWindow().getAttributes().layoutInDisplayCutoutMode =
WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES;
}
}
public static void showDecoderCrashDialog(Activity activity) {
final SharedPreferences prefs = activity.getSharedPreferences("DecoderTombstone", 0);
final int crashCount = prefs.getInt("CrashCount", 0);
int lastNotifiedCrashCount = prefs.getInt("LastNotifiedCrashCount", 0);
// Remember the last crash count we notified at, so we don't
// display the crash dialog every time the app is started until
// they stream again
if (crashCount != 0 && crashCount != lastNotifiedCrashCount) {
if (crashCount % 3 == 0) {
// At 3 consecutive crashes, we'll forcefully reset their settings
PreferenceConfiguration.resetStreamingSettings(activity);
Dialog.displayDialog(activity,
activity.getResources().getString(R.string.title_decoding_reset),
activity.getResources().getString(R.string.message_decoding_reset),
new Runnable() {
@Override
public void run() {
// Mark notification as acknowledged on dismissal
prefs.edit().putInt("LastNotifiedCrashCount", crashCount).apply();
}
});
}
else {
Dialog.displayDialog(activity,
activity.getResources().getString(R.string.title_decoding_error),
activity.getResources().getString(R.string.message_decoding_error),
new Runnable() {
@Override
public void run() {
// Mark notification as acknowledged on dismissal
prefs.edit().putInt("LastNotifiedCrashCount", crashCount).apply();
}
});
}
}
}
public static void displayQuitConfirmationDialog(Activity parent, final Runnable onYes, final Runnable onNo) {
DialogInterface.OnClickListener dialogClickListener = new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
switch (which){
case DialogInterface.BUTTON_POSITIVE:
if (onYes != null) {
onYes.run();
}
break;
case DialogInterface.BUTTON_NEGATIVE:
if (onNo != null) {
onNo.run();
}
break;
}
}
};
AlertDialog.Builder builder = new AlertDialog.Builder(parent);
builder.setMessage(parent.getResources().getString(R.string.applist_quit_confirmation))
.setPositiveButton(parent.getResources().getString(R.string.yes), dialogClickListener)
.setNegativeButton(parent.getResources().getString(R.string.no), dialogClickListener)
.show();
}
public static void displayDeletePcConfirmationDialog(Activity parent, ComputerDetails computer, final Runnable onYes, final Runnable onNo) {
DialogInterface.OnClickListener dialogClickListener = new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
switch (which){
case DialogInterface.BUTTON_POSITIVE:
if (onYes != null) {
onYes.run();
}
break;
case DialogInterface.BUTTON_NEGATIVE:
if (onNo != null) {
onNo.run();
}
break;
}
}
};
AlertDialog.Builder builder = new AlertDialog.Builder(parent);
builder.setMessage(parent.getResources().getString(R.string.delete_pc_msg))
.setTitle(computer.name)
.setPositiveButton(parent.getResources().getString(R.string.yes), dialogClickListener)
.setNegativeButton(parent.getResources().getString(R.string.no), dialogClickListener)
.show();
}
}

View File

@@ -0,0 +1,47 @@
package com.limelight.utils;
public class Vector2d {
private float x;
private float y;
private double magnitude;
public static final Vector2d ZERO = new Vector2d();
public Vector2d() {
initialize(0, 0);
}
public void initialize(float x, float y) {
this.x = x;
this.y = y;
this.magnitude = Math.sqrt(Math.pow(x, 2) + Math.pow(y, 2));
}
public double getMagnitude() {
return magnitude;
}
public void getNormalized(Vector2d vector) {
vector.initialize((float)(x / magnitude), (float)(y / magnitude));
}
public void scalarMultiply(double factor) {
initialize((float)(x * factor), (float)(y * factor));
}
public void setX(float x) {
initialize(x, this.y);
}
public void setY(float y) {
initialize(this.x, y);
}
public float getX() {
return x;
}
public float getY() {
return y;
}
}

View File

@@ -1,9 +1,4 @@
# Application.mk for Limelight
# Application.mk for Moonlight
# Our minimum version is Android 4.1
APP_PLATFORM := android-16
APP_ABI := armeabi-v7a arm64-v8a x86 x86_64 mips #mips64
# We want an optimized build
APP_OPTIM := release

View File

@@ -5,9 +5,32 @@ include $(call all-subdir-makefiles)
LOCAL_PATH := $(MY_LOCAL_PATH)
include $(CLEAR_VARS)
LOCAL_MODULE := evdev_reader
LOCAL_SRC_FILES := evdev_reader.c
LOCAL_LDLIBS := -llog
# Only build evdev_reader for the rooted APK flavor
ifeq (root,$(PRODUCT_FLAVOR))
include $(CLEAR_VARS)
LOCAL_MODULE := evdev_reader
LOCAL_SRC_FILES := evdev_reader.c
LOCAL_LDLIBS := -llog
# This next portion of the makefile is mostly copied from build-executable.mk but
# creates a binary with the libXXX.so form so the APK will install and drop
# the binary correctly.
LOCAL_BUILD_SCRIPT := BUILD_EXECUTABLE
LOCAL_MAKEFILE := $(local-makefile)
$(call check-defined-LOCAL_MODULE,$(LOCAL_BUILD_SCRIPT))
$(call check-LOCAL_MODULE,$(LOCAL_MAKEFILE))
$(call check-LOCAL_MODULE_FILENAME)
# we are building target objects
my := TARGET_
$(call handle-module-filename,lib,$(TARGET_SONAME_EXTENSION))
$(call handle-module-built)
LOCAL_MODULE_CLASS := EXECUTABLE
include $(BUILD_SYSTEM)/build-module.mk
endif
include $(BUILD_SHARED_LIBRARY)

View File

@@ -1,118 +1,412 @@
#include <stdlib.h>
#include <jni.h>
#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <linux/input.h>
#include <unistd.h>
#include <poll.h>
#include <errno.h>
#include <dirent.h>
#include <pthread.h>
#include <netinet/in.h>
#include <netinet/tcp.h>
#include <arpa/inet.h>
#include <android/log.h>
JNIEXPORT jint JNICALL
Java_com_limelight_binding_input_evdev_EvdevReader_open(JNIEnv *env, jobject this, jstring absolutePath) {
const char *path;
path = (*env)->GetStringUTFChars(env, absolutePath, NULL);
return open(path, O_RDWR);
}
#define EVDEV_MAX_EVENT_SIZE 24
JNIEXPORT jboolean JNICALL
Java_com_limelight_binding_input_evdev_EvdevReader_grab(JNIEnv *env, jobject this, jint fd) {
return ioctl(fd, EVIOCGRAB, 1) == 0;
}
#define REL_X 0x00
#define REL_Y 0x01
#define KEY_Q 16
#define BTN_LEFT 0x110
#define BTN_GAMEPAD 0x130
JNIEXPORT jboolean JNICALL
Java_com_limelight_binding_input_evdev_EvdevReader_ungrab(JNIEnv *env, jobject this, jint fd) {
return ioctl(fd, EVIOCGRAB, 0) == 0;
}
struct DeviceEntry {
struct DeviceEntry *next;
pthread_t thread;
int fd;
char devName[128];
};
// has*() and friends are based on Android's EventHub.cpp
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 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).
#define test_bit(bit, array) (array[bit/8] & (1<<(bit%8)))
JNIEXPORT jboolean JNICALL
Java_com_limelight_binding_input_evdev_EvdevReader_hasRelAxis(JNIEnv *env, jobject this, jint fd, jshort axis) {
static int hasRelAxis(int fd, short axis) {
unsigned char relBitmask[(REL_MAX + 1) / 8];
ioctl(fd, EVIOCGBIT(EV_REL, sizeof(relBitmask)), relBitmask);
return test_bit(axis, relBitmask);
}
JNIEXPORT jboolean JNICALL
Java_com_limelight_binding_input_evdev_EvdevReader_hasAbsAxis(JNIEnv *env, jobject this, jint fd, jshort axis) {
unsigned char absBitmask[(ABS_MAX + 1) / 8];
ioctl(fd, EVIOCGBIT(EV_ABS, sizeof(absBitmask)), absBitmask);
return test_bit(axis, absBitmask);
}
JNIEXPORT jboolean JNICALL
Java_com_limelight_binding_input_evdev_EvdevReader_hasKey(JNIEnv *env, jobject this, jint fd, jshort key) {
static int hasKey(int fd, short key) {
unsigned char keyBitmask[(KEY_MAX + 1) / 8];
ioctl(fd, EVIOCGBIT(EV_KEY, sizeof(keyBitmask)), keyBitmask);
return test_bit(key, keyBitmask);
}
JNIEXPORT jint JNICALL
Java_com_limelight_binding_input_evdev_EvdevReader_read(JNIEnv *env, jobject this, jint fd, jbyteArray buffer) {
jint ret;
jbyte *data;
int pollres;
static void outputEvdevData(char *data, int dataSize) {
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;
data = (*env)->GetByteArrayElements(env, buffer, NULL);
if (data == NULL) {
__android_log_print(ANDROID_LOG_ERROR, "EvdevReader",
"Failed to get byte array");
int pollres, ret;
char data[EVDEV_MAX_EVENT_SIZE];
__android_log_print(ANDROID_LOG_INFO, "EvdevReader", "Polling /dev/input/%s", device->devName);
if (grabbing) {
// Exclusively grab the input device (required to make the Android cursor disappear)
if (ioctl(device->fd, EVIOCGRAB, 1) < 0) {
__android_log_print(ANDROID_LOG_ERROR, "EvdevReader",
"EVIOCGRAB failed for %s: %d", device->devName, errno);
goto cleanup;
}
}
for (;;) {
do {
// Unwait every 250 ms to return to caller if the fd is closed
pollinfo.fd = device->fd;
pollinfo.events = POLLIN;
pollinfo.revents = 0;
pollres = poll(&pollinfo, 1, 250);
}
while (pollres == 0);
if (pollres > 0 && (pollinfo.revents & POLLIN)) {
// We'll have data available now
ret = read(device->fd, data, EVDEV_MAX_EVENT_SIZE);
if (ret < 0) {
__android_log_print(ANDROID_LOG_ERROR, "EvdevReader",
"read() failed: %d", errno);
goto cleanup;
}
else if (ret == 0) {
__android_log_print(ANDROID_LOG_ERROR, "EvdevReader",
"read() graceful EOF");
goto cleanup;
}
else if (grabbing) {
// Write out the data to our client
outputEvdevData(data, ret);
}
}
else {
if (pollres < 0) {
__android_log_print(ANDROID_LOG_ERROR, "EvdevReader",
"poll() failed: %d", errno);
}
else {
__android_log_print(ANDROID_LOG_ERROR, "EvdevReader",
"Unexpected revents: %d", pollinfo.revents);
}
// Terminate this thread
goto cleanup;
}
}
cleanup:
__android_log_print(ANDROID_LOG_INFO, "EvdevReader", "Closing /dev/input/%s", device->devName);
// Remove the context from the linked list
{
struct DeviceEntry *lastEntry;
// Lock the device list
pthread_mutex_lock(&DeviceListLock);
if (DeviceListHead == device) {
DeviceListHead = device->next;
}
else {
lastEntry = DeviceListHead;
while (lastEntry->next != NULL) {
if (lastEntry->next == device) {
lastEntry->next = device->next;
break;
}
lastEntry = lastEntry->next;
}
}
// Unlock device list
pthread_mutex_unlock(&DeviceListLock);
}
// Free the context
ioctl(device->fd, EVIOCGRAB, 0);
close(device->fd);
free(device);
return NULL;
}
static int precheckDeviceForPolling(int fd) {
int isMouse;
int isKeyboard;
int isGamepad;
// This is the same check that Android does in EventHub.cpp
isMouse = hasRelAxis(fd, REL_X) &&
hasRelAxis(fd, REL_Y) &&
hasKey(fd, BTN_LEFT);
// This is the same check that Android does in EventHub.cpp
isKeyboard = hasKey(fd, KEY_Q);
isGamepad = hasKey(fd, BTN_GAMEPAD);
// We only handle keyboards and mice that aren't gamepads
return (isMouse || isKeyboard) && !isGamepad;
}
static void startPollForDevice(char* deviceName) {
struct DeviceEntry *currentEntry;
char fullPath[256];
int fd;
// Lock the device list
pthread_mutex_lock(&DeviceListLock);
// Check if the device is already being polled
currentEntry = DeviceListHead;
while (currentEntry != NULL) {
if (strcmp(currentEntry->devName, deviceName) == 0) {
// Already polling this device
goto unlock;
}
currentEntry = currentEntry->next;
}
// Open the device
sprintf(fullPath, "/dev/input/%s", deviceName);
fd = open(fullPath, O_RDWR);
if (fd < 0) {
__android_log_print(ANDROID_LOG_ERROR, "EvdevReader", "Couldn't open %s: %d", fullPath, errno);
goto unlock;
}
// Allocate a context
currentEntry = malloc(sizeof(*currentEntry));
if (currentEntry == NULL) {
close(fd);
goto unlock;
}
// Populate context
currentEntry->fd = fd;
strcpy(currentEntry->devName, deviceName);
// Check if we support polling this device
if (!precheckDeviceForPolling(fd)) {
// Nope, get out
free(currentEntry);
close(fd);
goto unlock;
}
// Start the polling thread
if (pthread_create(&currentEntry->thread, NULL, pollThreadFunc, currentEntry) != 0) {
free(currentEntry);
close(fd);
goto unlock;
}
// Queue this onto the device list
currentEntry->next = DeviceListHead;
DeviceListHead = currentEntry;
unlock:
// Unlock and return
pthread_mutex_unlock(&DeviceListLock);
}
static int enumerateDevices(void) {
DIR *inputDir;
struct dirent *dirEnt;
inputDir = opendir("/dev/input");
if (!inputDir) {
__android_log_print(ANDROID_LOG_ERROR, "EvdevReader", "Couldn't open /dev/input: %d", errno);
return -1;
}
do
{
// Unwait every 250 ms to return to caller if the fd is closed
pollinfo.fd = fd;
pollinfo.events = POLLIN;
pollinfo.revents = 0;
pollres = poll(&pollinfo, 1, 250);
}
while (pollres == 0);
if (pollres > 0 && (pollinfo.revents & POLLIN)) {
// We'll have data available now
ret = read(fd, data, sizeof(struct input_event));
if (ret < 0) {
__android_log_print(ANDROID_LOG_ERROR, "EvdevReader",
"read() failed: %d", errno);
// Start polling each device in /dev/input
while ((dirEnt = readdir(inputDir)) != NULL) {
if (strcmp(dirEnt->d_name, ".") == 0 || strcmp(dirEnt->d_name, "..") == 0) {
// Skip these virtual directories
continue;
}
if (strstr(dirEnt->d_name, "event") == NULL) {
// Skip non-event devices
continue;
}
startPollForDevice(dirEnt->d_name);
}
else {
// There must have been a failure
ret = -1;
if (pollres < 0) {
__android_log_print(ANDROID_LOG_ERROR, "EvdevReader",
"poll() failed: %d", errno);
closedir(inputDir);
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
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();
if (ret < 0) {
return ret;
}
// Wait for requests from the client
for (;;) {
unsigned char requestId;
do {
// Every second we poll again for new devices if
// we haven't received any new events
pollinfo.fd = sock;
pollinfo.events = POLLIN;
pollinfo.revents = 0;
pollres = poll(&pollinfo, 1, 1000);
if (pollres == 0) {
// Timeout, re-enumerate devices
enumerateDevices();
}
}
while (pollres == 0);
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;
}
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 {
__android_log_print(ANDROID_LOG_ERROR, "EvdevReader",
"Unexpected revents: %d", pollinfo.revents);
// 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;
}
}
(*env)->ReleaseByteArrayElements(env, buffer, data, 0);
return ret;
}
JNIEXPORT jint JNICALL
Java_com_limelight_binding_input_evdev_EvdevReader_close(JNIEnv *env, jobject this, jint fd) {
return close(fd);
}
}

View File

@@ -1,17 +0,0 @@
# Android.mk for Limelight's H264 decoder
MY_LOCAL_PATH := $(call my-dir)
include $(call all-subdir-makefiles)
LOCAL_PATH := $(MY_LOCAL_PATH)
include $(CLEAR_VARS)
LOCAL_MODULE := nv_avc_dec
LOCAL_SRC_FILES := nv_avc_dec.c nv_avc_dec_jni.c
LOCAL_C_INCLUDES := $(LOCAL_PATH)/ffmpeg/$(TARGET_ARCH_ABI)/include
LOCAL_LDLIBS := -L$(SYSROOT)/usr/lib -llog -landroid
# Link to ffmpeg libraries
LOCAL_SHARED_LIBRARIES := libavcodec libavformat libswscale libavutil libwsresample
include $(BUILD_SHARED_LIBRARY)

View File

@@ -1,31 +0,0 @@
LOCAL_PATH:= $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE:= libavcodec
LOCAL_SRC_FILES:= $(TARGET_ARCH_ABI)/lib/libavcodec-55.so
LOCAL_EXPORT_C_INCLUDES := $(LOCAL_PATH)/include
include $(PREBUILT_SHARED_LIBRARY)
include $(CLEAR_VARS)
LOCAL_MODULE:= libavformat
LOCAL_SRC_FILES:= $(TARGET_ARCH_ABI)/lib/libavformat-55.so
LOCAL_EXPORT_C_INCLUDES := $(LOCAL_PATH)/include
include $(PREBUILT_SHARED_LIBRARY)
include $(CLEAR_VARS)
LOCAL_MODULE:= libswscale
LOCAL_SRC_FILES:= $(TARGET_ARCH_ABI)/lib/libswscale-2.so
LOCAL_EXPORT_C_INCLUDES := $(LOCAL_PATH)/include
include $(PREBUILT_SHARED_LIBRARY)
include $(CLEAR_VARS)
LOCAL_MODULE:= libavutil
LOCAL_SRC_FILES:= $(TARGET_ARCH_ABI)/lib/libavutil-52.so
LOCAL_EXPORT_C_INCLUDES := $(LOCAL_PATH)/include
include $(PREBUILT_SHARED_LIBRARY)
include $(CLEAR_VARS)
LOCAL_MODULE:= libwsresample
LOCAL_SRC_FILES:= $(TARGET_ARCH_ABI)/lib/libswresample-0.so
LOCAL_EXPORT_C_INCLUDES := $(LOCAL_PATH)/include
include $(PREBUILT_SHARED_LIBRARY)

Some files were not shown because too many files have changed in this diff Show More