Compare commits

..

284 Commits

Author SHA1 Message Date
Cameron Gutman ad3614c58e Version 8.1 2019-08-07 23:39:28 -07:00
Cameron Gutman 9401ecc9fb Fix location of 197 changelog 2019-08-07 23:21:17 -07:00
Cameron Gutman 1711e5e1a4 Update common to fix termination detection and STUN fallback 2019-08-07 23:19:01 -07:00
Cameron Gutman 8eb4014f01 Fix build 2019-08-07 23:02:28 -07:00
Cameron Gutman df0d7952db Merge pull request #727 from bubuleur/patch-5
Update french language
2019-08-07 22:59:54 -07:00
Cameron Gutman 77d1770063 Tweak padding and spacing 2019-08-07 22:58:29 -07:00
Cameron Gutman f433bfdc02 Use an edge-to-edge layout for Android Q 2019-08-07 22:01:46 -07:00
Cameron Gutman f75b6f9b80 Remove redundant LinearLayout 2019-08-07 21:09:56 -07:00
Cameron Gutman 621df9996d Remove extra view padding for TV 2019-08-07 20:27:11 -07:00
Cameron Gutman 6c29503db9 Move the Android TV banner into the correct drawable folder. Fixes #728 2019-08-07 20:14:27 -07:00
Cameron Gutman 304a02e2ec Add Travis CI badge 2019-08-07 01:37:02 -07:00
Cameron Gutman 7aea7ed8c6 Add Travis CI support 2019-08-07 01:22:51 -07:00
Cameron Gutman e5ab3baa7b Fix Lint error in BouncyCastle due to javax references 2019-08-07 01:11:17 -07:00
bubuleur 41b73f7cd9 Update french language 2019-08-01 11:22:04 +02:00
Cameron Gutman 38da42caf3 Ignore .cxx folder 2019-07-28 11:40:46 -07:00
Cameron Gutman 424d71fa13 Update common to fix IPv6 WoL and GFE 3.19 graceful termination 2019-07-28 11:39:16 -07:00
Cameron Gutman dbc9d78002 Fix PiP overlay hiding with OSC disabled 2019-07-28 11:38:35 -07:00
Cameron Gutman b7ef8f54b7 Allow installation on external storage 2019-07-28 11:38:35 -07:00
Cameron Gutman bea7cab0c3 Hide overlays in PiP mode 2019-07-28 11:38:35 -07:00
Cameron Gutman 352b6f7dd9 Delete cached box art when deleting a PC 2019-07-28 11:38:35 -07:00
Cameron Gutman 8665fe364f Merge pull request #725 from GinVavilon/restore-program-after-user-remove
Delete program if it is removed by user
2019-07-28 11:38:11 -07:00
Cameron Gutman 7d023c8865 Merge pull request #726 from GinVavilon/update-ru-strings
Update Russian translation
2019-07-28 11:37:14 -07:00
GinVavilon 503d4b970c Update Russian translation 2019-07-28 21:25:16 +03:00
GinVavilon 6b07072a08 Delete program if it is removed by user
Fix problem: if user removes program game is not shown on launch
2019-07-28 20:36:17 +03:00
Cameron Gutman c873bae3e4 Merge pull request #723 from Poussinou/patch-1
Update README.md
2019-07-23 16:41:13 -04:00
Poussinou 7397a97a9e Update README.md 2019-07-23 11:27:40 +02:00
Cameron Gutman b567db9ab7 Version 8.0 2019-07-19 20:34:34 -07:00
Cameron Gutman 3440f54598 Add changelog for v8.0 2019-07-19 19:35:08 -07:00
Cameron Gutman d533b25b29 Fix typo in v7.4 changelog name 2019-07-19 19:33:49 -07:00
Cameron Gutman 72290bd725 Update full description to use F-droid compatible formatting 2019-07-19 19:31:07 -07:00
Cameron Gutman 0ac83e1cf7 Add HDR state to app data in shortcut trampoline 2019-07-16 22:57:30 -07:00
Cameron Gutman e27129fc48 Add the app name to the shortcut trampoline 2019-07-16 22:32:37 -07:00
Cameron Gutman d54fdc9f5f Refactor shortcut and channel code and handle removal of apps and PCs properly 2019-07-16 22:16:29 -07:00
Cameron Gutman dc984e8679 Fix duplicate programs when starting games 2019-07-16 21:29:02 -07:00
Cameron Gutman ee46906376 Fix splitting of address string 2019-07-16 20:36:36 -07:00
Cameron Gutman 1d76536e31 Delete PCs by UUID instead of name 2019-07-16 20:35:18 -07:00
Cameron Gutman dc97adc7a1 Fix upgrading from a build prior to cert pinning support 2019-07-16 20:08:41 -07:00
Cameron Gutman a1c659b7b8 Add support for IPv6-only hosts 2019-07-15 01:28:23 -07:00
Cameron Gutman 27f0fd63b3 Add support for IPv6-only mDNS 2019-07-14 14:17:39 -07:00
Cameron Gutman 83b66b19de Add support for zero configuration IPv6 streaming 2019-07-14 00:21:13 -07:00
Cameron Gutman ba0171221c Upgrade BouncyCastyle to 1.62 2019-07-13 23:57:21 -07:00
Cameron Gutman 6fa1c35521 Merge pull request #718 from uniqx/store-metadata-de
german fdroid store listing translation
2019-07-12 18:10:00 -07:00
Cameron Gutman 7a3fbd8dae Merge pull request #715 from kevinxucs/kevinxucs/update-locales
Translate some of the zh-rCN strings
2019-07-12 18:09:27 -07:00
Michael Pöhn 329ee1a0bc german fdroid store listing translation 2019-07-12 10:44:45 +02:00
Kaiwen Xu 11908e07bf Translate some of the zh-rCN strings. 2019-07-12 01:27:20 -07:00
Cameron Gutman fd53122cb3 Create the PC channel on pairing and add each app to it upon launch 2019-07-12 00:23:13 -07:00
Cameron Gutman d9c0830198 Merge branch 'tv-channels' of https://github.com/GinVavilon/moonlight-android into GinVavilon-tv-channels 2019-07-11 19:19:15 -07:00
Cameron Gutman d0aafb3814 Add Windows to PC requirements 2019-07-10 22:15:58 -07:00
Cameron Gutman 40a3cc2ecb Tweak on-screen overlay a bit 2019-07-10 20:55:01 -07:00
Cameron Gutman 4469013bb5 Merge pull request #716 from kevinxucs/kevinxucs/stats-overlay
Implement performance stats overlay
2019-07-10 20:36:22 -07:00
Cameron Gutman 78393932d0 Update to AGP 3.4.2 2019-07-10 20:13:02 -07:00
Cameron Gutman dbc2491151 Don't manually specify a build tools version 2019-07-10 20:12:41 -07:00
Kaiwen Xu 01eb7a2b64 Add executable permission to gradlew scripts. 2019-07-08 01:12:36 -07:00
Kaiwen Xu 252285e4f7 Implement performance overlay. 2019-07-08 00:55:25 -07:00
GinVavilon df7333b8d0 Add channels support for the Android TV (Oreo) 2019-07-07 22:25:31 +03:00
Cameron Gutman cf98ec2c41 Fix streaming on older servers 2019-07-05 21:29:10 -07:00
Cameron Gutman 754773420f Generate SHA-256 client certificates instead of SHA-1 2019-07-05 21:23:18 -07:00
Cameron Gutman 6574a0aab2 Fix codec blacklisting 2019-07-02 23:20:14 -07:00
Cameron Gutman 5d4988969e Fix layout of Fastlane metadata 2019-06-29 22:48:28 -07:00
Cameron Gutman 5121eb1852 Add icon to metadata 2019-06-29 22:41:13 -07:00
Cameron Gutman 004aeef2a7 Initial Fastlane metadata for F-Droid 2019-06-29 22:11:35 -07:00
Cameron Gutman aa65a0312a Update moonlight-common with some minor cleanup 2019-06-26 17:40:49 -07:00
Cameron Gutman 1308a4ed80 Fix a user-reported crash 2019-06-22 22:01:30 -07:00
Cameron Gutman deb78e1c64 Version 7.4 2019-06-05 23:02:06 -07:00
Cameron Gutman 9aec6b1d31 Target API 29 2019-06-05 22:59:39 -07:00
Cameron Gutman 97702b8861 Fix mouse capture after returning focus to the window on Android Q 2019-06-05 22:43:16 -07:00
Cameron Gutman 832e7197c5 Delay a bit before reporting USB devices to allow the old InputDevice to go away 2019-06-05 22:26:06 -07:00
Cameron Gutman 26b992726c Use transparent status bar and navigation bar on Android Q 2019-06-05 21:50:03 -07:00
Cameron Gutman 1cb3588841 Use low latency WifiLock on Android Q 2019-06-05 21:09:55 -07:00
Cameron Gutman b461d546d6 Use new MediaCodecInfo helper to blacklist software codecs 2019-06-05 21:05:33 -07:00
Cameron Gutman b7810d6eb6 Use the newly public InputDevice.isExternal() function on Android Q 2019-06-05 20:23:22 -07:00
Cameron Gutman 6fb3a8e57d Build with the Android Q SDK 2019-06-05 20:21:19 -07:00
Cameron Gutman b521c784bc Version 7.3.1 2019-05-27 01:48:00 -07:00
twboyii 8e1641af5f Add untranslated string in zh-rTW (#701) 2019-05-27 01:47:10 -07:00
Cameron Gutman c0aac01d33 Update AGP to 3.4.1 2019-05-27 01:43:38 -07:00
Cameron Gutman 4f8b0adcbb Fix video on GFE 3.19 2019-05-27 01:42:39 -07:00
Cameron Gutman 393a4c9c8a Fix pointer capture on Android Q Beta 3 2019-05-16 21:27:01 -07:00
Cameron Gutman 99b53f9a6a Version 7.3 2019-05-07 20:53:12 -07:00
Cameron Gutman 8da563b280 Bound queued audio data to prevent excessive latency 2019-05-07 20:39:45 -07:00
Cameron Gutman d5b950e5cf Version 7.2.1 2019-05-01 20:14:21 -07:00
Cameron Gutman c46b9acf6b Update common to fix receive time 2019-04-30 23:19:19 -07:00
Cameron Gutman d8e322bac9 Sync PC offline icon with Moonlight Qt 2019-04-30 22:27:22 -07:00
Cameron Gutman 44871626cf Version 7.2 2019-04-27 22:11:02 -07:00
Cameron Gutman f661522b5d Update moonlight-common with additional perf improvements 2019-04-27 22:00:27 -07:00
Cameron Gutman a454b0ab78 Update moonlight-common with perf improvements 2019-04-26 18:37:27 -07:00
Cameron Gutman 75bf84d0d9 Update Gradle for AS 3.4 2019-04-26 18:34:16 -07:00
Cameron Gutman c248994ed4 Version 7.1 2019-04-07 14:09:58 -07:00
Cameron Gutman a7a34ec629 Update vibration weights to match Moonlight Qt 2019-04-06 01:02:03 -07:00
Cameron Gutman 8d469c5d0a Add on-screen connection warnings 2019-04-06 00:56:45 -07:00
Cameron Gutman e6979d50b5 Update AGP to 3.3.2 2019-04-06 00:48:40 -07:00
Cameron Gutman 6e25b135a3 Update ProGuard rules to avoid slf4j warnings 2019-03-20 18:57:40 -07:00
Cameron Gutman 04e093a2c2 Update moonlight-common 2019-03-20 18:51:08 -07:00
bubuleur 813f2edd95 Update French Language (#676) 2019-03-02 20:01:21 -08:00
Cameron Gutman 337d753a33 Reduce gamepad deadzone to 7% 2019-03-02 17:23:01 -08:00
Cameron Gutman 1137c74f76 Pass AudioAttributes on L+ when vibrating 2019-03-02 17:20:39 -08:00
Cameron Gutman 0c1451f757 Improve scaling of lock icon by increasing dimensions 2019-02-18 20:46:34 -08:00
Cameron Gutman 5ab9ea48fd Version 7.0.1 2019-02-17 18:20:40 -08:00
Cameron Gutman ffcb623040 Fix crash when a rumble effect only uses the high-frequency motor 2019-02-17 18:18:00 -08:00
Cameron Gutman bfe6929642 Version 7.0 2019-02-16 19:44:45 -08:00
Cameron Gutman 50d45011a8 Add device vibration and other fixes 2019-02-16 19:13:01 -08:00
Cameron Gutman 2f7087d6d3 Stop vibration on stream end 2019-02-16 18:05:08 -08:00
Cameron Gutman 92b71588d0 Implement rumble on Android InputDevice 2019-02-16 17:56:34 -08:00
Cameron Gutman 4f3d018764 Fix OSC colliding with player 1 2019-02-16 17:29:05 -08:00
Cameron Gutman a22e33eeb9 Add rumble support for the in-app Xbox driver 2019-02-16 17:03:10 -08:00
Cameron Gutman 6a939e7495 Don't display the termination dialog for intended terminations 2019-02-10 02:28:11 -08:00
Cameron Gutman f8ba7cf190 Update common with SOPS fixes 2019-02-09 20:59:59 -08:00
Cameron Gutman d1e135db4d Version 6.2 2019-02-06 22:10:29 -08:00
Cameron Gutman 61a17afe69 Fix *, @, #, and + keys on software keyboard 2019-02-06 21:40:28 -08:00
Cameron Gutman 47fd691884 Update to AGP 3.3.1 2019-02-06 21:14:50 -08:00
Cameron Gutman 0d171c6b28 Fix lock icon drawing on top of the loading spinner 2019-02-06 21:14:01 -08:00
Cameron Gutman f0c69d08b8 Add 480p option 2019-02-06 21:09:04 -08:00
Cameron Gutman 629bf5766d Fix a couple crash reports 2019-02-05 22:51:48 -08:00
Cameron Gutman 233bceeece Update common for GFE 3.17 2019-02-05 22:10:11 -08:00
Cameron Gutman 6660ea7d91 Update Xbox driver with Linux xpad.c and init quirks 2019-02-05 21:52:53 -08:00
Cameron Gutman 4864b2ca45 Add lock icon when PC is unpaired 2019-02-05 21:10:09 -08:00
Cameron Gutman 92097b318d Update Gradle and AGP 2019-02-05 20:58:49 -08:00
Cameron Gutman 997898c99d Version 6.1.3 2019-01-04 18:20:28 -08:00
Cameron Gutman 1174e03885 Fix incorrectly persisting host with missing server cert 2019-01-04 18:18:32 -08:00
Cameron Gutman ff0f54d541 Switch to using stun.moonlight-stream.org for STUN 2019-01-04 18:05:28 -08:00
Cameron Gutman 814964a100 Fix exception adding PCs 2019-01-01 23:32:16 -08:00
Cameron Gutman 7e154292a9 Stop suppressing exceptions 2019-01-01 23:31:38 -08:00
Cameron Gutman 0f9cba1053 Fix crash due to a null computer uuid 2019-01-01 22:34:27 -08:00
Cameron Gutman a4e134589d Version 6.1.1 2018-12-27 23:58:30 -08:00
Cameron Gutman cd80a94f28 Fix IllegalStateException caused by making HTTPS request without a pinned cert 2018-12-27 23:55:59 -08:00
Cameron Gutman 57c645a291 Change uuid field to String type due to new format UUIDs that fail to parse on GFE 3.16 2018-12-27 23:48:12 -08:00
Cameron Gutman 0cba200207 Version 6.1 2018-12-24 19:58:51 -08:00
Cameron Gutman 81582d7343 Revert "Hide the mouse cursor during pointer capture to work around DeX bug"
It doesn't actually fix the bug.

This reverts commit 16b845ab84.
2018-12-24 19:58:19 -08:00
Cameron Gutman 04e561fd54 Update common-c with bitrate fix 2018-12-24 19:56:42 -08:00
Cameron Gutman 5efbb5229d Fix up French translation 2018-12-24 19:09:16 -08:00
bubuleur 541e43eb18 Update Translation french Moonlight (#648)
* Update Translation french Moonlight

Hello
Herewith updated French language for your next Moonlight update
If you want you can contact me on igorlachaudarobaseaol.fr to update your application in French before an exit
cordially
Merci pour tous

* Update strings.xml
2018-12-24 19:07:38 -08:00
Cameron Gutman 7e679ff4c6 Fix short window where newly added PC could be incorrectly marked as unpaired 2018-12-23 21:34:20 -08:00
Cameron Gutman 486b4b4c4c Use a shared UID for all Moonlight clients 2018-12-22 21:03:42 -08:00
Cameron Gutman 7d76bf7868 Require cert pinning for HTTPS 2018-12-22 20:13:11 -08:00
Cameron Gutman db49077b9b Add cert pinning during pairing 2018-12-21 21:00:53 -08:00
Cameron Gutman 16b845ab84 Hide the mouse cursor during pointer capture to work around DeX bug 2018-12-19 15:06:46 +05:00
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
114 changed files with 3896 additions and 1393 deletions
+8
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
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
+8 -2
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,8 +33,11 @@ Thumbs.db
#.idea/workspace.xml - remove # and delete .idea if it better suit your needs.
.gradle
build/
app/app.iml
*.iml
# Compiled JNI libraries folder
**/jniLibs
app/.externalNativeBuild/
# NDK stuff
.cxx/
+15
View File
@@ -0,0 +1,15 @@
language: android
dist: trusty
git:
depth: 1
android:
components:
- tools
- platform-tools
- build-tools-29.0.1
- android-29
install:
- yes | sdkmanager "ndk-bundle"
+5 -5
View File
@@ -1,13 +1,13 @@
# Moonlight
# Moonlight Android
[Moonlight](http://moonlight-stream.com) is an open source implementation of NVIDIA's GameStream, as used by the NVIDIA Shield.
[![Travis CI Status](https://travis-ci.org/moonlight-stream/moonlight-android.svg?branch=master)](https://travis-ci.org/moonlight-stream/moonlight-android)
[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.
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.
[Moonlight-pc](https://github.com/moonlight-stream/moonlight-pc) is also currently in development for Windows, OS X and Linux. Versions for [iOS](https://github.com/moonlight-stream/moonlight-ios) and [Windows and Windows Phone](https://github.com/moonlight-stream/moonlight-windows) are also in development.
Check our [wiki](https://github.com/moonlight-stream/moonlight-docs/wiki) for more detailed information or a troubleshooting guide.
## Features
@@ -19,7 +19,7 @@ Check our [wiki](https://github.com/moonlight-stream/moonlight-docs/wiki) for mo
## Installation
* 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)
[Google Play](https://play.google.com/store/apps/details?id=com.limelight), [F-Droid](https://f-droid.org/packages/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
+27 -12
View File
@@ -1,18 +1,14 @@
import com.android.builder.model.ProductFlavor
import org.apache.tools.ant.taskdefs.condition.Os
apply plugin: 'com.android.application'
android {
compileSdkVersion 27
buildToolsVersion '27.0.3'
compileSdkVersion 29
defaultConfig {
minSdkVersion 16
targetSdkVersion 27
targetSdkVersion 29
versionName "5.7"
versionCode = 148
versionName "8.1"
versionCode = 198
}
flavorDimensions "root"
@@ -47,11 +43,30 @@ android {
lintOptions {
disable 'MissingTranslation'
lintConfig file("lint.xml")
}
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 {
// To whomever is releasing/using an APK in release mode with
@@ -86,8 +101,8 @@ android {
// TL;DR: Leave the following line alone!
applicationIdSuffix ".unofficial"
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt')
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
@@ -99,8 +114,8 @@ android {
}
dependencies {
implementation 'org.bouncycastle:bcprov-jdk15on:1.59'
implementation 'org.bouncycastle:bcpkix-jdk15on:1.59'
implementation 'org.bouncycastle:bcprov-jdk15on:1.62'
implementation 'org.bouncycastle:bcpkix-jdk15on:1.62'
implementation 'org.jcodec:jcodec:0.2.3'
implementation project(':moonlight-common')
+6
View File
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<lint>
<issue id="InvalidPackage">
<ignore path="**/bcpkix-jdk15on-*.jar"/>
</issue>
</lint>
+28
View File
@@ -0,0 +1,28 @@
# 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
-dontwarn org.slf4j.**
+7
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>
+15 -3
View File
@@ -4,9 +4,12 @@
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.VIBRATE" />
<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-permission android:name="com.android.providers.tv.permission.READ_EPG_DATA"/>
<uses-permission android:name="com.android.providers.tv.permission.WRITE_EPG_DATA"/>
<uses-feature
android:name="android.hardware.touchscreen"
@@ -32,13 +35,20 @@
<application
android:allowBackup="true"
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:installLocation="auto"
android:theme="@style/AppTheme">
<provider
android:name=".PosterContentProvider"
android:authorities="poster.${applicationId}"
android:enabled="true"
android:exported="true">
</provider>
<!-- Samsung multi-window support -->
<uses-library
android:name="com.sec.android.app.multiwindow"
@@ -66,8 +76,9 @@
</activity>
<!-- Small hack to support launcher shortcuts without relaunching over and over again when the back button is pressed -->
<activity
android:name=".AppViewShortcutTrampoline"
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">
<meta-data
@@ -98,10 +109,11 @@
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:configChanges="mcc|mnc|locale|touchscreen|keyboard|keyboardHidden|navigation|screenLayout|fontScale|uiMode|orientation|screenSize|smallestScreenSize|layoutDirection"
android:screenOrientation="sensorLandscape"
android:screenOrientation="userLandscape"
android:noHistory="true"
android:supportsPictureInPicture="true"
android:resizeableActivity="true"
+62 -22
View File
@@ -1,8 +1,8 @@
package com.limelight;
import java.io.IOException;
import java.io.StringReader;
import java.util.List;
import java.util.UUID;
import com.limelight.computers.ComputerManagerListener;
import com.limelight.computers.ComputerManagerService;
@@ -26,6 +26,9 @@ 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.os.Build;
import android.os.Bundle;
import android.os.IBinder;
import android.view.ContextMenu;
@@ -36,10 +39,13 @@ import android.view.ContextMenu.ContextMenuInfo;
import android.widget.AbsListView;
import android.widget.AdapterView;
import android.widget.AdapterView.OnItemClickListener;
import android.widget.ImageView;
import android.widget.TextView;
import android.widget.Toast;
import android.widget.AdapterView.AdapterContextMenuInfo;
import org.xmlpull.v1.XmlPullParserException;
public class AppView extends Activity implements AdapterFragmentCallbacks {
private AppGridAdapter appGridAdapter;
private String uuidString;
@@ -56,10 +62,13 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
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_WTIH_QUIT = 4;
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";
public final static String NEW_PAIR_EXTRA = "NewPair";
private ComputerManagerService.ComputerManagerBinder managerBinder;
private final ServiceConnection serviceConnection = new ServiceConnection() {
@@ -74,27 +83,33 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
// 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));
computer = localBinder.getComputer(uuidString);
if (computer == null) {
finish();
return;
}
// Add a launcher shortcut for this PC (forced, since this is user interaction)
shortcutHelper.createAppViewShortcut(computer, true, getIntent().getBooleanExtra(NEW_PAIR_EXTRA, false));
shortcutHelper.reportComputerShortcutUsed(computer);
try {
appGridAdapter = new AppGridAdapter(AppView.this,
PreferenceConfiguration.readPreferences(AppView.this).listMode,
PreferenceConfiguration.readPreferences(AppView.this).smallIconMode,
computer, managerBinder.getUniqueId());
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
@@ -147,7 +162,7 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
}
// Don't care about other computers
if (!details.uuid.toString().equalsIgnoreCase(uuidString)) {
if (!details.uuid.equalsIgnoreCase(uuidString)) {
return;
}
@@ -171,7 +186,7 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
@Override
public void run() {
// Disable shortcuts referencing this PC for now
shortcutHelper.disableShortcut(details.uuid.toString(),
shortcutHelper.disableComputerShortcut(details,
getResources().getString(R.string.scut_not_paired));
// Display a toast to the user and quit the activity
@@ -207,7 +222,9 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
blockingLoadSpinner.dismiss();
blockingLoadSpinner = null;
}
} catch (Exception ignored) {}
} catch (XmlPullParserException | IOException e) {
e.printStackTrace();
}
}
});
@@ -251,14 +268,9 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
String computerName = getIntent().getStringExtra(NAME_EXTRA);
String labelText = getResources().getString(R.string.title_applist)+" "+computerName;
TextView label = findViewById(R.id.appListText);
setTitle(labelText);
label.setText(labelText);
// Add a launcher shortcut for this PC (forced, since this is user interaction)
shortcutHelper.createAppViewShortcut(uuidString, computerName, uuidString, true);
shortcutHelper.reportShortcutUsed(uuidString);
setTitle(computerName);
label.setText(computerName);
// Bind to the computer manager service
bindService(new Intent(this, ComputerManagerService.class), serviceConnection,
@@ -272,7 +284,7 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
List<NvApp> applist = NvHTTP.getAppListByReader(new StringReader(lastRawApplist));
updateUiWithAppList(applist);
LimeLog.info("Loaded applist from cache");
} catch (Exception e) {
} catch (IOException | XmlPullParserException e) {
if (lastRawApplist != null) {
LimeLog.warning("Saved applist corrupted: "+lastRawApplist);
e.printStackTrace();
@@ -331,10 +343,25 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
menu.add(Menu.NONE, QUIT_ID, 2, getResources().getString(R.string.applist_menu_quit));
}
else {
menu.add(Menu.NONE, START_WTIH_QUIT, 1, getResources().getString(R.string.applist_menu_quit_and_start));
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
@@ -346,7 +373,7 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
AdapterContextMenuInfo info = (AdapterContextMenuInfo) item.getMenuInfo();
final AppObject app = (AppObject) appGridAdapter.getItem(info.position);
switch (item.getItemId()) {
case START_WTIH_QUIT:
case START_WITH_QUIT:
// Display a confirmation dialog first
UiHelper.displayQuitConfirmationDialog(this, new Runnable() {
@Override
@@ -367,8 +394,7 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
@Override
public void run() {
suspendGridUpdates = true;
ServerHelper.doQuit(AppView.this,
ServerHelper.getCurrentAddressFromComputer(computer),
ServerHelper.doQuit(AppView.this, computer,
app.app, managerBinder, new Runnable() {
@Override
public void run() {
@@ -386,6 +412,19 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
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(computer, app.app, appBits)) {
Toast.makeText(AppView.this, getResources().getString(R.string.unable_to_pin_shortcut), Toast.LENGTH_LONG).show();
}
return true;
default:
return super.onContextItemSelected(item);
}
@@ -477,6 +516,7 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
// This app was removed in the latest app list
if (!foundExistingApp) {
shortcutHelper.disableAppShortcut(computer, existingApp.app, "App removed from PC");
appGridAdapter.removeApp(existingApp);
updated = true;
@@ -1,164 +0,0 @@
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.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 AppViewShortcutTrampoline extends Activity {
private String uuidString;
private ComputerDetails computer;
private SpinnerDialog blockingLoadSpinner;
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();
// Now make the binder visible
managerBinder = localBinder;
// Get the computer object
computer = managerBinder.getComputer(UUID.fromString(uuidString));
// 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 (details.state == ComputerDetails.State.ONLINE) {
// Close this activity
finish();
// Create a new activity stack for this launch
ArrayList<Intent> intentStack = new ArrayList<>();
Intent i;
// Add the PC view at the back (and clear the task)
i = new Intent(AppViewShortcutTrampoline.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(AppViewShortcutTrampoline.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(AppViewShortcutTrampoline.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(AppViewShortcutTrampoline.this,
getResources().getString(R.string.conn_error_title),
getResources().getString(R.string.error_pc_offline),
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;
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
UiHelper.notifyNewRootView(this);
uuidString = getIntent().getStringExtra(UUID_EXTRA);
// 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 onPause() {
super.onPause();
if (blockingLoadSpinner != null) {
blockingLoadSpinner.dismiss();
blockingLoadSpinner = null;
}
Dialog.closeDialogs();
if (managerBinder != null) {
managerBinder.stopPolling();
unbindService(serviceConnection);
managerBinder = null;
}
finish();
}
}
+427 -131
View File
@@ -13,9 +13,11 @@ import com.limelight.binding.input.virtual_controller.VirtualController;
import com.limelight.binding.video.CrashListener;
import com.limelight.binding.video.MediaCodecDecoderRenderer;
import com.limelight.binding.video.MediaCodecHelper;
import com.limelight.binding.video.PerfOverlayListener;
import com.limelight.nvstream.NvConnection;
import com.limelight.nvstream.NvConnectionListener;
import com.limelight.nvstream.StreamConfiguration;
import com.limelight.nvstream.http.ComputerDetails;
import com.limelight.nvstream.http.NvApp;
import com.limelight.nvstream.input.KeyboardPacket;
import com.limelight.nvstream.input.MouseButtonPacket;
@@ -39,6 +41,8 @@ import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.content.SharedPreferences;
import android.content.pm.ActivityInfo;
import android.content.res.Configuration;
import android.graphics.Point;
import android.graphics.Rect;
import android.hardware.input.InputManager;
@@ -64,12 +68,20 @@ import android.view.Window;
import android.view.WindowManager;
import android.widget.FrameLayout;
import android.view.inputmethod.InputMethodManager;
import android.widget.TextView;
import android.widget.Toast;
import java.io.ByteArrayInputStream;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.util.Locale;
public class Game extends Activity implements SurfaceHolder.Callback,
OnGenericMotionListener, OnTouchListener, NvConnectionListener, EvdevListener,
OnSystemUiVisibilityChangeListener, GameGestures
OnSystemUiVisibilityChangeListener, GameGestures, StreamView.InputCallbacks,
PerfOverlayListener
{
private int lastMouseX = Integer.MIN_VALUE;
private int lastMouseY = Integer.MIN_VALUE;
@@ -95,6 +107,8 @@ public class Game extends Activity implements SurfaceHolder.Callback,
private boolean displayedFailureDialog = false;
private boolean connecting = false;
private boolean connected = false;
private boolean surfaceCreated = false;
private boolean attemptedConnection = false;
private InputCaptureProvider inputCaptureProvider;
private int modifierFlags = 0;
@@ -102,12 +116,18 @@ public class Game extends Activity implements SurfaceHolder.Callback,
private boolean grabComboDown = false;
private StreamView streamView;
private boolean isHidingOverlays;
private TextView notificationOverlayView;
private int requestedNotificationOverlayVisibility = View.GONE;
private TextView performanceOverlayView;
private ShortcutHelper shortcutHelper;
private MediaCodecDecoderRenderer decoderRenderer;
private boolean reportedCrash;
private WifiManager.WifiLock wifiLock;
private WifiManager.WifiLock highPerfWifiLock;
private WifiManager.WifiLock lowLatencyWifiLock;
private boolean connectedToUsbDriverService = false;
private ServiceConnection usbDriverServiceConnection = new ServiceConnection() {
@@ -128,10 +148,10 @@ public class Game extends Activity implements SurfaceHolder.Callback,
public static final String EXTRA_APP_NAME = "AppName";
public static final String EXTRA_APP_ID = "AppId";
public static final String EXTRA_UNIQUEID = "UniqueId";
public static final String EXTRA_STREAMING_REMOTE = "Remote";
public static final String EXTRA_PC_UUID = "UUID";
public static final String EXTRA_PC_NAME = "PcName";
public static final String EXTRA_APP_HDR = "HDR";
public static final String EXTRA_SERVER_CERT = "ServerCert";
@Override
protected void onCreate(Bundle savedInstanceState) {
@@ -142,10 +162,8 @@ public class Game extends Activity implements SurfaceHolder.Callback,
// We don't want a title bar
requestWindowFeature(Window.FEATURE_NO_TITLE);
// Full-screen and don't let the display go off
getWindow().addFlags(
WindowManager.LayoutParams.FLAG_FULLSCREEN |
WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
// Full-screen
getWindow().addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
// If we're going to use immersive mode, we want to have
// the entire screen
@@ -158,6 +176,12 @@ public class Game extends Activity implements SurfaceHolder.Callback,
getWindow().addFlags(WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN);
}
// We specified userLandscape in the manifest which isn't supported until 4.3,
// so we must fall back at runtime to sensorLandscape.
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR2) {
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE);
}
// Listen for UI visibility events
getWindow().getDecorView().setOnSystemUiVisibilityChangeListener(this);
@@ -175,11 +199,22 @@ public class Game extends Activity implements SurfaceHolder.Callback,
prefConfig = PreferenceConfiguration.readPreferences(this);
tombstonePrefs = Game.this.getSharedPreferences("DecoderTombstone", 0);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && prefConfig.stretchVideo) {
// Allow the activity to layout under notches if the fill-screen option
// was turned on by the user
getWindow().getAttributes().layoutInDisplayCutoutMode =
WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES;
}
// Listen for events on the game surface
streamView = findViewById(R.id.surfaceView);
streamView.setOnGenericMotionListener(this);
streamView.setOnTouchListener(this);
streamView.setInputCallbacks(this);
notificationOverlayView = findViewById(R.id.notificationOverlay);
performanceOverlayView = findViewById(R.id.performanceOverlay);
inputCaptureProvider = InputCaptureManager.getInputCaptureProvider(this, this);
@@ -203,28 +238,50 @@ public class Game extends Activity implements SurfaceHolder.Callback,
// Make sure Wi-Fi is fully powered up
WifiManager wifiMgr = (WifiManager) getApplicationContext().getSystemService(Context.WIFI_SERVICE);
wifiLock = wifiMgr.createWifiLock(WifiManager.WIFI_MODE_FULL_HIGH_PERF, "Limelight");
wifiLock.setReferenceCounted(false);
wifiLock.acquire();
highPerfWifiLock = wifiMgr.createWifiLock(WifiManager.WIFI_MODE_FULL_HIGH_PERF, "Moonlight High Perf Lock");
highPerfWifiLock.setReferenceCounted(false);
highPerfWifiLock.acquire();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
lowLatencyWifiLock = wifiMgr.createWifiLock(WifiManager.WIFI_MODE_FULL_LOW_LATENCY, "Moonlight Low Latency Lock");
lowLatencyWifiLock.setReferenceCounted(false);
lowLatencyWifiLock.acquire();
}
String host = Game.this.getIntent().getStringExtra(EXTRA_HOST);
String appName = Game.this.getIntent().getStringExtra(EXTRA_APP_NAME);
int appId = Game.this.getIntent().getIntExtra(EXTRA_APP_ID, StreamConfiguration.INVALID_APP_ID);
String uniqueId = Game.this.getIntent().getStringExtra(EXTRA_UNIQUEID);
boolean remote = Game.this.getIntent().getBooleanExtra(EXTRA_STREAMING_REMOTE, false);
String uuid = Game.this.getIntent().getStringExtra(EXTRA_PC_UUID);
String pcName = Game.this.getIntent().getStringExtra(EXTRA_PC_NAME);
boolean willStreamHdr = Game.this.getIntent().getBooleanExtra(EXTRA_APP_HDR, false);
byte[] derCertData = Game.this.getIntent().getByteArrayExtra(EXTRA_SERVER_CERT);
X509Certificate serverCert = null;
try {
if (derCertData != null) {
serverCert = (X509Certificate) CertificateFactory.getInstance("X.509")
.generateCertificate(new ByteArrayInputStream(derCertData));
}
} catch (CertificateException e) {
e.printStackTrace();
}
if (appId == StreamConfiguration.INVALID_APP_ID) {
finish();
return;
}
// Add a launcher shortcut for this PC (forced, since this is user interaction)
// Report this shortcut being used
ComputerDetails computer = new ComputerDetails();
computer.name = pcName;
computer.uuid = uuid;
shortcutHelper = new ShortcutHelper(this);
shortcutHelper.createAppViewShortcut(uuid, pcName, uuid, true);
shortcutHelper.reportShortcutUsed(uuid);
shortcutHelper.reportComputerShortcutUsed(computer);
if (appName != null) {
// This may be null if launched from the "Resume Session" PC context menu item
shortcutHelper.reportGameLaunched(computer, new NvApp(appName, appId, willStreamHdr));
}
// Initialize the MediaCodec helper before creating the decoder
GlPreferences glPrefs = GlPreferences.readPreferences(this);
@@ -244,10 +301,12 @@ public class Game extends Activity implements SurfaceHolder.Callback,
// We must now ensure our display is compatible with HDR10
boolean foundHdr10 = false;
for (int hdrType : hdrCaps.getSupportedHdrTypes()) {
if (hdrType == Display.HdrCapabilities.HDR_TYPE_HDR10) {
LimeLog.info("Display supports HDR10");
foundHdr10 = true;
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;
}
}
}
@@ -266,7 +325,14 @@ public class Game extends Activity implements SurfaceHolder.Callback,
willStreamHdr = false;
}
decoderRenderer = new MediaCodecDecoderRenderer(prefConfig,
// Check if the user has enabled performance stats overlay
if (prefConfig.enablePerfOverlay) {
performanceOverlayView.setVisibility(View.VISIBLE);
}
decoderRenderer = new MediaCodecDecoderRenderer(
this,
prefConfig,
new CrashListener() {
@Override
public void notifyCrash(Exception e) {
@@ -281,8 +347,8 @@ public class Game extends Activity implements SurfaceHolder.Callback,
tombstonePrefs.getInt("CrashCount", 0),
connMgr.isActiveNetworkMetered(),
willStreamHdr,
glPrefs.glRenderer
);
glPrefs.glRenderer,
this);
// Don't stream HDR if the decoder can't support it
if (willStreamHdr && !decoderRenderer.isHevcMain10Hdr10Supported()) {
@@ -296,8 +362,10 @@ public class Game extends Activity implements SurfaceHolder.Callback,
}
int gamepadMask = ControllerHandler.getAttachedControllerMask(this);
if (!prefConfig.multiController && gamepadMask != 0) {
// If any gamepads are present in non-MC mode, set only gamepad 1.
if (!prefConfig.multiController) {
// Always set gamepad 1 present for when multi-controller is
// disabled for games that don't properly support detection
// of gamepads removed and replugged at runtime.
gamepadMask = 1;
}
if (prefConfig.onscreenController) {
@@ -329,8 +397,13 @@ public class Game extends Activity implements SurfaceHolder.Callback,
// Hopefully, we can get rid of this once someone comes up with a better way
// to track the state of the pipeline and time frames.
int roundedRefreshRate = Math.round(displayRefreshRate);
if (!prefConfig.disableFrameDrop && prefConfig.fps >= roundedRefreshRate) {
if (roundedRefreshRate <= 49) {
if ((!prefConfig.disableFrameDrop || prefConfig.unlockFps) && prefConfig.fps >= roundedRefreshRate) {
if (prefConfig.unlockFps) {
// Use frame drops when rendering above the screen frame rate
decoderRenderer.enableLegacyFrameDropRendering();
LimeLog.info("Using drop mode for FPS > Hz");
}
else if (roundedRefreshRate <= 49) {
// Let's avoid clearly bogus refresh rates and fall back to legacy rendering
decoderRenderer.enableLegacyFrameDropRendering();
LimeLog.info("Bogus refresh rate: "+roundedRefreshRate);
@@ -349,12 +422,12 @@ public class Game extends Activity implements SurfaceHolder.Callback,
StreamConfiguration config = new StreamConfiguration.Builder()
.setResolution(prefConfig.width, prefConfig.height)
.setRefreshRate(prefConfig.fps)
.setApp(new NvApp(appName, appId, willStreamHdr))
.setApp(new NvApp(appName != null ? appName : "app", appId, willStreamHdr))
.setBitrate(prefConfig.bitrate)
.setEnableSops(prefConfig.enableSops)
.enableLocalAudioPlayback(prefConfig.playHostAudio)
.setMaxPacketSize((remote || prefConfig.width <= 1920) ? 1024 : 1292)
.setRemote(remote)
.setMaxPacketSize(1392)
.setRemoteConfiguration(StreamConfiguration.STREAM_CFG_AUTO)
.setHevcBitratePercentageMultiplier(75)
.setHevcSupported(decoderRenderer.isHevcSupported())
.setEnableHdr(willStreamHdr)
@@ -366,7 +439,7 @@ public class Game extends Activity implements SurfaceHolder.Callback,
.build();
// Initialize the connection
conn = new NvConnection(host, uniqueId, config, PlatformBinding.getCryptoProvider(this));
conn = new NvConnection(host, uniqueId, config, PlatformBinding.getCryptoProvider(this), serverCert);
controllerHandler = new ControllerHandler(this, conn, this, prefConfig);
InputManager inputManager = (InputManager) getSystemService(Context.INPUT_SERVICE);
@@ -387,10 +460,11 @@ public class Game extends Activity implements SurfaceHolder.Callback,
if (prefConfig.onscreenController) {
// create virtual onscreen controller
virtualController = new VirtualController(conn,
virtualController = new VirtualController(controllerHandler,
(FrameLayout)streamView.getParent(),
this);
virtualController.refreshLayout();
virtualController.show();
}
if (prefConfig.usbDriver) {
@@ -415,19 +489,65 @@ public class Game extends Activity implements SurfaceHolder.Callback,
streamView.getHolder().addCallback(this);
}
@Override
public void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
if (virtualController != null) {
// Refresh layout of OSC for possible new screen size
virtualController.refreshLayout();
}
// Hide on-screen overlays in PiP mode
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
if (isInPictureInPictureMode()) {
isHidingOverlays = true;
if (virtualController != null) {
virtualController.hide();
}
performanceOverlayView.setVisibility(View.GONE);
notificationOverlayView.setVisibility(View.GONE);
}
else {
isHidingOverlays = false;
// Restore overlays to previous state when leaving PiP
if (virtualController != null) {
virtualController.show();
}
if (prefConfig.enablePerfOverlay) {
performanceOverlayView.setVisibility(View.VISIBLE);
}
notificationOverlayView.setVisibility(requestedNotificationOverlayVisibility);
}
}
}
@Override
public void onUserLeaveHint() {
super.onUserLeaveHint();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
if (prefConfig.enablePip && connected) {
enterPictureInPictureMode(
new PictureInPictureParams.Builder()
.setAspectRatio(new Rational(prefConfig.width, prefConfig.height))
.setSourceRectHint(new Rect(
streamView.getLeft(), streamView.getTop(),
streamView.getRight(), streamView.getBottom()))
.build());
try {
// This has thrown all sorts of weird exceptions on Samsung devices
// running Oreo. Just eat them and close gracefully on leave, rather
// than crashing.
enterPictureInPictureMode(
new PictureInPictureParams.Builder()
.setAspectRatio(new Rational(prefConfig.width, prefConfig.height))
.setSourceRectHint(new Rect(
streamView.getLeft(), streamView.getTop(),
streamView.getRight(), streamView.getBottom()))
.build());
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
@@ -440,8 +560,17 @@ public class Game extends Activity implements SurfaceHolder.Callback,
// Capture is lost when focus is lost, so it must be requested again
// when focus is regained.
if (inputCaptureProvider.isCapturingEnabled() && hasFocus) {
// Recapture the pointer if focus was regained
streamView.requestPointerCapture();
// Recapture the pointer if focus was regained. On Android Q,
// we have to delay a bit before requesting capture because otherwise
// we'll hit the "requestPointerCapture called for a window that has no focus"
// error and it will not actually capture the cursor.
Handler h = new Handler();
h.postDelayed(new Runnable() {
@Override
public void run() {
streamView.requestPointerCapture();
}
}, 500);
}
}
}
@@ -455,8 +584,7 @@ public class Game extends Activity implements SurfaceHolder.Callback,
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
Display.Mode bestMode = display.getMode();
for (Display.Mode candidate : display.getSupportedModes()) {
boolean refreshRateOk = candidate.getRefreshRate() >= bestMode.getRefreshRate() &&
candidate.getRefreshRate() < 63;
boolean refreshRateOk = candidate.getRefreshRate() >= bestMode.getRefreshRate();
boolean resolutionOk = candidate.getPhysicalWidth() >= bestMode.getPhysicalWidth() &&
candidate.getPhysicalHeight() >= bestMode.getPhysicalHeight() &&
candidate.getPhysicalWidth() <= 4096;
@@ -472,6 +600,13 @@ public class Game extends Activity implements SurfaceHolder.Callback,
}
}
// Ensure the frame rate stays around 60 Hz for <= 60 FPS streams
if (prefConfig.fps <= 60) {
if (candidate.getRefreshRate() >= 63) {
continue;
}
}
// Make sure the refresh rate doesn't regress
if (!refreshRateOk) {
continue;
@@ -489,12 +624,20 @@ public class Game extends Activity implements SurfaceHolder.Callback,
windowLayoutParams.preferredDisplayModeId = bestMode.getModeId();
displayRefreshRate = bestMode.getRefreshRate();
}
// On L, we can at least tell the OS that we want 60 Hz
// On L, we can at least tell the OS that we want a refresh rate
else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
float bestRefreshRate = display.getRefreshRate();
for (float candidate : display.getSupportedRefreshRates()) {
if (candidate > bestRefreshRate && candidate < 63) {
if (candidate > bestRefreshRate) {
LimeLog.info("Examining refresh rate: "+candidate);
// Ensure the frame rate stays around 60 Hz for <= 60 FPS streams
if (prefConfig.fps <= 60) {
if (candidate >= 63) {
continue;
}
}
bestRefreshRate = candidate;
}
}
@@ -618,7 +761,12 @@ public class Game extends Activity implements SurfaceHolder.Callback,
inputManager.unregisterInputDeviceListener(controllerHandler);
}
wifiLock.release();
if (lowLatencyWifiLock != null) {
lowLatencyWifiLock.release();
}
if (highPerfWifiLock != null) {
highPerfWifiLock.release();
}
if (connectedToUsbDriverService) {
// Unbind from the discovery service
@@ -636,6 +784,10 @@ public class Game extends Activity implements SurfaceHolder.Callback,
SpinnerDialog.closeDialogs(this);
Dialog.closeDialogs();
if (virtualController != null) {
virtualController.hide();
}
if (conn != null) {
int videoFormat = decoderRenderer.getActiveVideoFormat();
@@ -779,25 +931,30 @@ public class Game extends Activity implements SurfaceHolder.Callback,
@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
return handleKeyDown(event) || super.onKeyDown(keyCode, event);
}
@Override
public boolean handleKeyDown(KeyEvent event) {
// Pass-through virtual navigation keys
if ((event.getFlags() & KeyEvent.FLAG_VIRTUAL_HARD_KEY) != 0) {
return super.onKeyDown(keyCode, event);
return false;
}
// Handle a synthetic back button event that some Android OS versions
// create as a result of a right-click.
if (event.getSource() == InputDevice.SOURCE_MOUSE && event.getKeyCode() == KeyEvent.KEYCODE_BACK) {
// create as a result of a right-click. This event WILL repeat if
// the right mouse button is held down, so we ignore those.
if (!prefConfig.mouseNavButtons &&
(event.getSource() == InputDevice.SOURCE_MOUSE ||
event.getSource() == InputDevice.SOURCE_MOUSE_RELATIVE) &&
event.getKeyCode() == KeyEvent.KEYCODE_BACK) {
conn.sendMouseButtonDown(MouseButtonPacket.BUTTON_RIGHT);
return true;
}
boolean handled = false;
boolean detectedGamepad = event.getDevice() != null && ((event.getDevice().getSources() & InputDevice.SOURCE_JOYSTICK) == InputDevice.SOURCE_JOYSTICK ||
(event.getDevice().getSources() & InputDevice.SOURCE_GAMEPAD) == InputDevice.SOURCE_GAMEPAD);
if (detectedGamepad || (event.getDevice() == null ||
event.getDevice().getKeyboardType() != InputDevice.KEYBOARD_TYPE_ALPHABETIC
)) {
if (ControllerHandler.isGameControllerDevice(event.getDevice())) {
// Always try the controller handler first, unless it's an alphanumeric keyboard device.
// Otherwise, controller handler will eat keyboard d-pad events.
handled = controllerHandler.handleButtonDown(event);
@@ -807,20 +964,25 @@ public class Game extends Activity implements SurfaceHolder.Callback,
// Try the keyboard handler
short translated = KeyboardTranslator.translate(event.getKeyCode());
if (translated == 0) {
return super.onKeyDown(keyCode, event);
return false;
}
// Let this method take duplicate key down events
if (handleSpecialKeys(keyCode, true)) {
if (handleSpecialKeys(event.getKeyCode(), true)) {
return true;
}
// Pass through keyboard input if we're not grabbing
if (!grabbedInput) {
return super.onKeyDown(keyCode, event);
return false;
}
conn.sendKeyboardInput(translated, KeyboardPacket.KEY_DOWN, getModifierState(event));
byte modifiers = getModifierState(event);
if (KeyboardTranslator.needsShift(event.getKeyCode())) {
modifiers |= KeyboardPacket.MODIFIER_SHIFT;
conn.sendKeyboardInput((short) 0x8010, KeyboardPacket.KEY_DOWN, modifiers);
}
conn.sendKeyboardInput(translated, KeyboardPacket.KEY_DOWN, modifiers);
}
return true;
@@ -828,24 +990,28 @@ public class Game extends Activity implements SurfaceHolder.Callback,
@Override
public boolean onKeyUp(int keyCode, KeyEvent event) {
return handleKeyUp(event) || super.onKeyUp(keyCode, event);
}
@Override
public boolean handleKeyUp(KeyEvent event) {
// Pass-through virtual navigation keys
if ((event.getFlags() & KeyEvent.FLAG_VIRTUAL_HARD_KEY) != 0) {
return super.onKeyUp(keyCode, event);
return false;
}
// Handle a synthetic back button event that some Android OS versions
// create as a result of a right-click.
if (event.getSource() == InputDevice.SOURCE_MOUSE && event.getKeyCode() == KeyEvent.KEYCODE_BACK) {
if (!prefConfig.mouseNavButtons &&
(event.getSource() == InputDevice.SOURCE_MOUSE ||
event.getSource() == InputDevice.SOURCE_MOUSE_RELATIVE) &&
event.getKeyCode() == KeyEvent.KEYCODE_BACK) {
conn.sendMouseButtonUp(MouseButtonPacket.BUTTON_RIGHT);
return true;
}
boolean handled = false;
boolean detectedGamepad = event.getDevice() != null && ((event.getDevice().getSources() & InputDevice.SOURCE_JOYSTICK) == InputDevice.SOURCE_JOYSTICK ||
(event.getDevice().getSources() & InputDevice.SOURCE_GAMEPAD) == InputDevice.SOURCE_GAMEPAD);
if (detectedGamepad || (event.getDevice() == null ||
event.getDevice().getKeyboardType() != InputDevice.KEYBOARD_TYPE_ALPHABETIC
)) {
if (ControllerHandler.isGameControllerDevice(event.getDevice())) {
// Always try the controller handler first, unless it's an alphanumeric keyboard device.
// Otherwise, controller handler will eat keyboard d-pad events.
handled = controllerHandler.handleButtonUp(event);
@@ -855,19 +1021,26 @@ public class Game extends Activity implements SurfaceHolder.Callback,
// Try the keyboard handler
short translated = KeyboardTranslator.translate(event.getKeyCode());
if (translated == 0) {
return super.onKeyUp(keyCode, event);
return false;
}
if (handleSpecialKeys(keyCode, false)) {
if (handleSpecialKeys(event.getKeyCode(), false)) {
return true;
}
// Pass through keyboard input if we're not grabbing
if (!grabbedInput) {
return super.onKeyUp(keyCode, event);
return false;
}
conn.sendKeyboardInput(translated, KeyboardPacket.KEY_UP, getModifierState(event));
byte modifiers = getModifierState(event);
if (KeyboardTranslator.needsShift(event.getKeyCode())) {
modifiers |= KeyboardPacket.MODIFIER_SHIFT;
}
conn.sendKeyboardInput(translated, KeyboardPacket.KEY_UP, modifiers);
if (KeyboardTranslator.needsShift(event.getKeyCode())) {
conn.sendKeyboardInput((short) 0x8010, KeyboardPacket.KEY_UP, getModifierState(event));
}
}
return true;
@@ -907,6 +1080,7 @@ public class Game extends Activity implements SurfaceHolder.Callback,
{
// This case is for mice
if (event.getSource() == InputDevice.SOURCE_MOUSE ||
event.getSource() == InputDevice.SOURCE_MOUSE_RELATIVE ||
(event.getPointerCount() >= 1 &&
event.getToolType(0) == MotionEvent.TOOL_TYPE_MOUSE))
{
@@ -922,6 +1096,13 @@ public class Game extends Activity implements SurfaceHolder.Callback,
byte vScrollClicks = (byte) event.getAxisValue(MotionEvent.AXIS_VSCROLL);
conn.sendMouseScroll(vScrollClicks);
}
else if (event.getActionMasked() == MotionEvent.ACTION_HOVER_ENTER ||
event.getActionMasked() == MotionEvent.ACTION_HOVER_EXIT) {
// On some devices (Galaxy S8 without Oreo pointer capture), we can
// get spurious ACTION_HOVER_ENTER events when right clicking with
// incorrect X and Y coordinates. Just eat this event without processing it.
return true;
}
if ((changedButtons & MotionEvent.BUTTON_PRIMARY) != 0) {
if ((event.getButtonState() & MotionEvent.BUTTON_PRIMARY) != 0) {
@@ -950,6 +1131,26 @@ public class Game extends Activity implements SurfaceHolder.Callback,
}
}
if (prefConfig.mouseNavButtons) {
if ((changedButtons & MotionEvent.BUTTON_BACK) != 0) {
if ((event.getButtonState() & MotionEvent.BUTTON_BACK) != 0) {
conn.sendMouseButtonDown(MouseButtonPacket.BUTTON_X1);
}
else {
conn.sendMouseButtonUp(MouseButtonPacket.BUTTON_X1);
}
}
if ((changedButtons & MotionEvent.BUTTON_FORWARD) != 0) {
if ((event.getButtonState() & MotionEvent.BUTTON_FORWARD) != 0) {
conn.sendMouseButtonDown(MouseButtonPacket.BUTTON_X2);
}
else {
conn.sendMouseButtonUp(MouseButtonPacket.BUTTON_X2);
}
}
}
// Get relative axis values if we can
if (inputCaptureProvider.eventHasRelativeMouseAxes(event)) {
// Send the deltas straight from the motion event
@@ -961,13 +1162,14 @@ public class Game extends Activity implements SurfaceHolder.Callback,
lastMouseX = (int)event.getX();
lastMouseY = (int)event.getY();
}
else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
// We get a normal (non-relative) MotionEvent when starting pointer capture to synchronize the
// location of the cursor with our app. We don't want this, so we must discard this event.
lastMouseX = (int)event.getX();
lastMouseY = (int)event.getY();
}
else {
// First process the history
for (int i = 0; i < event.getHistorySize(); i++) {
updateMousePosition((int)event.getHistoricalX(i), (int)event.getHistoricalY(i));
}
// Now process the current values
// Don't process the history. We just want the current position now.
updateMousePosition((int)event.getX(), (int)event.getY());
}
@@ -1115,10 +1317,15 @@ public class Game extends Activity implements SurfaceHolder.Callback,
}
@Override
public void stageStarting(String stage) {
if (spinner != null) {
spinner.setMessage(getResources().getString(R.string.conn_starting)+" "+stage);
}
public void stageStarting(final String stage) {
runOnUiThread(new Runnable() {
@Override
public void run() {
if (spinner != null) {
spinner.setMessage(getResources().getString(R.string.conn_starting) + " " + stage);
}
}
});
}
@Override
@@ -1129,6 +1336,8 @@ public class Game extends Activity implements SurfaceHolder.Callback,
if (connecting || connected) {
connecting = connected = false;
controllerHandler.stop();
// Stop may take a few hundred ms to do some network I/O to tell
// the server we're going away and clean up. Let it run in a separate
// thread to keep things smooth for the UI. Inside moonlight-common,
@@ -1143,70 +1352,123 @@ public class Game extends Activity implements SurfaceHolder.Callback,
}
@Override
public void stageFailed(String stage, long errorCode) {
if (spinner != null) {
spinner.dismiss();
spinner = null;
}
public void stageFailed(final String stage, final long errorCode) {
runOnUiThread(new Runnable() {
@Override
public void run() {
if (spinner != null) {
spinner.dismiss();
spinner = null;
}
// Enable cursor visibility again
inputCaptureProvider.disableCapture();
if (!displayedFailureDialog) {
displayedFailureDialog = true;
LimeLog.severe(stage + " failed: " + errorCode);
if (!displayedFailureDialog) {
displayedFailureDialog = true;
LimeLog.severe(stage+" failed: "+errorCode);
// If video initialization failed and the surface is still valid, display extra information for the user
if (stage.contains("video") && streamView.getHolder().getSurface().isValid()) {
runOnUiThread(new Runnable() {
@Override
public void run() {
// If video initialization failed and the surface is still valid, display extra information for the user
if (stage.contains("video") && streamView.getHolder().getSurface().isValid()) {
Toast.makeText(Game.this, "Video decoder failed to initialize. Your device may not support the selected resolution.", Toast.LENGTH_LONG).show();
}
});
}
Dialog.displayDialog(this, getResources().getString(R.string.conn_error_title),
getResources().getString(R.string.conn_error_msg)+" "+stage, true);
}
Dialog.displayDialog(Game.this, getResources().getString(R.string.conn_error_title),
getResources().getString(R.string.conn_error_msg) + " " + stage, true);
}
}
});
}
@Override
public void connectionTerminated(long errorCode) {
// Enable cursor visibility again
inputCaptureProvider.disableCapture();
public void connectionTerminated(final long errorCode) {
runOnUiThread(new Runnable() {
@Override
public void run() {
// Let the display go to sleep now
getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
if (!displayedFailureDialog) {
displayedFailureDialog = true;
LimeLog.severe("Connection terminated: "+errorCode);
stopConnection();
// Enable cursor visibility again
inputCaptureProvider.disableCapture();
Dialog.displayDialog(this, getResources().getString(R.string.conn_terminated_title),
getResources().getString(R.string.conn_terminated_msg), true);
}
if (!displayedFailureDialog) {
displayedFailureDialog = true;
LimeLog.severe("Connection terminated: " + errorCode);
stopConnection();
// Display the error dialog if it was an unexpected termination.
// Otherwise, just finish the activity immediately.
if (errorCode != 0) {
Dialog.displayDialog(Game.this, getResources().getString(R.string.conn_terminated_title),
getResources().getString(R.string.conn_terminated_msg), true);
}
else {
finish();
}
}
}
});
}
@Override
public void connectionStatusUpdate(final int connectionStatus) {
runOnUiThread(new Runnable() {
@Override
public void run() {
if (prefConfig.disableWarnings) {
return;
}
if (connectionStatus == MoonBridge.CONN_STATUS_POOR) {
if (prefConfig.bitrate > 5000) {
notificationOverlayView.setText(getResources().getString(R.string.slow_connection_msg));
}
else {
notificationOverlayView.setText(getResources().getString(R.string.poor_connection_msg));
}
requestedNotificationOverlayVisibility = View.VISIBLE;
}
else if (connectionStatus == MoonBridge.CONN_STATUS_OKAY) {
requestedNotificationOverlayVisibility = View.GONE;
}
if (!isHidingOverlays) {
notificationOverlayView.setVisibility(requestedNotificationOverlayVisibility);
}
}
});
}
@Override
public void connectionStarted() {
if (spinner != null) {
spinner.dismiss();
spinner = null;
}
connecting = false;
connected = true;
runOnUiThread(new Runnable() {
@Override
public void run() {
// Hide the mouse cursor now. Doing it before
// dismissing the spinner seems to be undone
// when the spinner gets displayed.
inputCaptureProvider.enableCapture();
if (spinner != null) {
spinner.dismiss();
spinner = null;
}
connected = true;
connecting = false;
// Hide the mouse cursor now after a short delay.
// Doing it before dismissing the spinner seems to be undone
// when the spinner gets displayed. On Android Q, even now
// is too early to capture. We will delay a second to allow
// the spinner to dismiss before capturing.
Handler h = new Handler();
h.postDelayed(new Runnable() {
@Override
public void run() {
inputCaptureProvider.enableCapture();
}
}, 500);
// Keep the display on
getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
hideSystemUi(1000);
}
});
hideSystemUi(1000);
}
@Override
@@ -1232,13 +1494,20 @@ public class Game extends Activity implements SurfaceHolder.Callback,
}
@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
public void rumble(short controllerNumber, short lowFreqMotor, short highFreqMotor) {
LimeLog.info(String.format((Locale)null, "Rumble on gamepad %d: %04x %04x", controllerNumber, lowFreqMotor, highFreqMotor));
controllerHandler.handleRumble(controllerNumber, lowFreqMotor, highFreqMotor);
}
@Override
public void surfaceCreated(SurfaceHolder holder) {
if (!connected && !connecting) {
connecting = true;
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
if (!surfaceCreated) {
throw new IllegalStateException("Surface changed before creation!");
}
if (!attemptedConnection) {
attemptedConnection = true;
decoderRenderer.setRenderTarget(holder);
conn.start(PlatformBinding.getAudioRenderer(), decoderRenderer, Game.this);
@@ -1246,12 +1515,23 @@ public class Game extends Activity implements SurfaceHolder.Callback,
}
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
// Let the decoder know immediately that the surface is gone
decoderRenderer.prepareForStop();
public void surfaceCreated(SurfaceHolder holder) {
surfaceCreated = true;
}
if (connected) {
stopConnection();
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
if (!surfaceCreated) {
throw new IllegalStateException("Surface destroyed before creation!");
}
if (attemptedConnection) {
// Let the decoder know immediately that the surface is gone
decoderRenderer.prepareForStop();
if (connected) {
stopConnection();
}
}
}
@@ -1275,6 +1555,12 @@ public class Game extends Activity implements SurfaceHolder.Callback,
case EvdevListener.BUTTON_RIGHT:
buttonIndex = MouseButtonPacket.BUTTON_RIGHT;
break;
case EvdevListener.BUTTON_X1:
buttonIndex = MouseButtonPacket.BUTTON_X1;
break;
case EvdevListener.BUTTON_X2:
buttonIndex = MouseButtonPacket.BUTTON_X2;
break;
default:
LimeLog.warning("Unhandled button: "+buttonId);
return;
@@ -1333,4 +1619,14 @@ public class Game extends Activity implements SurfaceHolder.Callback,
hideSystemUi(2000);
}
}
@Override
public void onPerfUpdate(final String text) {
runOnUiThread(new Runnable() {
@Override
public void run() {
performanceOverlayView.setText(text);
}
});
}
}
+60 -31
View File
@@ -9,6 +9,7 @@ import com.limelight.binding.crypto.AndroidCryptoProvider;
import com.limelight.computers.ComputerManagerListener;
import com.limelight.computers.ComputerManagerService;
import com.limelight.grid.PcGridAdapter;
import com.limelight.grid.assets.DiskAssetLoader;
import com.limelight.nvstream.http.ComputerDetails;
import com.limelight.nvstream.http.NvApp;
import com.limelight.nvstream.http.NvHTTP;
@@ -28,6 +29,7 @@ import com.limelight.utils.ShortcutHelper;
import com.limelight.utils.UiHelper;
import android.app.Activity;
import android.app.ActivityManager;
import android.app.Service;
import android.content.ComponentName;
import android.content.Intent;
@@ -52,6 +54,8 @@ import android.widget.RelativeLayout;
import android.widget.Toast;
import android.widget.AdapterView.AdapterContextMenuInfo;
import org.xmlpull.v1.XmlPullParserException;
import javax.microedition.khronos.egl.EGLConfig;
import javax.microedition.khronos.opengles.GL10;
@@ -112,6 +116,7 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
private final static int DELETE_ID = 5;
private final static int RESUME_ID = 6;
private final static int QUIT_ID = 7;
private final static int VIEW_DETAILS_ID = 8;
private void initializeViews() {
setContentView(R.layout.activity_pc_view);
@@ -311,8 +316,8 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
ComputerObject computer = (ComputerObject) pcGridAdapter.getItem(info.position);
// Inflate the context menu
if (computer.details.reachability == ComputerDetails.Reachability.OFFLINE ||
computer.details.reachability == ComputerDetails.Reachability.UNKNOWN) {
if (computer.details.state == ComputerDetails.State.OFFLINE ||
computer.details.state == ComputerDetails.State.UNKNOWN) {
menu.add(Menu.NONE, WOL_ID, 1, getResources().getString(R.string.pcview_menu_send_wol));
menu.add(Menu.NONE, DELETE_ID, 2, getResources().getString(R.string.pcview_menu_delete_pc));
}
@@ -332,6 +337,7 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
// it with delete which actually work
menu.add(Menu.NONE, DELETE_ID, 4, getResources().getString(R.string.pcview_menu_delete_pc));
}
menu.add(Menu.NONE, VIEW_DETAILS_ID, 5, getResources().getString(R.string.pcview_menu_details));
}
@Override
@@ -343,7 +349,8 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
}
private void doPair(final ComputerDetails computer) {
if (computer.reachability == ComputerDetails.Reachability.OFFLINE) {
if (computer.state == ComputerDetails.State.OFFLINE ||
ServerHelper.getCurrentAddressFromComputer(computer) == null) {
Toast.makeText(PcView.this, getResources().getString(R.string.pair_pc_offline), Toast.LENGTH_SHORT).show();
return;
}
@@ -369,9 +376,9 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
httpConn = new NvHTTP(ServerHelper.getCurrentAddressFromComputer(computer),
managerBinder.getUniqueId(),
PlatformBinding.getDeviceName(),
computer.serverCert,
PlatformBinding.getCryptoProvider(PcView.this));
if (httpConn.getPairState() == PairingManager.PairState.PAIRED) {
if (httpConn.getPairState() == PairState.PAIRED) {
// Don't display any toast, but open the app list
message = null;
success = true;
@@ -383,21 +390,26 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
Dialog.displayDialog(PcView.this, getResources().getString(R.string.pair_pairing_title),
getResources().getString(R.string.pair_pairing_msg)+" "+pinStr, false);
PairingManager.PairState pairState = httpConn.pair(httpConn.getServerInfo(), pinStr);
if (pairState == PairingManager.PairState.PIN_WRONG) {
PairingManager pm = httpConn.getPairingManager();
PairState pairState = pm.pair(httpConn.getServerInfo(), pinStr);
if (pairState == PairState.PIN_WRONG) {
message = getResources().getString(R.string.pair_incorrect_pin);
}
else if (pairState == PairingManager.PairState.FAILED) {
else if (pairState == PairState.FAILED) {
message = getResources().getString(R.string.pair_fail);
}
else if (pairState == PairingManager.PairState.ALREADY_IN_PROGRESS) {
else if (pairState == PairState.ALREADY_IN_PROGRESS) {
message = getResources().getString(R.string.pair_already_in_progress);
}
else if (pairState == PairingManager.PairState.PAIRED) {
else if (pairState == PairState.PAIRED) {
// Just navigate to the app view without displaying a toast
message = null;
success = true;
// Pin this certificate for later HTTPS use
managerBinder.getComputer(computer.uuid).serverCert = pm.getPairedCert();
// Invalidate reachability information after pairing to force
// a refresh before reading pair state again
managerBinder.invalidateStateForComputer(computer.uuid);
@@ -411,7 +423,7 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
message = getResources().getString(R.string.error_unknown_host);
} catch (FileNotFoundException e) {
message = getResources().getString(R.string.error_404);
} catch (Exception e) {
} catch (XmlPullParserException | IOException e) {
e.printStackTrace();
message = e.getMessage();
}
@@ -429,7 +441,7 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
if (toastSuccess) {
// Open the app list after a successful pairing attempt
doAppList(computer);
doAppList(computer, true);
}
else {
// Start polling again if we're still in the foreground
@@ -452,7 +464,6 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
return;
}
Toast.makeText(PcView.this, getResources().getString(R.string.wol_waking_pc), Toast.LENGTH_SHORT).show();
new Thread(new Runnable() {
@Override
public void run() {
@@ -476,7 +487,8 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
}
private void doUnpair(final ComputerDetails computer) {
if (computer.reachability == ComputerDetails.Reachability.OFFLINE) {
if (computer.state == ComputerDetails.State.OFFLINE ||
ServerHelper.getCurrentAddressFromComputer(computer) == null) {
Toast.makeText(PcView.this, getResources().getString(R.string.error_pc_offline), Toast.LENGTH_SHORT).show();
return;
}
@@ -494,7 +506,7 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
try {
httpConn = new NvHTTP(ServerHelper.getCurrentAddressFromComputer(computer),
managerBinder.getUniqueId(),
PlatformBinding.getDeviceName(),
computer.serverCert,
PlatformBinding.getCryptoProvider(PcView.this));
if (httpConn.getPairState() == PairingManager.PairState.PAIRED) {
httpConn.unpair();
@@ -512,8 +524,9 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
message = getResources().getString(R.string.error_unknown_host);
} catch (FileNotFoundException e) {
message = getResources().getString(R.string.error_404);
} catch (Exception e) {
} catch (XmlPullParserException | IOException e) {
message = e.getMessage();
e.printStackTrace();
}
final String toastMessage = message;
@@ -527,8 +540,8 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
}).start();
}
private void doAppList(ComputerDetails computer) {
if (computer.reachability == ComputerDetails.Reachability.OFFLINE) {
private void doAppList(ComputerDetails computer, boolean newlyPaired) {
if (computer.state == ComputerDetails.State.OFFLINE) {
Toast.makeText(PcView.this, getResources().getString(R.string.error_pc_offline), Toast.LENGTH_SHORT).show();
return;
}
@@ -539,7 +552,8 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
Intent i = new Intent(this, AppView.class);
i.putExtra(AppView.NAME_EXTRA, computer.name);
i.putExtra(AppView.UUID_EXTRA, computer.uuid.toString());
i.putExtra(AppView.UUID_EXTRA, computer.uuid);
i.putExtra(AppView.NEW_PAIR_EXTRA, newlyPaired);
startActivity(i);
}
@@ -561,16 +575,24 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
return true;
case DELETE_ID:
if (managerBinder == null) {
Toast.makeText(PcView.this, getResources().getString(R.string.error_manager_not_running), Toast.LENGTH_LONG).show();
if (ActivityManager.isUserAMonkey()) {
LimeLog.info("Ignoring delete PC request from monkey");
return true;
}
managerBinder.removeComputer(computer.details.name);
removeComputer(computer.details);
UiHelper.displayDeletePcConfirmationDialog(this, computer.details, new Runnable() {
@Override
public void run() {
if (managerBinder == null) {
Toast.makeText(PcView.this, getResources().getString(R.string.error_manager_not_running), Toast.LENGTH_LONG).show();
return;
}
removeComputer(computer.details);
}
}, null);
return true;
case APP_LIST_ID:
doAppList(computer.details);
doAppList(computer.details, false);
return true;
case RESUME_ID:
@@ -592,25 +614,32 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
UiHelper.displayQuitConfirmationDialog(this, new Runnable() {
@Override
public void run() {
ServerHelper.doQuit(PcView.this,
ServerHelper.getCurrentAddressFromComputer(computer.details),
ServerHelper.doQuit(PcView.this, computer.details,
new NvApp("app", 0, false), managerBinder, null);
}
}, null);
return true;
case VIEW_DETAILS_ID:
Dialog.displayDialog(PcView.this, getResources().getString(R.string.title_details), computer.details.toString(), false);
return true;
default:
return super.onContextItemSelected(item);
}
}
private void removeComputer(ComputerDetails details) {
managerBinder.removeComputer(details);
new DiskAssetLoader(this).deleteAssetsForComputer(details.uuid);
for (int i = 0; i < pcGridAdapter.getCount(); i++) {
ComputerObject computer = (ComputerObject) pcGridAdapter.getItem(i);
if (details.equals(computer.details)) {
// Disable or delete shortcuts referencing this PC
shortcutHelper.disableShortcut(details.uuid.toString(),
shortcutHelper.disableComputerShortcut(details,
getResources().getString(R.string.scut_deleted_pc));
pcGridAdapter.removeComputer(computer);
@@ -641,7 +670,7 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
// Add a launcher shortcut for this PC
if (details.pairState == PairState.PAIRED) {
shortcutHelper.createAppViewShortcut(details.uuid.toString(), details, false);
shortcutHelper.createAppViewShortcutForOnlineHost(details);
}
if (existingEntry != null) {
@@ -675,15 +704,15 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
public void onItemClick(AdapterView<?> arg0, View arg1, int pos,
long id) {
ComputerObject computer = (ComputerObject) pcGridAdapter.getItem(pos);
if (computer.details.reachability == ComputerDetails.Reachability.UNKNOWN ||
computer.details.reachability == ComputerDetails.Reachability.OFFLINE) {
if (computer.details.state == ComputerDetails.State.UNKNOWN ||
computer.details.state == ComputerDetails.State.OFFLINE) {
// Open the context menu if a PC is offline or refreshing
openContextMenu(arg1);
} else if (computer.details.pairState != PairState.PAIRED) {
// Pair an unpaired machine by default
doPair(computer.details);
} else {
doAppList(computer.details);
doAppList(computer.details, false);
}
}
});
@@ -0,0 +1,107 @@
package com.limelight;
import android.content.ContentProvider;
import android.content.ContentResolver;
import android.content.ContentValues;
import android.content.UriMatcher;
import android.database.Cursor;
import android.net.Uri;
import android.os.ParcelFileDescriptor;
import com.limelight.grid.assets.DiskAssetLoader;
import java.io.File;
import java.io.FileNotFoundException;
import java.util.List;
public class PosterContentProvider extends ContentProvider {
public static final String AUTHORITY = "poster." + BuildConfig.APPLICATION_ID;
public static final String PNG_MIME_TYPE = "image/png";
public static final int APP_ID_PATH_INDEX = 2;
public static final int COMPUTER_UUID_PATH_INDEX = 1;
private DiskAssetLoader mDiskAssetLoader;
private static final UriMatcher sUriMatcher;
private static final String BOXART_PATH = "boxart";
private static final int BOXART_URI_ID = 1;
static {
sUriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
sUriMatcher.addURI(AUTHORITY, BOXART_PATH, BOXART_URI_ID);
}
@Override
public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException {
int match = sUriMatcher.match(uri);
if (match == BOXART_URI_ID) {
return openBoxArtFile(uri, mode);
}
return openBoxArtFile(uri, mode);
}
public ParcelFileDescriptor openBoxArtFile(Uri uri, String mode) throws FileNotFoundException {
if (!"r".equals(mode)) {
throw new UnsupportedOperationException("This provider is only for read mode");
}
List<String> segments = uri.getPathSegments();
if (segments.size() != 3) {
throw new FileNotFoundException();
}
String appId = segments.get(APP_ID_PATH_INDEX);
String uuid = segments.get(COMPUTER_UUID_PATH_INDEX);
File file = mDiskAssetLoader.getFile(uuid, Integer.parseInt(appId));
if (file.exists()) {
return ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY);
}
throw new FileNotFoundException();
}
@Override
public int delete(Uri uri, String selection, String[] selectionArgs) {
throw new UnsupportedOperationException("This provider is only for read mode");
}
@Override
public String getType(Uri uri) {
return PNG_MIME_TYPE;
}
@Override
public Uri insert(Uri uri, ContentValues values) {
throw new UnsupportedOperationException("This provider is only for read mode");
}
@Override
public boolean onCreate() {
mDiskAssetLoader = new DiskAssetLoader(getContext());
return true;
}
@Override
public Cursor query(Uri uri, String[] projection, String selection,
String[] selectionArgs, String sortOrder) {
throw new UnsupportedOperationException("This provider doesn't support query");
}
@Override
public int update(Uri uri, ContentValues values, String selection,
String[] selectionArgs) {
throw new UnsupportedOperationException("This provider is support read only");
}
public static Uri createBoxArtUri(String uuid, String appId) {
return new Uri.Builder()
.scheme(ContentResolver.SCHEME_CONTENT)
.authority(AUTHORITY)
.appendPath(BOXART_PATH)
.appendPath(uuid)
.appendPath(appId)
.build();
}
}
@@ -0,0 +1,277 @@
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 NvApp app;
private ArrayList<Intent> intentStack = new ArrayList<>();
private ComputerDetails computer;
private SpinnerDialog blockingLoadSpinner;
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(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.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 (app != null) {
if (details.runningGameId == 0 || details.runningGameId == app.getAppId()) {
intentStack.add(ServerHelper.createStartIntent(ShortcutTrampoline.this, app, 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, app, 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(null, 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(String uuidString, String appIdString) {
// Validate UUID
if (uuidString == null) {
Dialog.displayDialog(ShortcutTrampoline.this,
getResources().getString(R.string.conn_error_title),
getResources().getString(R.string.scut_invalid_uuid),
true);
return false;
}
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);
String appIdString = getIntent().getStringExtra(Game.EXTRA_APP_ID);
uuidString = getIntent().getStringExtra(AppView.UUID_EXTRA);
if (validateInput(uuidString, appIdString)) {
if (appIdString != null && !appIdString.isEmpty()) {
app = new NvApp(getIntent().getStringExtra(Game.EXTRA_APP_NAME),
Integer.parseInt(appIdString),
getIntent().getBooleanExtra(Game.EXTRA_APP_HDR, false));
}
// 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();
}
}
@@ -168,8 +168,17 @@ public class AndroidAudioRenderer implements AudioRenderer {
}
@Override
public void playDecodedAudio(byte[] audioData) {
track.write(audioData, 0, audioData.length);
public void playDecodedAudio(short[] audioData) {
// Only queue up to 40 ms of pending audio data in addition to what AudioTrack is buffering for us.
if (MoonBridge.getPendingAudioFrames() < 8) {
// This will block until the write is completed. That can cause a backlog
// of pending audio data, so we do the above check to be able to bound
// latency at 40 ms in that situation.
track.write(audioData, 0, audioData.length);
}
else {
LimeLog.info("Too many pending audio frames: " + MoonBridge.getPendingAudioFrames());
}
}
@Override
@@ -12,9 +12,8 @@ import java.security.KeyFactory;
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;
@@ -54,10 +53,7 @@ public class AndroidCryptoProvider implements LimelightCryptoProvider {
private static final Object globalCryptoLock = new Object();
static {
// Install the Bouncy Castle provider
Security.addProvider(new BouncyCastleProvider());
}
private static final Provider bcProvider = new BouncyCastleProvider();
public AndroidCryptoProvider(Context c) {
String dataPath = c.getFilesDir().getAbsolutePath();
@@ -96,10 +92,10 @@ public class AndroidCryptoProvider implements LimelightCryptoProvider {
}
try {
CertificateFactory certFactory = CertificateFactory.getInstance("X.509", "BC");
CertificateFactory certFactory = CertificateFactory.getInstance("X.509", bcProvider);
cert = (X509Certificate) certFactory.generateCertificate(new ByteArrayInputStream(certBytes));
pemCertBytes = certBytes;
KeyFactory keyFactory = KeyFactory.getInstance("RSA", "BC");
KeyFactory keyFactory = KeyFactory.getInstance("RSA", bcProvider);
key = (RSAPrivateKey) keyFactory.generatePrivate(new PKCS8EncodedKeySpec(keyBytes));
} catch (CertificateException e) {
// May happen if the cert is corrupt
@@ -113,10 +109,6 @@ public class AndroidCryptoProvider implements LimelightCryptoProvider {
// 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;
@@ -129,17 +121,13 @@ public class AndroidCryptoProvider implements LimelightCryptoProvider {
KeyPair keyPair;
try {
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA", "BC");
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA", bcProvider);
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();
@@ -160,13 +148,13 @@ public class AndroidCryptoProvider implements LimelightCryptoProvider {
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));
ContentSigner sigGen = new JcaContentSignerBuilder("SHA256withRSA").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;
throw new RuntimeException(e);
}
LimeLog.info("Generated a new key pair");
@@ -4,7 +4,11 @@ import android.content.Context;
import android.hardware.input.InputManager;
import android.hardware.usb.UsbDevice;
import android.hardware.usb.UsbManager;
import android.media.AudioAttributes;
import android.os.Build;
import android.os.SystemClock;
import android.os.VibrationEffect;
import android.os.Vibrator;
import android.util.SparseArray;
import android.view.InputDevice;
import android.view.InputEvent;
@@ -13,6 +17,7 @@ import android.view.MotionEvent;
import android.widget.Toast;
import com.limelight.LimeLog;
import com.limelight.binding.input.driver.AbstractController;
import com.limelight.binding.input.driver.UsbDriverListener;
import com.limelight.binding.input.driver.UsbDriverService;
import com.limelight.nvstream.NvConnection;
@@ -22,6 +27,7 @@ import com.limelight.preferences.PreferenceConfiguration;
import com.limelight.ui.GameGestures;
import com.limelight.utils.Vector2d;
import java.lang.reflect.InvocationTargetException;
import java.util.Timer;
import java.util.TimerTask;
@@ -49,6 +55,7 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
private final double stickDeadzone;
private final InputDeviceContext defaultContext = new InputDeviceContext();
private final GameGestures gestures;
private final Vibrator deviceVibrator;
private boolean hasGameController;
private final PreferenceConfiguration prefConfig;
@@ -59,10 +66,11 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
this.conn = conn;
this.gestures = gestures;
this.prefConfig = prefConfig;
this.deviceVibrator = (Vibrator) activityContext.getSystemService(Context.VIBRATOR_SERVICE);
// HACK: For now we're hardcoding a 10% deadzone. Some deadzone
// HACK: For now we're hardcoding a 7% deadzone. Some deadzone
// is required for controller batching support to work.
int deadzonePercentage = 10;
int deadzonePercentage = 7;
int[] ids = InputDevice.getDeviceIds();
for (int id : ids) {
@@ -149,6 +157,42 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
onInputDeviceAdded(deviceId);
}
public void stop() {
for (int i = 0; i < inputDeviceContexts.size(); i++) {
InputDeviceContext deviceContext = inputDeviceContexts.valueAt(i);
if (deviceContext.vibrator != null) {
deviceContext.vibrator.cancel();
}
}
deviceVibrator.cancel();
}
private static boolean hasJoystickAxes(InputDevice device) {
return (device.getSources() & InputDevice.SOURCE_JOYSTICK) == InputDevice.SOURCE_JOYSTICK &&
getMotionRangeForJoystickAxis(device, MotionEvent.AXIS_X) != null &&
getMotionRangeForJoystickAxis(device, MotionEvent.AXIS_Y) != null;
}
private static boolean hasGamepadButtons(InputDevice device) {
return (device.getSources() & InputDevice.SOURCE_GAMEPAD) == InputDevice.SOURCE_GAMEPAD;
}
public static boolean isGameControllerDevice(InputDevice device) {
if (device == null) {
return true;
}
if (hasJoystickAxes(device) || hasGamepadButtons(device)) {
// Has real joystick axes or gamepad buttons
return true;
}
// Otherwise, we'll try anything that claims to be a non-alphabetic keyboard
return device.getKeyboardType() != InputDevice.KEYBOARD_TYPE_ALPHABETIC;
}
public static short getAttachedControllerMask(Context context) {
int count = 0;
short mask = 0;
@@ -161,7 +205,7 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
continue;
}
if ((dev.getSources() & InputDevice.SOURCE_JOYSTICK) != 0) {
if (hasJoystickAxes(dev)) {
LimeLog.info("Counting InputDevice: "+dev.getName());
mask |= 1 << count++;
}
@@ -180,6 +224,11 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
}
}
if (PreferenceConfiguration.readPreferences(context).onscreenController) {
LimeLog.info("Counting OSC gamepad");
mask |= 1;
}
LimeLog.info("Enumerated "+count+" gamepads");
return mask;
}
@@ -272,10 +321,11 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
context.assignedControllerNumber = true;
}
private UsbDeviceContext createUsbDeviceContextForDevice(int deviceId) {
private UsbDeviceContext createUsbDeviceContextForDevice(AbstractController device) {
UsbDeviceContext context = new UsbDeviceContext();
context.id = deviceId;
context.id = device.getControllerId();
context.device = device;
context.leftStickDeadzoneRadius = (float) stickDeadzone;
context.rightStickDeadzoneRadius = (float) stickDeadzone;
@@ -284,6 +334,85 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
return context;
}
private static boolean isExternal(InputDevice dev) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
// Landroid/view/InputDevice;->isExternal()Z is officially public on Android Q
return dev.isExternal();
}
else {
try {
// Landroid/view/InputDevice;->isExternal()Z is on the light graylist in Android P
return (Boolean)dev.getClass().getMethod("isExternal").invoke(dev);
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
} catch (ClassCastException e) {
e.printStackTrace();
}
}
// Answer true if we don't know
return true;
}
private boolean shouldIgnoreBack(InputDevice dev) {
String devName = dev.getName();
// The Serval has a Select button but the framework doesn't
// know about that because it uses a non-standard scancode.
if (devName.contains("Razer Serval")) {
return true;
}
// Classify this device as a remote by name if it has no joystick axes
if (getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_X) == null &&
getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_Y) == null &&
devName.toLowerCase().contains("remote")) {
return true;
}
// Otherwise, dynamically try to determine whether we should allow this
// back button to function for navigation.
//
// First, check if this is an internal device we're being called on.
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT && !isExternal(dev)) {
InputManager im = (InputManager) activityContext.getSystemService(Context.INPUT_SERVICE);
boolean foundInternalGamepad = false;
boolean foundInternalSelect = false;
for (int id : im.getInputDeviceIds()) {
InputDevice currentDev = im.getInputDevice(id);
// Ignore external devices
if (currentDev == null || isExternal(currentDev)) {
continue;
}
// Note that we are explicitly NOT excluding the current device we're examining here,
// since the other gamepad buttons may be on our current device and that's fine.
boolean[] keys = currentDev.hasKeys(KeyEvent.KEYCODE_BUTTON_SELECT, KeyEvent.KEYCODE_BUTTON_A);
if (keys[0]) {
foundInternalSelect = true;
}
if (keys[1]) {
foundInternalGamepad = true;
}
}
// Allow the back button to function for navigation if we either:
// a) have no internal gamepad (most phones)
// b) have an internal gamepad but also have an internal select button (GPD XD)
// but not:
// c) have an internal gamepad but no internal select button (NVIDIA SHIELD Portable)
return !foundInternalGamepad || foundInternalSelect;
}
return false;
}
private InputDeviceContext createInputDeviceContextForDevice(InputDevice dev) {
InputDeviceContext context = new InputDeviceContext();
String devName = dev.getName();
@@ -294,6 +423,10 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
context.name = devName;
context.id = dev.getId();
if (dev.getVibrator().hasVibrator()) {
context.vibrator = dev.getVibrator();
}
context.leftStickXAxis = MotionEvent.AXIS_X;
context.leftStickYAxis = MotionEvent.AXIS_Y;
if (getMotionRangeForJoystickAxis(dev, context.leftStickXAxis) != null &&
@@ -416,6 +549,8 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
}
}
context.ignoreBack = shouldIgnoreBack(dev);
if (devName != null) {
// For the Nexus Player (and probably other ATV devices), we should
// use the back button as start since it doesn't have a start/menu button
@@ -435,30 +570,15 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
// so we increase the deadzone on them to minimize this
context.triggerDeadzone = 0.30f;
}
// Classify this device as a remote by name
else if (devName.toLowerCase().contains("remote")) {
// It's only a remote if it doesn't any sticks
if (!context.hasJoystickAxes) {
context.ignoreBack = true;
}
}
// SHIELD controllers will use small stick deadzones
else if (devName.contains("SHIELD")) {
context.leftStickDeadzoneRadius = 0.07f;
context.rightStickDeadzoneRadius = 0.07f;
}
// Samsung's face buttons appear as a non-virtual button so we'll explicitly ignore
// back presses on this device. The Goodix buttons on the Nokia 6 also appear
// non-virtual so we'll ignore those too.
else if (devName.equals("sec_touchscreen") || devName.equals("sec_touchkey") ||
devName.equals("goodix_fp")) {
context.ignoreBack = true;
}
// The Serval has a couple of unknown buttons that are start and select. It also has
// a back button which we want to ignore since there's already a select button.
else if (devName.contains("Razer Serval")) {
context.isServal = true;
context.ignoreBack = true;
}
// The Xbox One S Bluetooth controller has some mappings that need fixing up.
// However, Microsoft released a firmware update with no change to VID/PID
@@ -528,7 +648,7 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
private short getActiveControllerMask() {
if (prefConfig.multiController) {
return (short)(currentControllers | initialControllers);
return (short)(currentControllers | initialControllers | (prefConfig.onscreenController ? 1 : 0));
}
else {
// Only Player 1 is active with multi-controller disabled
@@ -835,16 +955,16 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
// to normal behavior, so ignore triggersIdleNegative for each trigger until
// first touch.
if (lt != 0) {
context.leftTriggerUsed = true;
context.leftTriggerAxisUsed = true;
}
if (rt != 0) {
context.rightTriggerUsed = true;
context.rightTriggerAxisUsed = true;
}
if (context.triggersIdleNegative) {
if (context.leftTriggerUsed) {
if (context.leftTriggerAxisUsed) {
lt = (lt + 1) / 2;
}
if (context.rightTriggerUsed) {
if (context.rightTriggerAxisUsed) {
rt = (rt + 1) / 2;
}
}
@@ -957,6 +1077,94 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
}
}
private void rumbleVibrator(Vibrator vibrator, short lowFreqMotor, short highFreqMotor) {
// Since we can only use a single amplitude value, compute the desired amplitude
// by taking 80% of the big motor and 33% of the small motor, then capping to 255.
// NB: This value is now 0-255 as required by VibrationEffect.
short lowFreqMotorMSB = (short)((lowFreqMotor >> 8) & 0xFF);
short highFreqMotorMSB = (short)((highFreqMotor >> 8) & 0xFF);
int simulatedAmplitude = Math.min(255, (int)((lowFreqMotorMSB * 0.80) + (highFreqMotorMSB * 0.33)));
if (simulatedAmplitude == 0) {
// This case is easy - just cancel the current effect and get out.
// NB: We cannot simply check lowFreqMotor == highFreqMotor == 0
// because our simulatedAmplitude could be 0 even though our inputs
// are not (ex: lowFreqMotor == 0 && highFreqMotor == 1).
vibrator.cancel();
return;
}
// Attempt to use amplitude-based control if we're on Oreo and the device
// supports amplitude-based vibration control.
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
if (vibrator.hasAmplitudeControl()) {
VibrationEffect effect = VibrationEffect.createOneShot(60000, simulatedAmplitude);
AudioAttributes audioAttributes = new AudioAttributes.Builder()
.setUsage(AudioAttributes.USAGE_GAME)
.build();
vibrator.vibrate(effect, audioAttributes);
return;
}
}
// If we reach this point, we don't have amplitude controls available, so
// we must emulate it by PWMing the vibration. Ick.
long pwmPeriod = 20;
long onTime = (long)((simulatedAmplitude / 255.0) * pwmPeriod);
long offTime = pwmPeriod - onTime;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
AudioAttributes audioAttributes = new AudioAttributes.Builder()
.setUsage(AudioAttributes.USAGE_GAME)
.build();
vibrator.vibrate(new long[]{0, onTime, offTime}, 0, audioAttributes);
}
else {
vibrator.vibrate(new long[]{0, onTime, offTime}, 0);
}
}
public void handleRumble(short controllerNumber, short lowFreqMotor, short highFreqMotor) {
boolean foundMatchingDevice = false;
boolean vibrated = false;
for (int i = 0; i < inputDeviceContexts.size(); i++) {
InputDeviceContext deviceContext = inputDeviceContexts.valueAt(i);
if (deviceContext.controllerNumber == controllerNumber) {
foundMatchingDevice = true;
if (deviceContext.vibrator != null) {
vibrated = true;
rumbleVibrator(deviceContext.vibrator, lowFreqMotor, highFreqMotor);
}
}
}
for (int i = 0; i < usbDeviceContexts.size(); i++) {
UsbDeviceContext deviceContext = usbDeviceContexts.valueAt(i);
if (deviceContext.controllerNumber == controllerNumber) {
foundMatchingDevice = vibrated = true;
deviceContext.device.rumble((short)lowFreqMotor, (short)highFreqMotor);
}
}
// We may decide to rumble the device for player 1
if (controllerNumber == 0) {
// If we didn't find a matching device, it must be the on-screen
// controls that triggered the rumble. Vibrate the device if
// the user has requested that behavior.
if (!foundMatchingDevice && prefConfig.onscreenController && !prefConfig.onlyL3R3 && prefConfig.vibrateOsc) {
rumbleVibrator(deviceVibrator, lowFreqMotor, highFreqMotor);
}
else if (foundMatchingDevice && !vibrated && prefConfig.vibrateFallbackToDevice) {
// We found a device to vibrate but it didn't have rumble support. The user
// has requested us to vibrate the device in this case.
rumbleVibrator(deviceVibrator, lowFreqMotor, highFreqMotor);
}
}
}
public boolean handleButtonUp(KeyEvent event) {
InputDeviceContext context = getContextForEvent(event);
if (context == null) {
@@ -1040,15 +1248,15 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
context.inputMap &= ~ControllerPacket.RS_CLK_FLAG;
break;
case KeyEvent.KEYCODE_BUTTON_L2:
if (context.leftTriggerAxis >= 0) {
// Suppress this digital event if an analog trigger is present
if (context.leftTriggerAxisUsed) {
// Suppress this digital event if an analog trigger is active
return true;
}
context.leftTrigger = 0;
break;
case KeyEvent.KEYCODE_BUTTON_R2:
if (context.rightTriggerAxis >= 0) {
// Suppress this digital event if an analog trigger is present
if (context.rightTriggerAxisUsed) {
// Suppress this digital event if an analog trigger is active
return true;
}
context.rightTrigger = 0;
@@ -1160,15 +1368,15 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
context.inputMap |= ControllerPacket.RS_CLK_FLAG;
break;
case KeyEvent.KEYCODE_BUTTON_L2:
if (context.leftTriggerAxis >= 0) {
// Suppress this digital event if an analog trigger is present
if (context.leftTriggerAxisUsed) {
// Suppress this digital event if an analog trigger is active
return true;
}
context.leftTrigger = (byte)0xFF;
break;
case KeyEvent.KEYCODE_BUTTON_R2:
if (context.rightTriggerAxis >= 0) {
// Suppress this digital event if an analog trigger is present
if (context.rightTriggerAxisUsed) {
// Suppress this digital event if an analog trigger is active
return true;
}
context.rightTrigger = (byte)0xFF;
@@ -1208,12 +1416,30 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
return true;
}
public void reportOscState(short buttonFlags,
short leftStickX, short leftStickY,
short rightStickX, short rightStickY,
byte leftTrigger, byte rightTrigger) {
defaultContext.leftStickX = leftStickX;
defaultContext.leftStickY = leftStickY;
defaultContext.rightStickX = rightStickX;
defaultContext.rightStickY = rightStickY;
defaultContext.leftTrigger = leftTrigger;
defaultContext.rightTrigger = rightTrigger;
defaultContext.inputMap = buttonFlags;
sendControllerInputPacket(defaultContext);
}
@Override
public void reportControllerState(int controllerId, short buttonFlags,
float leftStickX, float leftStickY,
float rightStickX, float rightStickY,
float leftTrigger, float rightTrigger) {
UsbDeviceContext context = usbDeviceContexts.get(controllerId);
GenericControllerContext context = usbDeviceContexts.get(controllerId);
if (context == null) {
return;
}
@@ -1248,19 +1474,19 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
}
@Override
public void deviceRemoved(int controllerId) {
UsbDeviceContext context = usbDeviceContexts.get(controllerId);
public void deviceRemoved(AbstractController controller) {
UsbDeviceContext context = usbDeviceContexts.get(controller.getControllerId());
if (context != null) {
LimeLog.info("Removed controller: "+controllerId);
LimeLog.info("Removed controller: "+controller.getControllerId());
releaseControllerNumber(context);
usbDeviceContexts.remove(controllerId);
usbDeviceContexts.remove(controller.getControllerId());
}
}
@Override
public void deviceAdded(int controllerId) {
UsbDeviceContext context = createUsbDeviceContextForDevice(controllerId);
usbDeviceContexts.put(controllerId, context);
public void deviceAdded(AbstractController controller) {
UsbDeviceContext context = createUsbDeviceContextForDevice(controller);
usbDeviceContexts.put(controller.getControllerId(), context);
}
class GenericControllerContext {
@@ -1289,6 +1515,7 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
class InputDeviceContext extends GenericControllerContext {
public String name;
public Vibrator vibrator;
public int leftStickXAxis = -1;
public int leftStickYAxis = -1;
@@ -1299,7 +1526,7 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
public int leftTriggerAxis = -1;
public int rightTriggerAxis = -1;
public boolean triggersIdleNegative;
public boolean leftTriggerUsed, rightTriggerUsed;
public boolean leftTriggerAxisUsed, rightTriggerAxisUsed;
public int hatXAxis = -1;
public int hatYAxis = -1;
@@ -1326,5 +1553,7 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
public long startDownTime = 0;
}
class UsbDeviceContext extends GenericControllerContext {}
class UsbDeviceContext extends GenericControllerContext {
public AbstractController device;
}
}
@@ -47,7 +47,21 @@ public class KeyboardTranslator {
public static final int VK_BACK_QUOTE = 192;
public static final int VK_QUOTE = 222;
public static final int VK_PAUSE = 19;
public static boolean needsShift(int keycode) {
switch (keycode)
{
case KeyEvent.KEYCODE_AT:
case KeyEvent.KEYCODE_POUND:
case KeyEvent.KEYCODE_PLUS:
case KeyEvent.KEYCODE_STAR:
return true;
default:
return false;
}
}
/**
* Translates the given keycode and returns the GFE keycode
* @param keycode the code to be translated
@@ -116,7 +130,8 @@ public class KeyboardTranslator {
case KeyEvent.KEYCODE_ENTER:
translated = 0x0d;
break;
case KeyEvent.KEYCODE_PLUS:
case KeyEvent.KEYCODE_EQUALS:
translated = 0xbb;
break;
@@ -257,7 +272,19 @@ public class KeyboardTranslator {
case KeyEvent.KEYCODE_NUMPAD_DOT:
translated = 0x6E;
break;
case KeyEvent.KEYCODE_AT:
translated = 2 + VK_0;
break;
case KeyEvent.KEYCODE_POUND:
translated = 3 + VK_0;
break;
case KeyEvent.KEYCODE_STAR:
translated = 8 + VK_0;
break;
default:
System.out.println("No key for "+keycode);
return 0;
@@ -43,11 +43,19 @@ public class AndroidNativePointerCaptureProvider extends InputCaptureProvider {
@Override
public float getRelativeAxisX(MotionEvent event) {
return event.getX();
float x = event.getX();
for (int i = 0; i < event.getHistorySize(); i++) {
x += event.getHistoricalX(i);
}
return x;
}
@Override
public float getRelativeAxisY(MotionEvent event) {
return event.getY();
float y = event.getY();
for (int i = 0; i < event.getHistorySize(); i++) {
y += event.getHistoricalY(i);
}
return y;
}
}
@@ -37,11 +37,13 @@ public abstract class AbstractController {
this.listener = listener;
}
public abstract void rumble(short lowFreqMotor, short highFreqMotor);
protected void notifyDeviceRemoved() {
listener.deviceRemoved(deviceId);
listener.deviceRemoved(this);
}
protected void notifyDeviceAdded() {
listener.deviceAdded(deviceId);
listener.deviceAdded(this);
}
}
@@ -30,6 +30,18 @@ public abstract class AbstractXboxController extends AbstractController {
private Thread createInputThread() {
return new Thread() {
public void run() {
try {
// Delay for a moment before reporting the new gamepad and
// accepting new input. This allows time for the old InputDevice
// to go away before we reclaim its spot. If the old device is still
// around when we call notifyDeviceAdded(), we won't be able to claim
// the controller number used by the original InputDevice.
Thread.sleep(1000);
} catch (InterruptedException e) {}
// Report that we're added _before_ reporting input
notifyDeviceAdded();
while (!isInterrupted() && !stopped) {
byte[] buffer = new byte[64];
@@ -114,9 +126,6 @@ public abstract class AbstractXboxController extends AbstractController {
return false;
}
// Report that we're added _before_ starting the input thread
notifyDeviceAdded();
// Start listening for controller input
inputThread = createInputThread();
inputThread.start();
@@ -131,6 +140,9 @@ public abstract class AbstractXboxController extends AbstractController {
stopped = true;
// Cancel any rumble effects
rumble((short)0, (short)0);
// Stop the input thread
if (inputThread != null) {
inputThread.interrupt();
@@ -6,6 +6,6 @@ public interface UsbDriverListener {
float rightStickX, float rightStickY,
float leftTrigger, float rightTrigger);
void deviceRemoved(int controllerId);
void deviceAdded(int controllerId);
void deviceRemoved(AbstractController controller);
void deviceAdded(AbstractController controller);
}
@@ -47,26 +47,21 @@ public class UsbDriverService extends Service implements UsbDriverListener {
}
@Override
public void deviceRemoved(int controllerId) {
public void deviceRemoved(AbstractController controller) {
// Remove the the controller from our list (if not removed already)
for (AbstractController controller : controllers) {
if (controller.getControllerId() == controllerId) {
controllers.remove(controller);
break;
}
}
controllers.remove(controller);
// Call through to the client's listener
if (listener != null) {
listener.deviceRemoved(controllerId);
listener.deviceRemoved(controller);
}
}
@Override
public void deviceAdded(int controllerId) {
public void deviceAdded(AbstractController controller) {
// Call through to the client's listener
if (listener != null) {
listener.deviceAdded(controllerId);
listener.deviceAdded(controller);
}
}
@@ -113,7 +108,7 @@ public class UsbDriverService extends Service implements UsbDriverListener {
// Report all controllerMap that already exist
if (listener != null) {
for (AbstractController controller : controllers) {
listener.deviceAdded(controller.getControllerId());
listener.deviceAdded(controller);
}
}
}
@@ -14,6 +14,7 @@ public class Xbox360Controller extends AbstractXboxController {
private static final int XB360_IFACE_PROTOCOL = 1; // Wired only
private static final int[] SUPPORTED_VENDORS = {
0x0079, // GPD Win 2
0x044f, // Thrustmaster
0x045e, // Microsoft
0x046d, // Logitech
@@ -23,6 +24,7 @@ public class Xbox360Controller extends AbstractXboxController {
0x07ff, // Mad Catz
0x0e6f, // Unknown
0x0f0d, // Hori
0x1038, // SteelSeries
0x11c9, // Nacon
0x12ab, // Unknown
0x1430, // RedOctane
@@ -137,4 +139,17 @@ public class Xbox360Controller extends AbstractXboxController {
// No need to fail init if the LED command fails
return true;
}
@Override
public void rumble(short lowFreqMotor, short highFreqMotor) {
byte[] data = {
0x00, 0x08, 0x00,
(byte)(lowFreqMotor >> 8), (byte)(highFreqMotor >> 8),
0x00, 0x00, 0x00
};
int res = connection.bulkTransfer(outEndpt, data, data.length, 100);
if (res != data.length) {
LimeLog.warning("Rumble transfer failed: "+res);
}
}
}
@@ -8,6 +8,7 @@ import com.limelight.LimeLog;
import com.limelight.nvstream.input.ControllerPacket;
import java.nio.ByteBuffer;
import java.util.Arrays;
public class XboxOneController extends AbstractXboxController {
@@ -23,8 +24,31 @@ public class XboxOneController extends AbstractXboxController {
0x24c6, // PowerA
};
// FIXME: odata_serial
private static final byte[] XB1_INIT_DATA = {0x05, 0x20, 0x00, 0x01, 0x00};
private static final byte[] FW2015_INIT = {0x05, 0x20, 0x00, 0x01, 0x00};
private static final byte[] HORI_INIT = {0x01, 0x20, 0x00, 0x09, 0x00, 0x04, 0x20, 0x3a,
0x00, 0x00, 0x00, (byte)0x80, 0x00};
private static final byte[] PDP_INIT1 = {0x0a, 0x20, 0x00, 0x03, 0x00, 0x01, 0x14};
private static final byte[] PDP_INIT2 = {0x06, 0x20, 0x00, 0x02, 0x01, 0x00};
private static final byte[] RUMBLE_INIT1 = {0x09, 0x00, 0x00, 0x09, 0x00, 0x0F, 0x00, 0x00,
0x1D, 0x1D, (byte)0xFF, 0x00, 0x00};
private static final byte[] RUMBLE_INIT2 = {0x09, 0x00, 0x00, 0x09, 0x00, 0x0F, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00};
private static InitPacket[] INIT_PKTS = {
new InitPacket(0x0e6f, 0x0165, HORI_INIT),
new InitPacket(0x0f0d, 0x0067, HORI_INIT),
new InitPacket(0x0000, 0x0000, FW2015_INIT),
new InitPacket(0x0e6f, 0x0000, PDP_INIT1),
new InitPacket(0x0e6f, 0x0000, PDP_INIT2),
new InitPacket(0x24c6, 0x541a, RUMBLE_INIT1),
new InitPacket(0x24c6, 0x542a, RUMBLE_INIT1),
new InitPacket(0x24c6, 0x543a, RUMBLE_INIT1),
new InitPacket(0x24c6, 0x541a, RUMBLE_INIT2),
new InitPacket(0x24c6, 0x542a, RUMBLE_INIT2),
new InitPacket(0x24c6, 0x543a, RUMBLE_INIT2),
};
private byte seqNum = 0;
public XboxOneController(UsbDevice device, UsbDeviceConnection connection, int deviceId, UsbDriverListener listener) {
super(device, connection, deviceId, listener);
@@ -111,13 +135,55 @@ public class XboxOneController extends AbstractXboxController {
@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;
// Send all applicable init packets
for (InitPacket pkt : INIT_PKTS) {
if (pkt.vendorId != 0 && device.getVendorId() != pkt.vendorId) {
continue;
}
if (pkt.productId != 0 && device.getProductId() != pkt.productId) {
continue;
}
byte[] data = Arrays.copyOf(pkt.data, pkt.data.length);
// Populate sequence number
data[2] = seqNum++;
// Send the initialization packet
int res = connection.bulkTransfer(outEndpt, data, data.length, 3000);
if (res != data.length) {
LimeLog.warning("Initialization transfer failed: "+res);
return false;
}
}
return true;
}
@Override
public void rumble(short lowFreqMotor, short highFreqMotor) {
byte[] data = {
0x09, 0x00, seqNum++, 0x09, 0x00,
0x0F, 0x00, 0x00,
(byte)(lowFreqMotor >> 9), (byte)(highFreqMotor >> 9),
(byte)0xFF, 0x00, (byte)0xFF
};
int res = connection.bulkTransfer(outEndpt, data, data.length, 100);
if (res != data.length) {
LimeLog.warning("Rumble transfer failed: "+res);
}
}
private static class InitPacket {
final int vendorId;
final int productId;
final byte[] data;
InitPacket(int vendorId, int productId, byte[] data) {
this.vendorId = vendorId;
this.productId = productId;
this.data = data;
}
}
}
@@ -4,6 +4,8 @@ public interface EvdevListener {
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);
@@ -210,7 +210,7 @@ public class AnalogStick extends VirtualControllerElement {
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
// calculate new radius sizes depending
radius_complete = getPercent(getCorrectWidth() / 2, 90);
radius_complete = getPercent(getCorrectWidth() / 2, 100);
radius_dead_zone = getPercent(getCorrectWidth() / 2, 30);
radius_analog_stick = getPercent(getCorrectWidth() / 2, 20);
@@ -338,6 +338,7 @@ public class AnalogStick extends VirtualControllerElement {
} else {
stick_state = STICK_STATE.NO_MOVEMENT;
notifyOnRevoke();
// not longer pressed reset analog stick
notifyOnMovement(0, 0);
}
@@ -13,10 +13,13 @@ import android.widget.RelativeLayout;
import android.widget.Toast;
import com.limelight.R;
import com.limelight.binding.input.ControllerHandler;
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 {
@@ -36,12 +39,14 @@ public class VirtualController {
private static final boolean _PRINT_DEBUG_INFORMATION = false;
private NvConnection connection = null;
private ControllerHandler controllerHandler;
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();
@@ -49,8 +54,8 @@ public class VirtualController {
private List<VirtualControllerElement> elements = new ArrayList<>();
public VirtualController(final NvConnection conn, FrameLayout layout, final Context context) {
this.connection = conn;
public VirtualController(final ControllerHandler controllerHandler, FrameLayout layout, final Context context) {
this.controllerHandler = controllerHandler;
this.frame_layout = layout;
this.context = context;
@@ -60,6 +65,7 @@ public class VirtualController {
buttonConfigure = new Button(context);
buttonConfigure.setAlpha(0.25f);
buttonConfigure.setFocusable(false);
buttonConfigure.setBackgroundResource(R.drawable.ic_settings);
buttonConfigure.setOnClickListener(new View.OnClickListener() {
@Override
@@ -86,6 +92,27 @@ public class VirtualController {
});
}
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);
@@ -140,32 +167,23 @@ public class VirtualController {
return inputContext;
}
public void sendControllerInputContext() {
sendControllerInputPacket();
}
void sendControllerInputContext() {
_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);
private void sendControllerInputPacket() {
try {
_DBG("INPUT_MAP + " + inputContext.inputMap);
_DBG("LEFT_TRIGGER " + inputContext.leftTrigger);
_DBG("RIGHT_TRIGGER " + inputContext.rightTrigger);
_DBG("LEFT STICK X: " + inputContext.leftStickX + " Y: " + inputContext.leftStickY);
_DBG("RIGHT STICK X: " + inputContext.rightStickX + " Y: " + inputContext.rightStickY);
_DBG("RIGHT STICK X: " + inputContext.rightStickX + " Y: " + inputContext.rightStickY);
if (connection != null) {
connection.sendControllerInput(
inputContext.inputMap,
inputContext.leftTrigger,
inputContext.rightTrigger,
inputContext.leftStickX,
inputContext.leftStickY,
inputContext.rightStickX,
inputContext.rightStickY
);
}
} catch (Exception e) {
e.printStackTrace();
if (controllerHandler != null) {
controllerHandler.reportOscState(
inputContext.inputMap,
inputContext.leftStickX,
inputContext.leftStickY,
inputContext.rightStickX,
inputContext.rightStickY,
inputContext.leftTrigger,
inputContext.rightTrigger
);
}
}
}
@@ -166,58 +166,54 @@ public abstract class VirtualControllerElement extends View {
}
protected void showConfigurationDialog() {
try {
AlertDialog.Builder alertBuilder = new AlertDialog.Builder(getContext());
AlertDialog.Builder alertBuilder = new AlertDialog.Builder(getContext());
alertBuilder.setTitle("Configuration");
alertBuilder.setTitle("Configuration");
CharSequence functions[] = new CharSequence[]{
"Move",
"Resize",
/*election
"Set n
Disable color sormal color",
"Set pressed color",
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;
}
*/
"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();
default: { // cancel
actionCancel();
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();
}
}
});
AlertDialog alert = alertBuilder.create();
// show menu
alert.show();
}
@Override
@@ -1,7 +1,6 @@
package com.limelight.binding.video;
import java.nio.ByteBuffer;
import java.util.Arrays;
import java.util.Locale;
import org.jcodec.codecs.h264.H264Utils;
@@ -9,10 +8,12 @@ import org.jcodec.codecs.h264.io.model.SeqParameterSet;
import org.jcodec.codecs.h264.io.model.VUIParameters;
import com.limelight.LimeLog;
import com.limelight.R;
import com.limelight.nvstream.av.video.VideoDecoderRenderer;
import com.limelight.nvstream.jni.MoonBridge;
import com.limelight.preferences.PreferenceConfiguration;
import android.content.Context;
import android.media.MediaCodec;
import android.media.MediaCodecInfo;
import android.media.MediaFormat;
@@ -39,9 +40,9 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
private boolean submittedCsd;
private boolean submitCsdNextCall;
private Context context;
private MediaCodec videoDecoder;
private Thread rendererThread;
private Thread[] spinnerThreads;
private boolean needsSpsBitstreamFixup, isExynos4;
private boolean adaptivePlayback, directSubmit;
private boolean constrainedHighProfile;
@@ -57,6 +58,7 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
private String glRenderer;
private boolean foreground = true;
private boolean legacyFrameDropRendering = false;
private PerfOverlayListener perfListener;
private boolean needsBaselineSpsHack;
private SeqParameterSet savedSps;
@@ -65,13 +67,11 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
private long initialExceptionTimestamp;
private static final int EXCEPTION_REPORT_DELAY_MS = 3000;
private VideoStats activeWindowVideoStats;
private VideoStats lastWindowVideoStats;
private VideoStats globalVideoStats;
private long lastTimestampUs;
private long decoderTimeMs;
private long totalTimeMs;
private int totalFramesReceived;
private int totalFramesRendered;
private int frameLossEvents;
private int framesLost;
private int lastFrameNumber;
private int refreshRate;
private PreferenceConfiguration prefs;
@@ -121,24 +121,22 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
this.renderTarget = renderTarget;
}
public MediaCodecDecoderRenderer(PreferenceConfiguration prefs,
public MediaCodecDecoderRenderer(Context context, PreferenceConfiguration prefs,
CrashListener crashListener, int consecutiveCrashCount,
boolean meteredData, boolean requestedHdr,
String glRenderer) {
String glRenderer, PerfOverlayListener perfListener) {
//dumpDecoders();
this.context = context;
this.prefs = prefs;
this.crashListener = crashListener;
this.consecutiveCrashCount = consecutiveCrashCount;
this.glRenderer = glRenderer;
this.perfListener = perfListener;
// Disable spinner threads in battery saver mode or 4K
if (prefs.batterySaver || prefs.height >= 2160) {
spinnerThreads = new Thread[0];
}
else {
spinnerThreads = new Thread[Runtime.getRuntime().availableProcessors()];
}
this.activeWindowVideoStats = new VideoStats();
this.lastWindowVideoStats = new VideoStats();
this.globalVideoStats = new VideoStats();
avcDecoder = findAvcDecoder();
if (avcDecoder != null) {
@@ -217,15 +215,10 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
}
public void notifyVideoForeground() {
startSpinnerThreads();
foreground = true;
}
public void notifyVideoBackground() {
// Signal the spinner threads to stop but
// don't wait for them to terminate to avoid
// delaying the state transition
signalSpinnerStop();
foreground = false;
}
@@ -326,7 +319,7 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
long delta = (renderTimeNanos / 1000000L) - (presentationTimeUs / 1000);
if (delta >= 0 && delta < 1000) {
if (USE_FRAME_RENDER_TIME) {
totalTimeMs += delta;
activeWindowVideoStats.totalTimeMs += delta;
}
}
}
@@ -420,23 +413,30 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
}
// Render the last buffer
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && !legacyFrameDropRendering) {
// Use a PTS that will cause this frame to never be dropped if frame dropping
// is disabled
videoDecoder.releaseOutputBuffer(lastIndex, 0);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
if (legacyFrameDropRendering) {
// Use a PTS that will cause this frame to be dropped if another comes in within
// the same V-sync period
videoDecoder.releaseOutputBuffer(lastIndex, System.nanoTime());
}
else {
// Use a PTS that will cause this frame to never be dropped if frame dropping
// is disabled
videoDecoder.releaseOutputBuffer(lastIndex, 0);
}
}
else {
videoDecoder.releaseOutputBuffer(lastIndex, true);
}
totalFramesRendered++;
activeWindowVideoStats.totalFramesRendered++;
// Add delta time to the totals (excluding probable outliers)
long delta = MediaCodecHelper.getMonotonicMillis() - (presentationTimeUs / 1000);
if (delta >= 0 && delta < 1000) {
decoderTimeMs += delta;
activeWindowVideoStats.decoderTimeMs += delta;
if (!USE_FRAME_RENDER_TIME) {
totalTimeMs += delta;
activeWindowVideoStats.totalTimeMs += delta;
}
}
} else {
@@ -462,73 +462,6 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
rendererThread.start();
}
private void startSpinnerThread(final int i) {
spinnerThreads[i] = new Thread() {
@Override
public void run() {
// This thread exists to keep the CPU at a higher DVFS state on devices
// where the governor scales clock speed sporadically, causing dropped frames.
//
// Run until we notice our thread has been removed from the spinner threads
// array. Even if we don't notice immediately, we'll notice soon enough.
// This will also ensure we terminate even if someone has restarted spinning
// before we realized we should stop.
while (this == spinnerThreads[i]) {
try {
Thread.sleep(0, 1);
} catch (InterruptedException e) {
break;
}
}
}
};
spinnerThreads[i].setName("Spinner-"+i);
spinnerThreads[i].setPriority(Thread.MIN_PRIORITY);
spinnerThreads[i].start();
}
private void startSpinnerThreads() {
LimeLog.info("Using "+spinnerThreads.length+" spinner threads");
for (int i = 0; i < spinnerThreads.length; i++) {
if (spinnerThreads[i] != null) {
continue;
}
startSpinnerThread(i);
}
}
private Thread[] signalSpinnerStop() {
// Capture current running threads
Thread[] runningThreads = Arrays.copyOf(spinnerThreads, spinnerThreads.length);
// Clear the spinner threads to signal their termination
for (int i = 0; i < spinnerThreads.length; i++) {
spinnerThreads[i] = null;
}
// Interrupt the threads
for (int i = 0; i < runningThreads.length; i++) {
if (runningThreads[i] != null) {
runningThreads[i].interrupt();
}
}
return runningThreads;
}
private void stopSpinnerThreads() {
// Signal and wait for the threads to stop
Thread[] runningThreads = signalSpinnerStop();
for (int i = 0; i < runningThreads.length; i++) {
if (runningThreads[i] != null) {
try {
runningThreads[i].join();
} catch (InterruptedException ignored) { }
}
}
}
private int dequeueInputBuffer() {
int index = -1;
long startTime;
@@ -570,7 +503,6 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
@Override
public void start() {
startRendererThread();
startSpinnerThreads();
}
// !!! May be called even if setup()/start() fails !!!
@@ -593,9 +525,6 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
try {
rendererThread.join();
} catch (InterruptedException ignored) { }
// Halt the spinner threads
stopSpinnerThreads();
}
@Override
@@ -664,17 +593,58 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
return MoonBridge.DR_OK;
}
totalFramesReceived++;
// We can receive the same "frame" multiple times if it's an IDR frame.
// In that case, each frame start NALU is submitted independently.
if (frameNumber != lastFrameNumber && frameNumber != lastFrameNumber + 1) {
framesLost += frameNumber - lastFrameNumber - 1;
frameLossEvents++;
if (lastFrameNumber == 0) {
activeWindowVideoStats.measurementStartTimestamp = System.currentTimeMillis();
} else if (frameNumber != lastFrameNumber && frameNumber != lastFrameNumber + 1) {
// We can receive the same "frame" multiple times if it's an IDR frame.
// In that case, each frame start NALU is submitted independently.
activeWindowVideoStats.framesLost += frameNumber - lastFrameNumber - 1;
activeWindowVideoStats.totalFrames += frameNumber - lastFrameNumber - 1;
activeWindowVideoStats.frameLossEvents++;
}
lastFrameNumber = frameNumber;
// Flip stats windows roughly every second
if (System.currentTimeMillis() >= activeWindowVideoStats.measurementStartTimestamp + 1000) {
if (prefs.enablePerfOverlay) {
VideoStats lastTwo = new VideoStats();
lastTwo.add(lastWindowVideoStats);
lastTwo.add(activeWindowVideoStats);
VideoStatsFps fps = lastTwo.getFps();
String decoder;
if ((videoFormat & MoonBridge.VIDEO_FORMAT_MASK_H264) != 0) {
decoder = avcDecoder.getName();
} else if ((videoFormat & MoonBridge.VIDEO_FORMAT_MASK_H265) != 0) {
decoder = hevcDecoder.getName();
} else {
decoder = "(unknown)";
}
float decodeTimeMs = (float)lastTwo.decoderTimeMs / lastTwo.totalFramesReceived;
String perfText = context.getString(
R.string.perf_overlay_text,
initialWidth + "x" + initialHeight,
decoder,
fps.totalFps,
fps.receivedFps,
fps.renderedFps,
(float)lastTwo.framesLost / lastTwo.totalFrames * 100,
((float)lastTwo.totalTimeMs / lastTwo.totalFramesReceived) - decodeTimeMs,
decodeTimeMs);
perfListener.onPerfUpdate(perfText);
}
globalVideoStats.add(activeWindowVideoStats);
lastWindowVideoStats.copy(activeWindowVideoStats);
activeWindowVideoStats.clear();
activeWindowVideoStats.measurementStartTimestamp = System.currentTimeMillis();
}
activeWindowVideoStats.totalFramesReceived++;
activeWindowVideoStats.totalFrames++;
int inputBufferIndex;
ByteBuffer buf;
@@ -682,7 +652,7 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
if (!FRAME_RENDER_TIME_ONLY) {
// Count time from first packet received to decode start
totalTimeMs += (timestampUs / 1000) - receiveTimeMs;
activeWindowVideoStats.totalTimeMs += (timestampUs / 1000) - receiveTimeMs;
}
if (timestampUs <= lastTimestampUs) {
@@ -718,7 +688,7 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
LimeLog.info("Patching level_idc to 31");
sps.levelIdc = 31;
}
if (initialWidth <= 1280 && initialHeight <= 720) {
else if (initialWidth <= 1280 && initialHeight <= 720) {
// Max 5 buffered frames at 1280x720x60
LimeLog.info("Patching level_idc to 32");
sps.levelIdc = 32;
@@ -989,17 +959,17 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
}
public int getAverageEndToEndLatency() {
if (totalFramesReceived == 0) {
if (globalVideoStats.totalFramesReceived == 0) {
return 0;
}
return (int)(totalTimeMs / totalFramesReceived);
return (int)(globalVideoStats.totalTimeMs / globalVideoStats.totalFramesReceived);
}
public int getAverageDecoderLatency() {
if (totalFramesReceived == 0) {
if (globalVideoStats.totalFramesReceived == 0) {
return 0;
}
return (int)(decoderTimeMs / totalFramesReceived);
return (int)(globalVideoStats.decoderTimeMs / globalVideoStats.totalFramesReceived);
}
static class DecoderHungException extends RuntimeException {
@@ -1060,9 +1030,9 @@ public class MediaCodecDecoderRenderer extends VideoDecoderRenderer {
str += "FPS target: "+renderer.refreshRate+"\n";
str += "Bitrate: "+renderer.prefs.bitrate+" Kbps \n";
str += "In stats: "+renderer.numVpsIn+", "+renderer.numSpsIn+", "+renderer.numPpsIn+"\n";
str += "Total frames received: "+renderer.totalFramesReceived+"\n";
str += "Total frames rendered: "+renderer.totalFramesRendered+"\n";
str += "Frame losses: "+renderer.framesLost+" in "+renderer.frameLossEvents+" loss events\n";
str += "Total frames received: "+renderer.globalVideoStats.totalFramesReceived+"\n";
str += "Total frames rendered: "+renderer.globalVideoStats.totalFramesRendered+"\n";
str += "Frame losses: "+renderer.globalVideoStats.framesLost+" in "+renderer.globalVideoStats.frameLossEvents+" loss events\n";
str += "Average end-to-end client latency: "+renderer.getAverageEndToEndLatency()+"ms\n";
str += "Average hardware decoder latency: "+renderer.getAverageDecoderLatency()+"ms\n";
@@ -71,8 +71,8 @@ public class MediaCodecHelper {
blacklistedDecoderPrefixes = new LinkedList<>();
// Blacklist software decoders that don't support H264 high profile,
// but exclude the official AOSP emulator from this restriction.
if (!Build.HARDWARE.equals("ranchu") || !Build.BRAND.equals("google")) {
// 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");
}
@@ -131,10 +131,11 @@ public class MediaCodecHelper {
}
// Sony ATVs have broken MediaTek codecs (decoder hangs after rendering the first frame).
// I know the Fire TV 2 works, so I'll just whitelist Amazon devices which seem
// to actually be tested. Ugh...
// 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
@@ -159,12 +160,16 @@ public class MediaCodecHelper {
// 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 Oreo.
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
// 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();
@@ -258,6 +263,18 @@ public class MediaCodecHelper {
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;
@@ -336,7 +353,7 @@ public class MediaCodecHelper {
// This device seems to crash constantly at 720p, so try disabling
// RFI to see if we can get that under control.
if (Build.PRODUCT.equalsIgnoreCase("b3_att_us")) {
if (Build.DEVICE.equals("b3") || Build.DEVICE.equals("b5")) {
return false;
}
@@ -457,6 +474,24 @@ public class MediaCodecHelper {
return null;
}
private static boolean isCodecBlacklisted(MediaCodecInfo codecInfo) {
// Use the new isSoftwareOnly() function on Android Q
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
if (codecInfo.isSoftwareOnly()) {
LimeLog.info("Skipping software-only decoder: "+codecInfo.getName());
return true;
}
}
// Check for explicitly blacklisted decoders
if (isDecoderInList(blacklistedDecoderPrefixes, codecInfo.getName())) {
LimeLog.info("Skipping blacklisted decoder: "+codecInfo.getName());
return true;
}
return false;
}
public static MediaCodecInfo findFirstDecoder(String mimeType) {
for (MediaCodecInfo codecInfo : getMediaCodecList()) {
@@ -465,15 +500,14 @@ public class MediaCodecHelper {
continue;
}
// Check for explicitly blacklisted decoders
if (isDecoderInList(blacklistedDecoderPrefixes, codecInfo.getName())) {
LimeLog.info("Skipping blacklisted decoder: "+codecInfo.getName());
continue;
}
// Find a decoder that supports the specified video format
for (String mime : codecInfo.getSupportedTypes()) {
if (mime.equalsIgnoreCase(mimeType)) {
// Skip blacklisted codecs
if (isCodecBlacklisted(codecInfo)) {
continue;
}
LimeLog.info("First decoder choice is "+codecInfo.getName());
return codecInfo;
}
@@ -513,17 +547,16 @@ public class MediaCodecHelper {
continue;
}
// Check for explicitly blacklisted decoders
if (isDecoderInList(blacklistedDecoderPrefixes, codecInfo.getName())) {
LimeLog.info("Skipping blacklisted decoder: "+codecInfo.getName());
continue;
}
// Find a decoder that supports the requested video format
for (String mime : codecInfo.getSupportedTypes()) {
if (mime.equalsIgnoreCase(mimeType)) {
LimeLog.info("Examining decoder capabilities of "+codecInfo.getName());
// Skip blacklisted codecs
if (isCodecBlacklisted(codecInfo)) {
continue;
}
CodecCapabilities caps = codecInfo.getCapabilitiesForType(mime);
if (requiredProfile != -1) {
@@ -0,0 +1,5 @@
package com.limelight.binding.video;
public interface PerfOverlayListener {
void onPerfUpdate(final String text);
}
@@ -0,0 +1,70 @@
package com.limelight.binding.video;
class VideoStats {
long decoderTimeMs;
long totalTimeMs;
int totalFrames;
int totalFramesReceived;
int totalFramesRendered;
int frameLossEvents;
int framesLost;
long measurementStartTimestamp;
void add(VideoStats other) {
this.decoderTimeMs += other.decoderTimeMs;
this.totalTimeMs += other.totalTimeMs;
this.totalFrames += other.totalFrames;
this.totalFramesReceived += other.totalFramesReceived;
this.totalFramesRendered += other.totalFramesRendered;
this.frameLossEvents += other.frameLossEvents;
this.framesLost += other.framesLost;
if (this.measurementStartTimestamp == 0) {
this.measurementStartTimestamp = other.measurementStartTimestamp;
}
assert other.measurementStartTimestamp <= this.measurementStartTimestamp;
}
void copy(VideoStats other) {
this.decoderTimeMs = other.decoderTimeMs;
this.totalTimeMs = other.totalTimeMs;
this.totalFrames = other.totalFrames;
this.totalFramesReceived = other.totalFramesReceived;
this.totalFramesRendered = other.totalFramesRendered;
this.frameLossEvents = other.frameLossEvents;
this.framesLost = other.framesLost;
this.measurementStartTimestamp = other.measurementStartTimestamp;
}
void clear() {
this.decoderTimeMs = 0;
this.totalTimeMs = 0;
this.totalFrames = 0;
this.totalFramesReceived = 0;
this.totalFramesRendered = 0;
this.frameLossEvents = 0;
this.framesLost = 0;
this.measurementStartTimestamp = 0;
}
VideoStatsFps getFps() {
float elapsed = (System.currentTimeMillis() - this.measurementStartTimestamp) / (float) 1000;
VideoStatsFps fps = new VideoStatsFps();
if (elapsed > 0) {
fps.totalFps = this.totalFrames / elapsed;
fps.receivedFps = this.totalFramesReceived / elapsed;
fps.renderedFps = this.totalFramesRendered / elapsed;
}
return fps;
}
}
class VideoStatsFps {
float totalFps;
float receivedFps;
float renderedFps;
}
@@ -1,13 +1,14 @@
package com.limelight.computers;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.io.ByteArrayInputStream;
import java.security.cert.CertificateEncodingException;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
import java.util.UUID;
import com.limelight.LimeLog;
import com.limelight.nvstream.http.ComputerDetails;
import android.content.ContentValues;
@@ -17,15 +18,15 @@ 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_DB_NAME = "computers3.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 static final String COMPUTER_NAME_COLUMN_NAME = "ComputerName";
private static final String ADDRESSES_COLUMN_NAME = "Addresses";
private static final String MAC_ADDRESS_COLUMN_NAME = "MacAddress";
private static final String SERVER_CERT_COLUMN_NAME = "ServerCert";
private static final String ADDRESS_PREFIX = "ADDRESS_PREFIX__";
private static final char ADDRESS_DELIMITER = ';';
private SQLiteDatabase computerDb;
@@ -38,86 +39,98 @@ public class ComputerDatabaseManager {
c.deleteDatabase(COMPUTER_DB_NAME);
computerDb = c.openOrCreateDatabase(COMPUTER_DB_NAME, 0, null);
}
initializeDb();
initializeDb(c);
}
public void close() {
computerDb.close();
}
private void initializeDb() {
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 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));
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, %s TEXT)",
COMPUTER_TABLE_NAME, COMPUTER_UUID_COLUMN_NAME, COMPUTER_NAME_COLUMN_NAME,
ADDRESSES_COLUMN_NAME, MAC_ADDRESS_COLUMN_NAME, SERVER_CERT_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);
}
oldComputers = LegacyDatabaseReader2.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 void deleteComputer(ComputerDetails details) {
computerDb.delete(COMPUTER_TABLE_NAME, COMPUTER_UUID_COLUMN_NAME+"=?", new String[]{details.uuid});
}
public boolean updateComputer(ComputerDetails details) {
ContentValues values = new ContentValues();
values.put(COMPUTER_UUID_COLUMN_NAME, details.uuid);
values.put(COMPUTER_NAME_COLUMN_NAME, details.name);
values.put(COMPUTER_UUID_COLUMN_NAME, details.uuid.toString());
values.put(LOCAL_IP_COLUMN_NAME, ADDRESS_PREFIX+details.localAddress);
values.put(REMOTE_IP_COLUMN_NAME, ADDRESS_PREFIX+details.remoteAddress);
values.put(MAC_COLUMN_NAME, details.macAddress);
StringBuilder addresses = new StringBuilder();
addresses.append(details.localAddress != null ? details.localAddress : "");
addresses.append(ADDRESS_DELIMITER).append(details.remoteAddress != null ? details.remoteAddress : "");
addresses.append(ADDRESS_DELIMITER).append(details.manualAddress != null ? details.manualAddress : "");
addresses.append(ADDRESS_DELIMITER).append(details.ipv6Address != null ? details.ipv6Address : "");
values.put(ADDRESSES_COLUMN_NAME, addresses.toString());
values.put(MAC_ADDRESS_COLUMN_NAME, details.macAddress);
try {
if (details.serverCert != null) {
values.put(SERVER_CERT_COLUMN_NAME, details.serverCert.getEncoded());
}
else {
values.put(SERVER_CERT_COLUMN_NAME, (byte[])null);
}
} catch (CertificateEncodingException e) {
values.put(SERVER_CERT_COLUMN_NAME, (byte[])null);
e.printStackTrace();
}
return -1 != computerDb.insertWithOnConflict(COMPUTER_TABLE_NAME, null, values, SQLiteDatabase.CONFLICT_REPLACE);
}
private static String readNonEmptyString(String input) {
if (input.isEmpty()) {
return null;
}
return input;
}
private ComputerDetails getComputerFromCursor(Cursor c) {
ComputerDetails details = new ComputerDetails();
details.name = c.getString(0);
details.uuid = c.getString(0);
details.name = c.getString(1);
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);
}
String[] addresses = c.getString(2).split(""+ADDRESS_DELIMITER, -1);
// 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);
}
}
details.localAddress = readNonEmptyString(addresses[0]);
details.remoteAddress = readNonEmptyString(addresses[1]);
details.manualAddress = readNonEmptyString(addresses[2]);
details.ipv6Address = readNonEmptyString(addresses[3]);
details.macAddress = c.getString(3);
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 local address for "+details.name);
}
}
byte[] derCertData = c.getBlob(4);
details.macAddress = c.getString(4);
if (derCertData != null) {
details.serverCert = (X509Certificate) CertificateFactory.getInstance("X.509")
.generateCertificate(new ByteArrayInputStream(derCertData));
}
} catch (CertificateException e) {
e.printStackTrace();
}
// This signifies we don't have dynamic state (like pair state)
details.state = ComputerDetails.State.UNKNOWN;
details.reachability = ComputerDetails.Reachability.UNKNOWN;
return details;
}
@@ -126,16 +139,7 @@ public class ComputerDatabaseManager {
Cursor c = computerDb.rawQuery("SELECT * FROM "+COMPUTER_TABLE_NAME, null);
LinkedList<ComputerDetails> computerList = new LinkedList<>();
while (c.moveToNext()) {
ComputerDetails details = getComputerFromCursor(c);
// If a field is corrupt or missing, skip the database entry
if (details.uuid == null || details.localAddress == null || details.remoteAddress == null ||
details.macAddress == null) {
continue;
}
computerList.add(details);
computerList.add(getComputerFromCursor(c));
}
c.close();
@@ -143,8 +147,8 @@ public class ComputerDatabaseManager {
return computerList;
}
public ComputerDetails getComputerByName(String name) {
Cursor c = computerDb.query(COMPUTER_TABLE_NAME, null, COMPUTER_NAME_COLUMN_NAME+"=?", new String[]{name}, null, null, null);
public ComputerDetails getComputerByUUID(String uuid) {
Cursor c = computerDb.query(COMPUTER_TABLE_NAME, null, COMPUTER_UUID_COLUMN_NAME+"=?", new String[]{ uuid }, null, null, null);
if (!c.moveToFirst()) {
// No matching computer
c.close();
@@ -154,13 +158,6 @@ public class ComputerDatabaseManager {
ComputerDetails details = getComputerFromCursor(c);
c.close();
// If a field is corrupt or missing, delete the database entry
if (details.uuid == null || details.localAddress == null || details.remoteAddress == null ||
details.macAddress == null) {
deleteComputer(details.name);
return null;
}
return details;
}
}
@@ -3,21 +3,22 @@ package com.limelight.computers;
import java.io.IOException;
import java.io.OutputStream;
import java.io.StringReader;
import java.net.InetAddress;
import java.net.Inet4Address;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.net.UnknownHostException;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicInteger;
import com.limelight.LimeLog;
import com.limelight.binding.PlatformBinding;
import com.limelight.discovery.DiscoveryService;
import com.limelight.nvstream.NvConnection;
import com.limelight.nvstream.http.ComputerDetails;
import com.limelight.nvstream.http.NvApp;
import com.limelight.nvstream.http.NvHTTP;
import com.limelight.nvstream.http.PairingManager;
import com.limelight.nvstream.mdns.MdnsComputer;
import com.limelight.nvstream.mdns.MdnsDiscoveryListener;
import com.limelight.utils.CacheHelper;
@@ -95,7 +96,6 @@ public class ComputerManagerService extends Service {
}
details.state = ComputerDetails.State.OFFLINE;
details.reachability = ComputerDetails.Reachability.OFFLINE;
}
} catch (InterruptedException e) {
releaseLocalDatabaseReference();
@@ -106,17 +106,27 @@ public class ComputerManagerService extends Service {
// If it's online, update our persistent state
if (details.state == ComputerDetails.State.ONLINE) {
if (!newPc) {
// Check if it's in the database because it could have been
// removed after this was issued
if (dbManager.getComputerByName(details.name) == null) {
// It's gone
releaseLocalDatabaseReference();
return false;
}
ComputerDetails existingComputer = dbManager.getComputerByUUID(details.uuid);
// Check if it's in the database because it could have been
// removed after this was issued
if (!newPc && existingComputer == null) {
// It's gone
releaseLocalDatabaseReference();
return false;
}
dbManager.updateComputer(details);
// If we already have an entry for this computer in the DB, we must
// combine the existing data with this new data (which may be partially available
// due to detecting the PC via mDNS) without the saved external address. If we
// write to the DB without doing this first, we can overwrite our existing data.
if (existingComputer != null) {
existingComputer.update(details);
dbManager.updateComputer(existingComputer);
}
else {
dbManager.updateComputer(details);
}
}
// Don't call the listener if this is a failed lookup of a new PC
@@ -156,7 +166,7 @@ public class ComputerManagerService extends Service {
}
}
};
t.setName("Polling thread for " + tuple.computer.localAddress);
t.setName("Polling thread for " + tuple.computer.name);
return t;
}
@@ -177,7 +187,6 @@ public class ComputerManagerService extends Service {
if (System.currentTimeMillis() - tuple.lastSuccessfulPollMs > POLL_DATA_TTL_MS) {
LimeLog.info("Timing out polled state for "+tuple.computer.name);
tuple.computer.state = ComputerDetails.State.UNKNOWN;
tuple.computer.reachability = ComputerDetails.Reachability.UNKNOWN;
}
// Report this computer initially
@@ -212,12 +221,12 @@ public class ComputerManagerService extends Service {
}
}
public boolean addComputerBlocking(String addr, boolean manuallyAdded) {
return ComputerManagerService.this.addComputerBlocking(addr, manuallyAdded);
public boolean addComputerBlocking(ComputerDetails fakeDetails) {
return ComputerManagerService.this.addComputerBlocking(fakeDetails);
}
public void removeComputer(String name) {
ComputerManagerService.this.removeComputer(name);
public void removeComputer(ComputerDetails computer) {
ComputerManagerService.this.removeComputer(computer);
}
public void stopPolling() {
@@ -233,7 +242,7 @@ public class ComputerManagerService extends Service {
return idManager.getUniqueId();
}
public ComputerDetails getComputer(UUID uuid) {
public ComputerDetails getComputer(String uuid) {
synchronized (pollingTuples) {
for (PollingTuple tuple : pollingTuples) {
if (uuid.equals(tuple.computer.uuid)) {
@@ -245,7 +254,7 @@ public class ComputerManagerService extends Service {
return null;
}
public void invalidateStateForComputer(UUID uuid) {
public void invalidateStateForComputer(String uuid) {
synchronized (pollingTuples) {
for (PollingTuple tuple : pollingTuples) {
if (uuid.equals(tuple.computer.uuid)) {
@@ -253,7 +262,6 @@ public class ComputerManagerService extends Service {
// from wiping this change out
synchronized (tuple.networkLock) {
tuple.computer.state = ComputerDetails.State.UNKNOWN;
tuple.computer.reachability = ComputerDetails.Reachability.UNKNOWN;
}
}
}
@@ -290,8 +298,27 @@ public class ComputerManagerService extends Service {
return new MdnsDiscoveryListener() {
@Override
public void notifyComputerAdded(MdnsComputer computer) {
ComputerDetails details = new ComputerDetails();
// Populate the computer template with mDNS info
if (computer.getLocalAddress() != null) {
details.localAddress = computer.getLocalAddress().getHostAddress();
// Since we're on the same network, we can use STUN to find
// our WAN address, which is also very likely the WAN address
// of the PC. We can use this later to connect remotely.
if (computer.getLocalAddress() instanceof Inet4Address) {
details.remoteAddress = NvConnection.findExternalAddressForMdns("stun.moonlight-stream.org", 3478);
}
}
if (computer.getIpv6Address() != null) {
details.ipv6Address = computer.getIpv6Address().getHostAddress();
}
// Kick off a serverinfo poll on this machine
addComputerBlocking(computer.getAddress().getHostAddress(), false);
if (!addComputerBlocking(details)) {
LimeLog.warning("Auto-discovered PC failed to respond: "+details);
}
}
@Override
@@ -307,22 +334,13 @@ public class ComputerManagerService extends Service {
};
}
private void addTuple(ComputerDetails details, boolean manuallyAdded) {
private void addTuple(ComputerDetails details) {
synchronized (pollingTuples) {
for (PollingTuple tuple : pollingTuples) {
// Check if this is the same computer
if (tuple.computer.uuid.equals(details.uuid)) {
if (manuallyAdded) {
// Update details anyway in case this machine has been re-added by IP
// after not being reachable by our existing information
tuple.computer.localAddress = details.localAddress;
tuple.computer.remoteAddress = details.remoteAddress;
}
else {
// This indicates that mDNS discovered this address, so we
// should only apply the local address.
tuple.computer.localAddress = details.localAddress;
}
// Update the saved computer with potentially new details
tuple.computer.update(details);
// Start a polling thread if polling is active
if (pollingActive && tuple.thread == null) {
@@ -347,15 +365,26 @@ public class ComputerManagerService extends Service {
}
}
public boolean addComputerBlocking(String addr, boolean manuallyAdded) {
// Setup a placeholder
ComputerDetails fakeDetails = new ComputerDetails();
fakeDetails.localAddress = addr;
fakeDetails.remoteAddress = addr;
public boolean addComputerBlocking(ComputerDetails fakeDetails) {
// Block while we try to fill the details
try {
runPoll(fakeDetails, true, 0);
// We cannot use runPoll() here because it will attempt to persist the state of the machine
// in the database, which would be bad because we don't have our pinned cert loaded yet.
if (pollComputer(fakeDetails)) {
// See if we have record of this PC to pull its pinned cert
synchronized (pollingTuples) {
for (PollingTuple tuple : pollingTuples) {
if (tuple.computer.uuid.equals(fakeDetails.uuid)) {
fakeDetails.serverCert = tuple.computer.serverCert;
break;
}
}
}
// Poll again, possibly with the pinned cert, to get accurate pairing information.
// This will insert the host into the database too.
runPoll(fakeDetails, true, 0);
}
} catch (InterruptedException e) {
return false;
}
@@ -365,30 +394,26 @@ public class ComputerManagerService extends Service {
LimeLog.info("New PC ("+fakeDetails.name+") is UUID "+fakeDetails.uuid);
// Start a polling thread for this machine
addTuple(fakeDetails, manuallyAdded);
addTuple(fakeDetails);
return true;
}
else {
if (!manuallyAdded) {
LimeLog.warning("Auto-discovered PC failed to respond: "+addr);
}
return false;
}
}
public void removeComputer(String name) {
public void removeComputer(ComputerDetails computer) {
if (!getLocalDatabaseReference()) {
return;
}
// Remove it from the database
dbManager.deleteComputer(name);
dbManager.deleteComputer(computer);
synchronized (pollingTuples) {
// Remove the computer from the computer list
for (PollingTuple tuple : pollingTuples) {
if (tuple.computer.name.equals(name)) {
if (tuple.computer.uuid.equals(computer.uuid)) {
if (tuple.thread != null) {
// Interrupt the thread on this entry
tuple.thread.interrupt();
@@ -425,21 +450,29 @@ public class ComputerManagerService extends Service {
}
try {
NvHTTP http = new NvHTTP(address, idManager.getUniqueId(),
null, PlatformBinding.getCryptoProvider(ComputerManagerService.this));
NvHTTP http = new NvHTTP(address, idManager.getUniqueId(), details.serverCert,
PlatformBinding.getCryptoProvider(ComputerManagerService.this));
ComputerDetails newDetails = http.getComputerDetails();
// Check if this is the PC we expected
if (details.uuid != null && newDetails.uuid != null &&
!details.uuid.equals(newDetails.uuid)) {
if (newDetails.uuid == null) {
LimeLog.severe("Polling returned no UUID!");
return null;
}
// details.uuid can be null on initial PC add
else if (details.uuid != null && !details.uuid.equals(newDetails.uuid)) {
// We got the wrong PC!
LimeLog.info("Polling returned the wrong PC!");
return null;
}
// Set the new active address
newDetails.activeAddress = address;
return newDetails;
} catch (Exception e) {
} catch (XmlPullParserException | IOException e) {
e.printStackTrace();
return null;
}
}
@@ -447,6 +480,11 @@ public class ComputerManagerService extends Service {
// Just try to establish a TCP connection to speculatively detect a running
// GFE server
private boolean fastPollIp(String address) {
if (address == null) {
// Don't bother if our address is null
return false;
}
Socket s = new Socket();
try {
s.connect(new InetSocketAddress(address, NvHTTP.HTTPS_PORT), FAST_POLL_TIMEOUT);
@@ -475,12 +513,16 @@ public class ComputerManagerService extends Service {
t.start();
}
private ComputerDetails.Reachability fastPollPc(final String localAddress, final String remoteAddress) throws InterruptedException {
private String fastPollPc(final String localAddress, final String remoteAddress, final String manualAddress, final String ipv6Address) throws InterruptedException {
final boolean[] remoteInfo = new boolean[2];
final boolean[] localInfo = new boolean[2];
final boolean[] manualInfo = new boolean[2];
final boolean[] ipv6Info = new boolean[2];
startFastPollThread(localAddress, localInfo);
startFastPollThread(remoteAddress, remoteInfo);
startFastPollThread(manualAddress, manualInfo);
startFastPollThread(ipv6Address, ipv6Info);
// Check local first
synchronized (localInfo) {
@@ -489,174 +531,90 @@ public class ComputerManagerService extends Service {
}
if (localInfo[1]) {
return ComputerDetails.Reachability.LOCAL;
return localAddress;
}
}
// Now remote
// Now manual
synchronized (manualInfo) {
while (!manualInfo[0]) {
manualInfo.wait(500);
}
if (manualInfo[1]) {
return manualAddress;
}
}
// Now remote IPv4
synchronized (remoteInfo) {
while (!remoteInfo[0]) {
remoteInfo.wait(500);
}
if (remoteInfo[1]) {
return ComputerDetails.Reachability.REMOTE;
return remoteAddress;
}
}
return ComputerDetails.Reachability.OFFLINE;
}
// Now global IPv6
synchronized (ipv6Info) {
while (!ipv6Info[0]) {
ipv6Info.wait(500);
}
private static boolean isAddressLikelyLocal(String str) {
try {
// This will tend to be wrong for IPv6 but falling back to
// remote will be fine in that case. For IPv4, it should be
// pretty accurate due to NAT prevalence.
InetAddress addr = InetAddress.getByName(str);
return addr.isSiteLocalAddress() || addr.isLinkLocalAddress();
} catch (UnknownHostException e) {
e.printStackTrace();
return false;
}
}
private ReachabilityTuple pollForReachability(ComputerDetails details) throws InterruptedException {
ComputerDetails polledDetails;
ComputerDetails.Reachability reachability;
if (details.localAddress.equals(details.remoteAddress)) {
reachability = isAddressLikelyLocal(details.localAddress) ?
ComputerDetails.Reachability.LOCAL : ComputerDetails.Reachability.REMOTE;
}
else {
// Do a TCP-level connection to the HTTP server to see if it's listening
LimeLog.info("Starting fast poll for "+details.name+" ("+details.localAddress +", "+details.remoteAddress +")");
reachability = fastPollPc(details.localAddress, details.remoteAddress);
LimeLog.info("Fast poll for "+details.name+" returned "+reachability.toString());
// If no connection could be established to either IP address, there's nothing we can do
if (reachability == ComputerDetails.Reachability.OFFLINE) {
return null;
if (ipv6Info[1]) {
return ipv6Address;
}
}
boolean localFirst = (reachability == ComputerDetails.Reachability.LOCAL);
if (localFirst) {
polledDetails = tryPollIp(details, details.localAddress);
}
else {
polledDetails = tryPollIp(details, details.remoteAddress);
}
String reachableAddr = null;
if (polledDetails == null && !details.localAddress.equals(details.remoteAddress)) {
// Failed, so let's try the fallback
if (!localFirst) {
polledDetails = tryPollIp(details, details.localAddress);
}
else {
polledDetails = tryPollIp(details, details.remoteAddress);
}
if (polledDetails != null) {
// The fallback poll worked
reachableAddr = !localFirst ? details.localAddress : details.remoteAddress;
}
}
else if (polledDetails != null) {
reachableAddr = localFirst ? details.localAddress : details.remoteAddress;
}
if (reachableAddr == null) {
return null;
}
// If both addresses are the same, guess whether we're local based on
// IP address heuristics.
if (reachableAddr.equals(polledDetails.localAddress) &&
reachableAddr.equals(polledDetails.remoteAddress)) {
polledDetails.reachability = isAddressLikelyLocal(reachableAddr) ?
ComputerDetails.Reachability.LOCAL : ComputerDetails.Reachability.REMOTE;
}
else if (polledDetails.remoteAddress.equals(reachableAddr)) {
polledDetails.reachability = ComputerDetails.Reachability.REMOTE;
}
else if (polledDetails.localAddress.equals(reachableAddr)) {
polledDetails.reachability = ComputerDetails.Reachability.LOCAL;
}
else {
polledDetails.reachability = ComputerDetails.Reachability.UNKNOWN;
}
return new ReachabilityTuple(polledDetails, reachableAddr);
return null;
}
private boolean pollComputer(ComputerDetails details) throws InterruptedException {
ReachabilityTuple initialReachTuple = pollForReachability(details);
if (initialReachTuple == null) {
ComputerDetails polledDetails;
// Do a TCP-level connection to the HTTP server to see if it's listening.
// Do not write this address to details.activeAddress because:
// a) it's only a candidate and may be wrong (multiple PCs behind a single router)
// b) if it's null, it will be unexpectedly nulling the activeAddress of a possibly online PC
LimeLog.info("Starting fast poll for "+details.name+" ("+details.localAddress +", "+details.remoteAddress +", "+details.manualAddress+", "+details.ipv6Address+")");
String candidateAddress = fastPollPc(details.localAddress, details.remoteAddress, details.manualAddress, details.ipv6Address);
LimeLog.info("Fast poll for "+details.name+" returned candidate address: "+candidateAddress);
// If no connection could be established to either IP address, there's nothing we can do
if (candidateAddress == null) {
return false;
}
if (initialReachTuple.computer.reachability == ComputerDetails.Reachability.UNKNOWN) {
// Neither IP address reported in the serverinfo response was the one we used.
// Poll again to see if we can contact this machine on either of its reported addresses.
ReachabilityTuple confirmationReachTuple = pollForReachability(initialReachTuple.computer);
if (confirmationReachTuple == null) {
// Neither of those seem to work, so we'll hold onto the address that did work
initialReachTuple.computer.localAddress = initialReachTuple.reachableAddress;
initialReachTuple.computer.reachability = ComputerDetails.Reachability.LOCAL;
}
else {
// We got it on one of the returned addresses; replace the original reach tuple
// with the new one
initialReachTuple = confirmationReachTuple;
// Try using the active address from fast-poll
polledDetails = tryPollIp(details, candidateAddress);
if (polledDetails == null) {
// If that failed, try all unique addresses except what we've
// already tried
HashSet<String> uniqueAddresses = new HashSet<>();
uniqueAddresses.add(details.localAddress);
uniqueAddresses.add(details.manualAddress);
uniqueAddresses.add(details.remoteAddress);
uniqueAddresses.add(details.ipv6Address);
for (String addr : uniqueAddresses) {
if (addr == null || addr.equals(candidateAddress)) {
continue;
}
polledDetails = tryPollIp(details, addr);
if (polledDetails != null) {
break;
}
}
}
// Save some details about the old state of the PC that we may wish
// to restore later.
String savedMacAddress = details.macAddress;
String savedLocalAddress = details.localAddress;
String savedRemoteAddress = details.remoteAddress;
// If we got here, it's reachable
details.update(initialReachTuple.computer);
// If the new MAC address is empty, restore the old one (workaround for GFE bug)
if (details.macAddress.equals("00:00:00:00:00:00") && savedMacAddress != null) {
LimeLog.info("MAC address was empty; using existing value: "+savedMacAddress);
details.macAddress = savedMacAddress;
if (polledDetails != null) {
details.update(polledDetails);
return true;
}
// We never want to lose IP addresses by polling server info. If we get a poll back
// where localAddress == remoteAddress but savedLocalAddress != savedRemoteAddress,
// then we've lost an address in the polling and we should restore the one that's missing.
if (details.localAddress.equals(details.remoteAddress) &&
!savedLocalAddress.equals(savedRemoteAddress)) {
if (details.localAddress.equals(savedLocalAddress)) {
// Local addresses are identical, so put the old remote address back
details.remoteAddress = savedRemoteAddress;
}
else if (details.remoteAddress.equals(savedRemoteAddress)) {
// Remote addresses are identical, so put the old local address back
details.localAddress = savedLocalAddress;
}
else {
// Neither IP address match. Let's restore the remote address to be safe.
details.remoteAddress = savedRemoteAddress;
}
// Now update the reachability so the correct address is used
if (details.localAddress.equals(initialReachTuple.reachableAddress)) {
details.reachability = ComputerDetails.Reachability.LOCAL;
}
else {
details.reachability = ComputerDetails.Reachability.REMOTE;
}
else {
return false;
}
return true;
}
@Override
@@ -679,7 +637,7 @@ public class ComputerManagerService extends Service {
for (ComputerDetails computer : dbManager.getAllComputers()) {
// Add tuples for each computer
addTuple(computer, true);
addTuple(computer);
}
releaseLocalDatabaseReference();
@@ -757,8 +715,9 @@ public class ComputerManagerService extends Service {
public void run() {
int emptyAppListResponses = 0;
do {
// Can't poll if it's not online
if (computer.state != ComputerDetails.State.ONLINE) {
// Can't poll if it's not online or paired
if (computer.state != ComputerDetails.State.ONLINE ||
computer.pairState != PairingManager.PairState.PAIRED) {
if (listener != null) {
listener.notifyComputerUpdated(computer);
}
@@ -774,7 +733,7 @@ public class ComputerManagerService extends Service {
try {
NvHTTP http = new NvHTTP(ServerHelper.getCurrentAddressFromComputer(computer), idManager.getUniqueId(),
null, PlatformBinding.getCryptoProvider(ComputerManagerService.this));
computer.serverCert, PlatformBinding.getCryptoProvider(ComputerManagerService.this));
String appList;
if (tuple != null) {
@@ -798,12 +757,12 @@ public class ComputerManagerService extends Service {
// in a row, we'll go ahead and believe it.
emptyAppListResponses++;
}
if (appList != null && !appList.isEmpty() &&
if (!appList.isEmpty() &&
(!list.isEmpty() || emptyAppListResponses >= EMPTY_LIST_THRESHOLD)) {
// Open the cache file
OutputStream cacheOut = null;
try {
cacheOut = CacheHelper.openCacheFileForOutput(getCacheDir(), "applist", computer.uuid.toString());
cacheOut = CacheHelper.openCacheFileForOutput(getCacheDir(), "applist", computer.uuid);
CacheHelper.writeStringToOutputStream(cacheOut, appList);
} catch (IOException e) {
e.printStackTrace();
@@ -830,7 +789,7 @@ public class ComputerManagerService extends Service {
listener.notifyComputerUpdated(computer);
}
}
else if (appList == null || appList.isEmpty()) {
else if (appList.isEmpty()) {
LimeLog.warning("Null app list received from "+computer.uuid);
}
} catch (IOException e) {
@@ -841,7 +800,7 @@ public class ComputerManagerService extends Service {
} while (waitPollingDelay());
}
};
thread.setName("App list polling thread for " + computer.localAddress);
thread.setName("App list polling thread for " + computer.name);
thread.start();
}
@@ -0,0 +1,105 @@
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;
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);
details.uuid = c.getString(1);
// 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);
}
}
}
@@ -0,0 +1,86 @@
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.nvstream.http.ComputerDetails;
import java.io.ByteArrayInputStream;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.util.LinkedList;
import java.util.List;
public class LegacyDatabaseReader2 {
private static final String COMPUTER_DB_NAME = "computers2.db";
private static final String COMPUTER_TABLE_NAME = "Computers";
private static ComputerDetails getComputerFromCursor(Cursor c) {
ComputerDetails details = new ComputerDetails();
details.uuid = c.getString(0);
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 column wasn't always present in the old schema
if (c.getColumnCount() >= 7) {
try {
byte[] derCertData = c.getBlob(6);
if (derCertData != null) {
details.serverCert = (X509Certificate) CertificateFactory.getInstance("X.509")
.generateCertificate(new ByteArrayInputStream(derCertData));
}
} catch (CertificateException e) {
e.printStackTrace();
}
}
// This signifies we don't have dynamic state (like pair state)
details.state = ComputerDetails.State.UNKNOWN;
return details;
}
public static List<ComputerDetails> getAllComputers(SQLiteDatabase computerDb) {
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 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);
}
}
}
@@ -1,7 +1,6 @@
package com.limelight.grid;
import android.app.Activity;
import android.graphics.BitmapFactory;
import android.widget.ImageView;
import android.widget.ProgressBar;
import android.widget.TextView;
@@ -46,13 +45,10 @@ public class AppGridAdapter extends GenericGridAdapter<AppView.AppObject> {
}
LimeLog.info("Art scaling divisor: " + scalingDivisor);
BitmapFactory.Options options = new BitmapFactory.Options();
options.inSampleSize = (int) scalingDivisor;
this.loader = new CachedAppAssetLoader(computer, scalingDivisor,
new NetworkAssetLoader(context, uniqueId),
new MemoryAssetLoader(),
new DiskAssetLoader(context.getCacheDir()));
new DiskAssetLoader(context));
}
public void cancelQueuedOperations() {
@@ -9,6 +9,7 @@ import android.widget.TextView;
import com.limelight.PcView;
import com.limelight.R;
import com.limelight.nvstream.http.ComputerDetails;
import com.limelight.nvstream.http.PairingManager;
import java.util.Collections;
import java.util.Comparator;
@@ -46,7 +47,7 @@ public class PcGridAdapter extends GenericGridAdapter<PcView.ComputerObject> {
imgView.setAlpha(0.4f);
}
if (obj.details.reachability == ComputerDetails.Reachability.UNKNOWN) {
if (obj.details.state == ComputerDetails.State.UNKNOWN) {
prgView.setVisibility(View.VISIBLE);
}
else {
@@ -77,6 +78,14 @@ public class PcGridAdapter extends GenericGridAdapter<PcView.ComputerObject> {
overlayView.setAlpha(0.4f);
return true;
}
// We must check if the status is exactly online and unpaired
// to avoid colliding with the loading spinner when status is unknown
else if (obj.details.state == ComputerDetails.State.ONLINE &&
obj.details.pairState == PairingManager.PairState.NOT_PAIRED) {
overlayView.setImageResource(R.drawable.ic_lock);
overlayView.setAlpha(1.0f);
return true;
}
return false;
}
}
@@ -1,7 +1,11 @@
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;
@@ -19,14 +23,23 @@ public class DiskAssetLoader {
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(File cacheDir) {
this.cacheDir = 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");
return CacheHelper.cacheFileExists(cacheDir, "boxart", tuple.computer.uuid, tuple.app.getAppId() + ".png");
}
// https://developer.android.com/topic/performance/graphics/load-bitmap.html
@@ -52,7 +65,7 @@ public class DiskAssetLoader {
}
public Bitmap loadBitmapFromCache(CachedAppAssetLoader.LoaderTuple tuple, int sampleSize) {
File file = CacheHelper.openPath(false, cacheDir, "boxart", tuple.computer.uuid.toString(), tuple.app.getAppId() + ".png");
File file = getFile(tuple.computer.uuid, tuple.app.getAppId());
// Don't bother with anything if it doesn't exist
if (!file.exists()) {
@@ -66,38 +79,79 @@ public class DiskAssetLoader {
return null;
}
// 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;
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);
}
}
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);
options.inPreferredConfig = Bitmap.Config.RGB_565;
options.inDither = true;
Bitmap 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 File getFile(String computerUuid, int appId) {
return CacheHelper.openPath(false, cacheDir, "boxart", computerUuid, appId + ".png");
}
public void deleteAssetsForComputer(String computerUuid) {
File dir = CacheHelper.openPath(false, cacheDir, "boxart", computerUuid);
File[] files = dir.listFiles();
if (files != null) {
for (File f : files) {
f.delete();
}
}
}
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");
out = CacheHelper.openCacheFileForOutput(cacheDir, "boxart", tuple.computer.uuid, tuple.app.getAppId() + ".png");
CacheHelper.writeInputStreamToOutputStream(input, out, MAX_ASSET_SIZE);
success = true;
} catch (IOException e) {
@@ -111,7 +165,7 @@ public class DiskAssetLoader {
if (!success) {
LimeLog.warning("Unable to populate cache with tuple: "+tuple);
CacheHelper.deleteCacheFile(cacheDir, "boxart", tuple.computer.uuid.toString(), tuple.app.getAppId() + ".png");
CacheHelper.deleteCacheFile(cacheDir, "boxart", tuple.computer.uuid, tuple.app.getAppId() + ".png");
}
}
}
@@ -16,7 +16,7 @@ public class MemoryAssetLoader {
};
private static String constructKey(CachedAppAssetLoader.LoaderTuple tuple) {
return tuple.computer.uuid.toString()+"-"+tuple.app.getAppId();
return tuple.computer.uuid+"-"+tuple.app.getAppId();
}
public Bitmap loadBitmapFromCache(CachedAppAssetLoader.LoaderTuple tuple) {
@@ -22,7 +22,8 @@ public class NetworkAssetLoader {
public InputStream getBitmapStream(CachedAppAssetLoader.LoaderTuple tuple) {
InputStream in = null;
try {
NvHTTP http = new NvHTTP(ServerHelper.getCurrentAddressFromComputer(tuple.computer), uniqueId, null, PlatformBinding.getCryptoProvider(context));
NvHTTP http = new NvHTTP(ServerHelper.getCurrentAddressFromComputer(tuple.computer), uniqueId,
tuple.computer.serverCert, PlatformBinding.getCryptoProvider(context));
in = http.getBoxArt(tuple.app);
} catch (IOException ignored) {}
@@ -11,6 +11,7 @@ import java.util.concurrent.LinkedBlockingQueue;
import com.limelight.computers.ComputerManagerService;
import com.limelight.R;
import com.limelight.nvstream.http.ComputerDetails;
import com.limelight.utils.Dialog;
import com.limelight.utils.SpinnerDialog;
import com.limelight.utils.UiHelper;
@@ -24,6 +25,7 @@ import android.content.ServiceConnection;
import android.os.Bundle;
import android.os.IBinder;
import android.view.KeyEvent;
import android.view.View;
import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputMethodManager;
import android.widget.TextView;
@@ -90,14 +92,22 @@ public class AddComputerManually extends Activity {
}
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);
try {
ComputerDetails details = new ComputerDetails();
details.manualAddress = host;
success = managerBinder.addComputerBlocking(details);
} catch (IllegalArgumentException e) {
// This can be thrown from OkHttp if the host fails to canonicalize to a valid name.
// https://github.com/square/okhttp/blob/okhttp_27/okhttp/src/main/java/com/squareup/okhttp/HttpUrl.java#L705
e.printStackTrace();
success = false;
}
if (!success){
wrongSiteLocal = isWrongSubnetSiteLocalAddress(host);
}
@@ -196,12 +206,7 @@ public class AddComputerManually extends Activity {
(keyEvent != null &&
keyEvent.getAction() == KeyEvent.ACTION_DOWN &&
keyEvent.getKeyCode() == KeyEvent.KEYCODE_ENTER)) {
if (hostText.getText().length() == 0) {
Toast.makeText(AddComputerManually.this, getResources().getString(R.string.addpc_enter_ip), Toast.LENGTH_LONG).show();
return true;
}
computersToAdd.add(hostText.getText().toString().trim());
return handleDoneEvent();
}
else if (actionId == EditorInfo.IME_ACTION_PREVIOUS) {
// This is how the Fire TV dismisses the keyboard
@@ -214,8 +219,28 @@ public class AddComputerManually extends Activity {
}
});
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;
}
}
@@ -3,7 +3,6 @@ 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;
@@ -7,7 +7,11 @@ 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 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";
@@ -24,24 +28,19 @@ public class PreferenceConfiguration {
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 BATTERY_SAVER_PREF_STRING = "checkbox_battery_saver";
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 ENABLE_PERF_OVERLAY_STRING = "checkbox_enable_perf_overlay";
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 String VIBRATE_OSC_PREF_STRING = "checkbox_vibrate_osc";
private static final String VIBRATE_FALLBACK_PREF_STRING = "checkbox_vibrate_fallback";
private static final int BITRATE_DEFAULT_360_30 = 1000;
private static final int BITRATE_DEFAULT_360_60 = 2000;
private static final int BITRATE_DEFAULT_720_30 = 5000;
private static final int BITRATE_DEFAULT_720_60 = 10000;
private static final int BITRATE_DEFAULT_1080_30 = 10000;
private static final int BITRATE_DEFAULT_1080_60 = 20000;
private static final int BITRATE_DEFAULT_4K_30 = 40000;
private static final int BITRATE_DEFAULT_4K_60 = 80000;
private static final String DEFAULT_RES_FPS = "720p60";
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;
@@ -55,12 +54,16 @@ public class PreferenceConfiguration {
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_BATTERY_SAVER = 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_ENABLE_PERF_OVERLAY = 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;
private static final boolean DEFAULT_VIBRATE_OSC = true;
private static final boolean DEFAULT_VIBRATE_FALLBACK = false;
public static final int FORCE_H265_ON = -1;
public static final int AUTOSELECT_H265 = 0;
@@ -75,41 +78,103 @@ public class PreferenceConfiguration {
public boolean listMode, smallIconMode, multiController, enable51Surround, usbDriver;
public boolean onscreenController;
public boolean onlyL3R3;
public boolean batterySaver;
public boolean disableFrameDrop;
public boolean enableHdr;
public boolean enablePip;
public boolean enablePerfOverlay;
public boolean bindAllUsb;
public boolean mouseEmulation;
public boolean mouseNavButtons;
public boolean unlockFps;
public boolean vibrateOsc;
public boolean vibrateFallbackToDevice;
public static int getDefaultBitrate(String resFpsString) {
if (resFpsString.equals("360p30")) {
return BITRATE_DEFAULT_360_30;
private static int getHeightFromResolutionString(String resString) {
if (resString.equalsIgnoreCase("360p")) {
return 360;
}
else if (resFpsString.equals("360p60")) {
return BITRATE_DEFAULT_360_60;
else if (resString.equalsIgnoreCase("480p")) {
return 480;
}
else if (resFpsString.equals("720p30")) {
return BITRATE_DEFAULT_720_30;
else if (resString.equalsIgnoreCase("720p")) {
return 720;
}
else if (resFpsString.equals("720p60")) {
return BITRATE_DEFAULT_720_60;
else if (resString.equalsIgnoreCase("1080p")) {
return 1080;
}
else if (resFpsString.equals("1080p30")) {
return BITRATE_DEFAULT_1080_30;
else if (resString.equalsIgnoreCase("1440p")) {
return 1440;
}
else if (resFpsString.equals("1080p60")) {
return BITRATE_DEFAULT_1080_60;
}
else if (resFpsString.equals("4K30")) {
return BITRATE_DEFAULT_4K_30;
}
else if (resFpsString.equals("4K60")) {
return BITRATE_DEFAULT_4K_60;
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) {
int height = getHeightFromResolutionString(resString);
if (height == 480) {
// This isn't an exact 16:9 resolution
return 854;
}
else {
return (height * 16) / 9;
}
}
private static String getResolutionString(int width, int height) {
switch (height) {
case 360:
return "360p";
case 480:
return "480p";
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));
}
else if (width * height <= 854 * 480) {
return (int)(1500 * (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));
}
}
@@ -135,7 +200,9 @@ public class PreferenceConfiguration {
public static int getDefaultBitrate(Context context) {
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
return getDefaultBitrate(prefs.getString(RES_FPS_PREF_STRING, DEFAULT_RES_FPS));
return getDefaultBitrate(
prefs.getString(RESOLUTION_PREF_STRING, DEFAULT_RESOLUTION),
prefs.getString(FPS_PREF_STRING, DEFAULT_FPS));
}
private static int getVideoFormatValue(Context context) {
@@ -163,9 +230,12 @@ public class PreferenceConfiguration {
prefs.edit()
.remove(BITRATE_PREF_STRING)
.remove(BITRATE_PREF_OLD_STRING)
.remove(RES_FPS_PREF_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();
}
@@ -173,59 +243,76 @@ public class PreferenceConfiguration {
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
PreferenceConfiguration config = new PreferenceConfiguration();
// 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 {
// 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));
}
// 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);
}
String str = prefs.getString(RES_FPS_PREF_STRING, DEFAULT_RES_FPS);
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;
}
config.videoFormat = getVideoFormatValue(context);
config.deadzonePercentage = prefs.getInt(DEADZONE_PREF_STRING, DEFAULT_DEADZONE);
@@ -244,12 +331,16 @@ public class PreferenceConfiguration {
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.batterySaver = prefs.getBoolean(BATTERY_SAVER_PREF_STRING, DEFAULT_BATTERY_SAVER);
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.enablePerfOverlay = prefs.getBoolean(ENABLE_PERF_OVERLAY_STRING, DEFAULT_ENABLE_PERF_OVERLAY);
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);
config.vibrateOsc = prefs.getBoolean(VIBRATE_OSC_PREF_STRING, DEFAULT_VIBRATE_OSC);
config.vibrateFallbackToDevice = prefs.getBoolean(VIBRATE_FALLBACK_PREF_STRING, DEFAULT_VIBRATE_FALLBACK);
return config;
}
@@ -6,6 +6,7 @@ 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;
@@ -24,6 +25,12 @@ import com.limelight.utils.UiHelper;
public class StreamSettings extends Activity {
private PreferenceConfiguration previousPrefs;
void reloadSettings() {
getFragmentManager().beginTransaction().replace(
R.id.stream_settings, new SettingsFragment()
).commit();
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
@@ -33,9 +40,7 @@ public class StreamSettings extends Activity {
UiHelper.setLocale(this);
setContentView(R.layout.activity_stream_settings);
getFragmentManager().beginTransaction().replace(
R.id.stream_settings, new SettingsFragment()
).commit();
reloadSettings();
UiHelper.notifyNewRootView(this);
}
@@ -58,12 +63,20 @@ public class StreamSettings extends Activity {
public static class SettingsFragment extends PreferenceFragment {
private static void removeResolution(ListPreference pref, String prefix) {
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().startsWith(prefix)) {
if (seq.toString().equalsIgnoreCase(value)) {
matchingCount++;
}
}
@@ -73,8 +86,8 @@ public class StreamSettings extends Activity {
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().startsWith(prefix)) {
// Skip matching prefixes
if (pref.getEntryValues()[i].toString().equalsIgnoreCase(value)) {
// Skip matching values
continue;
}
@@ -83,15 +96,34 @@ public class StreamSettings extends Activity {
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();
@@ -110,6 +142,8 @@ public class StreamSettings extends Activity {
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();
@@ -134,9 +168,16 @@ public class StreamSettings extends Activity {
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
@@ -186,20 +227,102 @@ public class StreamSettings extends Activity {
LimeLog.info("Maximum resolution slot: "+maxSupportedResW);
ListPreference resPref = (ListPreference) findPreference("list_resolution_fps");
if (maxSupportedResW != 0) {
if (maxSupportedResW < 3840) {
// 4K is unsupported
removeResolution(resPref, "4K");
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
removeResolution(resPref, "1080p");
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");
@@ -213,9 +336,12 @@ public class StreamSettings extends Activity {
// We must now ensure our display is compatible with HDR10
boolean foundHdr10 = false;
for (int hdrType : hdrCaps.getSupportedHdrTypes()) {
if (hdrType == Display.HdrCapabilities.HDR_TYPE_HDR10) {
foundHdr10 = true;
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;
}
}
}
@@ -229,18 +355,27 @@ public class StreamSettings extends Activity {
// 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;
@@ -3,15 +3,21 @@ 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);
}
@@ -52,4 +58,29 @@ public class StreamView extends SurfaceView {
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);
}
}
@@ -4,8 +4,10 @@ import android.app.Activity;
import android.content.Intent;
import android.widget.Toast;
import com.limelight.AppView;
import com.limelight.Game;
import com.limelight.R;
import com.limelight.ShortcutTrampoline;
import com.limelight.binding.PlatformBinding;
import com.limelight.computers.ComputerManagerService;
import com.limelight.nvstream.http.ComputerDetails;
@@ -13,13 +15,35 @@ import com.limelight.nvstream.http.GfeHttpResponseException;
import com.limelight.nvstream.http.NvApp;
import com.limelight.nvstream.http.NvHTTP;
import org.xmlpull.v1.XmlPullParserException;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.net.UnknownHostException;
import java.security.cert.CertificateEncodingException;
public class ServerHelper {
public static String getCurrentAddressFromComputer(ComputerDetails computer) {
return computer.reachability == ComputerDetails.Reachability.LOCAL ?
computer.localAddress : computer.remoteAddress;
return computer.activeAddress;
}
public static Intent createPcShortcutIntent(Activity parent, ComputerDetails computer) {
Intent i = new Intent(parent, ShortcutTrampoline.class);
i.putExtra(AppView.NAME_EXTRA, computer.name);
i.putExtra(AppView.UUID_EXTRA, computer.uuid);
i.setAction(Intent.ACTION_DEFAULT);
return i;
}
public static Intent createAppShortcutIntent(Activity parent, ComputerDetails computer, NvApp app) {
Intent i = new Intent(parent, ShortcutTrampoline.class);
i.putExtra(AppView.NAME_EXTRA, computer.name);
i.putExtra(AppView.UUID_EXTRA, computer.uuid);
i.putExtra(Game.EXTRA_APP_NAME, app.getAppName());
i.putExtra(Game.EXTRA_APP_ID, ""+app.getAppId());
i.putExtra(Game.EXTRA_APP_HDR, app.isHdrSupported());
i.setAction(Intent.ACTION_DEFAULT);
return i;
}
public static Intent createStartIntent(Activity parent, NvApp app, ComputerDetails computer,
@@ -30,20 +54,30 @@ public class ServerHelper {
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_STREAMING_REMOTE,
computer.reachability != ComputerDetails.Reachability.LOCAL);
intent.putExtra(Game.EXTRA_PC_UUID, computer.uuid.toString());
intent.putExtra(Game.EXTRA_PC_UUID, computer.uuid);
intent.putExtra(Game.EXTRA_PC_NAME, computer.name);
try {
if (computer.serverCert != null) {
intent.putExtra(Game.EXTRA_SERVER_CERT, computer.serverCert.getEncoded());
}
} catch (CertificateEncodingException e) {
e.printStackTrace();
}
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 ComputerDetails computer,
final NvApp app,
final ComputerManagerService.ComputerManagerBinder managerBinder,
final Runnable onComplete) {
@@ -54,8 +88,8 @@ public class ServerHelper {
NvHTTP httpConn;
String message;
try {
httpConn = new NvHTTP(address,
managerBinder.getUniqueId(), null, PlatformBinding.getCryptoProvider(parent));
httpConn = new NvHTTP(ServerHelper.getCurrentAddressFromComputer(computer),
managerBinder.getUniqueId(), computer.serverCert, PlatformBinding.getCryptoProvider(parent));
if (httpConn.quitApp()) {
message = parent.getResources().getString(R.string.applist_quit_success) + " " + app.getAppName();
} else {
@@ -74,8 +108,9 @@ public class ServerHelper {
message = parent.getResources().getString(R.string.error_unknown_host);
} catch (FileNotFoundException e) {
message = parent.getResources().getString(R.string.error_404);
} catch (Exception e) {
} catch (IOException | XmlPullParserException e) {
message = e.getMessage();
e.printStackTrace();
} finally {
if (onComplete != null) {
onComplete.run();
@@ -1,17 +1,19 @@
package com.limelight.utils;
import android.annotation.TargetApi;
import android.content.Context;
import android.app.Activity;
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.AppViewShortcutTrampoline;
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;
@@ -20,9 +22,10 @@ import java.util.List;
public class ShortcutHelper {
private final ShortcutManager sm;
private final Context context;
private final Activity context;
private final TvChannelHelper tvChannelHelper;
public ShortcutHelper(Context context) {
public ShortcutHelper(Activity context) {
this.context = context;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) {
sm = context.getSystemService(ShortcutManager.class);
@@ -30,6 +33,7 @@ public class ShortcutHelper {
else {
sm = null;
}
this.tvChannelHelper = new TvChannelHelper(context);
}
@TargetApi(Build.VERSION_CODES.N_MR1)
@@ -78,40 +82,39 @@ public class ShortcutHelper {
return false;
}
public void reportShortcutUsed(String id) {
public void reportComputerShortcutUsed(ComputerDetails computer) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) {
ShortcutInfo sinfo = getInfoForId(id);
if (sinfo != null) {
sm.reportShortcutUsed(id);
if (getInfoForId(computer.uuid) != null) {
sm.reportShortcutUsed(computer.uuid);
}
}
}
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, AppViewShortcutTrampoline.class);
i.putExtra(AppView.NAME_EXTRA, computerName);
i.putExtra(AppView.UUID_EXTRA, computerUuid);
i.setAction(Intent.ACTION_DEFAULT);
public void reportGameLaunched(ComputerDetails computer, NvApp app) {
tvChannelHelper.createTvChannel(computer);
tvChannelHelper.addGameToChannel(computer, app);
}
ShortcutInfo sinfo = new ShortcutInfo.Builder(context, id)
.setIntent(i)
.setShortLabel(computerName)
.setLongLabel(computerName)
public void createAppViewShortcut(ComputerDetails computer, boolean forceAdd, boolean newlyPaired) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) {
ShortcutInfo sinfo = new ShortcutInfo.Builder(context, computer.uuid)
.setIntent(ServerHelper.createPcShortcutIntent(context, computer))
.setShortLabel(computer.name)
.setLongLabel(computer.name)
.setIcon(Icon.createWithResource(context, R.mipmap.ic_pc_scut))
.build();
ShortcutInfo existingSinfo = getInfoForId(id);
ShortcutInfo existingSinfo = getInfoForId(computer.uuid);
if (existingSinfo != null) {
// Update in place
sm.updateShortcuts(Collections.singletonList(sinfo));
sm.enableShortcuts(Collections.singletonList(id));
sm.enableShortcuts(Collections.singletonList(computer.uuid));
}
// 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)) {
if (!isExistingDynamicShortcut(computer.uuid)) {
// 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.
@@ -121,16 +124,70 @@ public class ShortcutHelper {
}
}
}
if (newlyPaired) {
// Avoid hammering the channel API for each computer poll because it will throttle us
tvChannelHelper.createTvChannel(computer);
tvChannelHelper.requestChannelOnHomeScreen(computer);
}
}
public void createAppViewShortcut(String id, ComputerDetails details, boolean forceAdd) {
createAppViewShortcut(id, details.name, details.uuid.toString(), forceAdd);
public void createAppViewShortcutForOnlineHost(ComputerDetails details) {
createAppViewShortcut(details, false, false);
}
public void disableShortcut(String id, CharSequence reason) {
private String getShortcutIdForGame(ComputerDetails computer, NvApp app) {
return computer.uuid + app.getAppId();
}
@TargetApi(Build.VERSION_CODES.O)
public boolean createPinnedGameShortcut(ComputerDetails computer, NvApp app, Bitmap iconBits) {
if (sm.isRequestPinShortcutSupported()) {
Icon appIcon;
if (iconBits != null) {
appIcon = Icon.createWithAdaptiveBitmap(iconBits);
} else {
appIcon = Icon.createWithResource(context, R.mipmap.ic_pc_scut);
}
ShortcutInfo sInfo = new ShortcutInfo.Builder(context, getShortcutIdForGame(computer, app))
.setIntent(ServerHelper.createAppShortcutIntent(context, computer, app))
.setShortLabel(app.getAppName() + " (" + computer.name + ")")
.setIcon(appIcon)
.build();
return sm.requestPinShortcut(sInfo, null);
} else {
return false;
}
}
public void disableComputerShortcut(ComputerDetails computer, CharSequence reason) {
tvChannelHelper.deleteChannel(computer);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) {
ShortcutInfo sinfo = getInfoForId(id);
if (sinfo != null) {
// Delete the computer shortcut itself
if (getInfoForId(computer.uuid) != null) {
sm.disableShortcuts(Collections.singletonList(computer.uuid), reason);
}
// Delete all associated app shortcuts too
List<ShortcutInfo> shortcuts = getAllShortcuts();
LinkedList<String> appShortcutIds = new LinkedList<>();
for (ShortcutInfo info : shortcuts) {
if (info.getId().startsWith(computer.uuid)) {
appShortcutIds.add(info.getId());
}
}
sm.disableShortcuts(appShortcutIds, reason);
}
}
public void disableAppShortcut(ComputerDetails computer, NvApp app, CharSequence reason) {
tvChannelHelper.deleteProgram(computer, app);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) {
String id = getShortcutIdForGame(computer, app);
if (getInfoForId(id) != null) {
sm.disableShortcuts(Collections.singletonList(id), reason);
}
}
@@ -0,0 +1,345 @@
package com.limelight.utils;
import android.annotation.TargetApi;
import android.app.Activity;
import android.content.ActivityNotFoundException;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.database.Cursor;
import android.database.sqlite.SQLiteException;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.drawable.Drawable;
import android.media.tv.TvContract;
import android.net.Uri;
import android.os.Build;
import com.limelight.LimeLog;
import com.limelight.PosterContentProvider;
import com.limelight.R;
import com.limelight.nvstream.http.ComputerDetails;
import com.limelight.nvstream.http.NvApp;
import java.io.IOException;
import java.io.OutputStream;
public class TvChannelHelper {
private static final int ASPECT_RATIO_MOVIE_POSTER = 5;
private static final int TYPE_GAME = 12;
private static final int INTERNAL_PROVIDER_ID_INDEX = 1;
private static final int PROGRAM_BROWSABLE_INDEX = 2;
private static final int ID_INDEX = 0;
private Activity context;
public TvChannelHelper(Activity context) {
this.context = context;
}
void requestChannelOnHomeScreen(ComputerDetails computer) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
if (!isAndroidTV()) {
return;
}
Long channelId = getChannelId(computer.uuid);
if (channelId == null) {
return;
}
Intent intent = new Intent(TvContract.ACTION_REQUEST_CHANNEL_BROWSABLE);
intent.putExtra(TvContract.EXTRA_CHANNEL_ID, getChannelId(computer.uuid));
try {
context.startActivityForResult(intent, 0);
} catch (ActivityNotFoundException e) {
}
}
}
void createTvChannel(ComputerDetails computer) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
if (!isAndroidTV()) {
return;
}
ChannelBuilder builder = new ChannelBuilder()
.setType(TvContract.Channels.TYPE_PREVIEW)
.setDisplayName(computer.name)
.setInternalProviderId(computer.uuid)
.setAppLinkIntent(ServerHelper.createPcShortcutIntent(context, computer));
Long channelId = getChannelId(computer.uuid);
if (channelId != null) {
context.getContentResolver().update(TvContract.buildChannelUri(channelId),
builder.toContentValues(), null, null);
return;
}
Uri channelUri = context.getContentResolver().insert(
TvContract.Channels.CONTENT_URI, builder.toContentValues());
if (channelUri != null) {
long id = ContentUris.parseId(channelUri);
updateChannelIcon(id);
}
}
}
@TargetApi(Build.VERSION_CODES.O)
private void updateChannelIcon(long channelId) {
Bitmap logo = drawableToBitmap(context.getResources().getDrawable(R.drawable.ic_channel));
try {
Uri localUri = TvContract.buildChannelLogoUri(channelId);
try (OutputStream outputStream = context.getContentResolver().openOutputStream(localUri)) {
logo.compress(Bitmap.CompressFormat.PNG, 100, outputStream);
outputStream.flush();
} catch (SQLiteException | IOException e) {
LimeLog.warning("Failed to store the logo to the system content provider.");
e.printStackTrace();
}
} finally {
logo.recycle();
}
}
private Bitmap drawableToBitmap(Drawable drawable) {
int width = context.getResources().getDimensionPixelSize(R.dimen.tv_channel_logo_width);
int height = context.getResources().getDimensionPixelSize(R.dimen.tv_channel_logo_width);
Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(bitmap);
drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight());
drawable.draw(canvas);
return bitmap;
}
void addGameToChannel(ComputerDetails computer, NvApp app) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
if (!isAndroidTV()) {
return;
}
Long channelId = getChannelId(computer.uuid);
if (channelId == null) {
return;
}
PreviewProgramBuilder builder = new PreviewProgramBuilder()
.setChannelId(channelId)
.setType(TYPE_GAME)
.setTitle(app.getAppName())
.setPosterArtAspectRatio(ASPECT_RATIO_MOVIE_POSTER)
.setPosterArtUri(PosterContentProvider.createBoxArtUri(computer.uuid, ""+app.getAppId()))
.setIntent(ServerHelper.createAppShortcutIntent(context, computer, app))
.setInternalProviderId(""+app.getAppId())
// Weight should increase each time we run the game
.setWeight((int)((System.currentTimeMillis() - 1500000000000L) / 1000));
Long programId = getProgramId(channelId, ""+app.getAppId());
if (programId != null) {
context.getContentResolver().update(TvContract.buildPreviewProgramUri(programId),
builder.toContentValues(), null, null);
return;
}
context.getContentResolver().insert(TvContract.PreviewPrograms.CONTENT_URI,
builder.toContentValues());
TvContract.requestChannelBrowsable(context, channelId);
}
}
void deleteChannel(ComputerDetails computer) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
if (!isAndroidTV()) {
return;
}
Long channelId = getChannelId(computer.uuid);
if (channelId == null) {
return;
}
context.getContentResolver().delete(TvContract.buildChannelUri(channelId), null, null);
}
}
void deleteProgram(ComputerDetails computer, NvApp app) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
if (!isAndroidTV()) {
return;
}
Long channelId = getChannelId(computer.uuid);
if (channelId == null) {
return;
}
Long programId = getProgramId(channelId, ""+app.getAppId());
if (programId == null) {
return;
}
context.getContentResolver().delete(TvContract.buildPreviewProgramUri(programId), null, null);
}
}
@TargetApi(Build.VERSION_CODES.O)
private Long getChannelId(String computerUuid) {
try (Cursor cursor = context.getContentResolver().query(
TvContract.Channels.CONTENT_URI,
new String[] {TvContract.Channels._ID, TvContract.Channels.COLUMN_INTERNAL_PROVIDER_ID},
null,
null,
null)) {
if (cursor == null || cursor.getCount() == 0) {
return null;
}
while (cursor.moveToNext()) {
String internalProviderId = cursor.getString(INTERNAL_PROVIDER_ID_INDEX);
if (computerUuid.equals(internalProviderId)) {
return cursor.getLong(ID_INDEX);
}
}
return null;
}
}
@TargetApi(Build.VERSION_CODES.O)
private Long getProgramId(long channelId, String appId) {
try (Cursor cursor = context.getContentResolver().query(
TvContract.buildPreviewProgramsUriForChannel(channelId),
new String[] {TvContract.PreviewPrograms._ID, TvContract.PreviewPrograms.COLUMN_INTERNAL_PROVIDER_ID, TvContract.PreviewPrograms.COLUMN_BROWSABLE},
null,
null,
null)) {
if (cursor == null || cursor.getCount() == 0) {
return null;
}
while (cursor.moveToNext()) {
String internalProviderId = cursor.getString(INTERNAL_PROVIDER_ID_INDEX);
if (appId.equals(internalProviderId)) {
long id = cursor.getLong(ID_INDEX);
int browsable = cursor.getInt(PROGRAM_BROWSABLE_INDEX);
if (browsable != 0) {
return id;
} else {
int countDeleted = context.getContentResolver().delete(TvContract.buildPreviewProgramUri(id), null, null);
if (countDeleted > 0) {
LimeLog.info("Preview program has been deleted");
} else {
LimeLog.warning("Preview program has not been deleted");
}
}
}
}
return null;
}
}
private static <T> String toValueString(T value) {
return value == null ? null : value.toString();
}
private static String toUriString(Intent intent) {
return intent == null ? null : intent.toUri(Intent.URI_INTENT_SCHEME);
}
@TargetApi(Build.VERSION_CODES.O)
private boolean isAndroidTV() {
return context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_LEANBACK);
}
@TargetApi(Build.VERSION_CODES.O)
private static class PreviewProgramBuilder {
private ContentValues mValues = new ContentValues();
public PreviewProgramBuilder setChannelId(Long channelId) {
mValues.put(TvContract.PreviewPrograms.COLUMN_CHANNEL_ID, channelId);
return this;
}
public PreviewProgramBuilder setType(int type) {
mValues.put(TvContract.PreviewPrograms.COLUMN_TYPE, type);
return this;
}
public PreviewProgramBuilder setTitle(String title) {
mValues.put(TvContract.PreviewPrograms.COLUMN_TITLE, title);
return this;
}
public PreviewProgramBuilder setPosterArtAspectRatio(int aspectRatio) {
mValues.put(TvContract.PreviewPrograms.COLUMN_POSTER_ART_ASPECT_RATIO, aspectRatio);
return this;
}
public PreviewProgramBuilder setIntent(Intent intent) {
mValues.put(TvContract.PreviewPrograms.COLUMN_INTENT_URI, toUriString(intent));
return this;
}
public PreviewProgramBuilder setIntentUri(Uri uri) {
mValues.put(TvContract.PreviewPrograms.COLUMN_INTENT_URI, toValueString(uri));
return this;
}
public PreviewProgramBuilder setInternalProviderId(String id) {
mValues.put(TvContract.PreviewPrograms.COLUMN_INTERNAL_PROVIDER_ID, id);
return this;
}
public PreviewProgramBuilder setPosterArtUri(Uri uri) {
mValues.put(TvContract.PreviewPrograms.COLUMN_POSTER_ART_URI, toValueString(uri));
return this;
}
public PreviewProgramBuilder setWeight(int weight) {
mValues.put(TvContract.PreviewPrograms.COLUMN_WEIGHT, weight);
return this;
}
public ContentValues toContentValues() {
return new ContentValues(mValues);
}
}
@TargetApi(Build.VERSION_CODES.O)
private static class ChannelBuilder {
private ContentValues mValues = new ContentValues();
public ChannelBuilder setType(String type) {
mValues.put(TvContract.Channels.COLUMN_TYPE, type);
return this;
}
public ChannelBuilder setDisplayName(String displayName) {
mValues.put(TvContract.Channels.COLUMN_DISPLAY_NAME, displayName);
return this;
}
public ChannelBuilder setInternalProviderId(String internalProviderId) {
mValues.put(TvContract.Channels.COLUMN_INTERNAL_PROVIDER_ID, internalProviderId);
return this;
}
public ChannelBuilder setAppLinkIntent(Intent intent) {
mValues.put(TvContract.Channels.COLUMN_APP_LINK_INTENT_URI, toUriString(intent));
return this;
}
public ContentValues toContentValues() {
return new ContentValues(mValues);
}
}
}
@@ -2,24 +2,22 @@ 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.WindowInsets;
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;
@@ -44,18 +42,31 @@ public class UiHelper {
public static void notifyNewRootView(Activity activity)
{
View rootView = activity.findViewById(android.R.id.content);
UiModeManager modeMgr = (UiModeManager) activity.getSystemService(Context.UI_MODE_SERVICE);
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;
}
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);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
// Draw under the status bar on Android Q devices
rootView.setPadding(horizontalPaddingPixels, verticalPaddingPixels,
horizontalPaddingPixels, verticalPaddingPixels);
activity.getWindow().getDecorView().setOnApplyWindowInsetsListener(new View.OnApplyWindowInsetsListener() {
@Override
public WindowInsets onApplyWindowInsets(View view, WindowInsets windowInsets) {
view.setPadding(windowInsets.getSystemWindowInsetLeft(),
windowInsets.getSystemWindowInsetTop(),
windowInsets.getSystemWindowInsetRight(),
0);
return windowInsets;
}
});
activity.getWindow().getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_STABLE | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION);
}
}
@@ -123,4 +134,32 @@ public class UiHelper {
.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();
}
}

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

+10
View File
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@color/ic_launcher_background"/>
<item android:drawable="@drawable/ic_lime_layer"
android:bottom="10dp"
android:left="10dp"
android:right="10dp"
android:top="10dp"
/>
</layer-list>
@@ -0,0 +1,19 @@
<vector android:height="24dp" android:viewportHeight="546.15576"
android:viewportWidth="546.08374" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#ffffff"
android:pathData="M252.584,0.356c-58.2,4.8 -112.3,27.1 -156.8,64.6l-8.4,7 86.9,86.9c47.7,47.7 87.1,86.8 87.6,86.8 0.4,-0 0.6,-55.2 0.5,-122.8l-0.3,-122.7 -2.5,-0.1c-1.4,-0 -4.5,0.1 -7,0.3z" android:strokeColor="#00000000"/>
<path android:fillColor="#ffffff"
android:pathData="M284.284,0.356c-1,0.9 -0.9,245.3 0.1,245.3 0.4,-0 39.8,-39.1 87.5,-86.8l86.9,-86.9 -8.4,-7c-34.4,-29 -74.9,-49.2 -117.8,-58.7 -16.6,-3.6 -46.9,-7.4 -48.3,-5.9z" android:strokeColor="#00000000"/>
<path android:fillColor="#ffffff"
android:pathData="M64.884,95.856c-24.1,28.5 -41.2,59.5 -52.6,95.3 -6.3,19.6 -11.1,45.8 -11.9,64l-0.3,7 122.8,0.3c67.5,0.1 122.7,-0.1 122.7,-0.5 0,-0.5 -39.1,-39.9 -86.8,-87.6l-86.9,-86.9 -7,8.4z" android:strokeColor="#00000000"/>
<path android:fillColor="#ffffff"
android:pathData="M387.384,174.356c-47.8,47.7 -86.8,87.1 -86.8,87.6 0,0.4 55.2,0.6 122.8,0.5l122.7,-0.3 -0.3,-7c-1.4,-32.6 -12.4,-72.9 -28.7,-105.5 -9.1,-18.2 -25.9,-43 -38.7,-57.3l-4.3,-4.8 -86.7,86.8z" android:strokeColor="#00000000"/>
<path android:fillColor="#ffffff"
android:pathData="M0.184,284.556c-0.7,1.1 1,19.8 3,32.1 7.6,48.6 29.1,95.2 61.7,133.8l7,8.4 86.9,-86.9c47.7,-47.7 86.8,-87.1 86.8,-87.5 0,-1.1 -244.8,-1 -245.4,0.1z" android:strokeColor="#00000000"/>
<path android:fillColor="#ffffff"
android:pathData="M300.584,284.356c0,0.5 39.1,39.9 86.8,87.6l86.9,86.9 7,-8.4c24.1,-28.5 41.2,-59.5 52.6,-95.3 6.3,-19.6 11.1,-45.8 11.9,-64l0.3,-7 -122.7,-0.3c-67.6,-0.1 -122.8,0.1 -122.8,0.5z" android:strokeColor="#00000000"/>
<path android:fillColor="#ffffff"
android:pathData="M174.284,387.456l-86.9,86.9 8.4,7c28.5,24.1 59.5,41.2 95.3,52.6 19.6,6.3 45.8,11.1 64,11.9l7,0.3 0.3,-122.8c0.1,-67.5 -0.1,-122.7 -0.5,-122.7 -0.5,-0 -39.9,39.1 -87.6,86.8z" android:strokeColor="#00000000"/>
<path android:fillColor="#ffffff"
android:pathData="M283.784,423.356l0.3,122.8 7,-0.3c18.2,-0.8 44.4,-5.6 64,-11.9 35.8,-11.4 66.8,-28.5 95.3,-52.6l8.4,-7 -86.9,-86.9c-47.7,-47.7 -87.1,-86.8 -87.6,-86.8 -0.4,-0 -0.6,55.2 -0.5,122.7z" android:strokeColor="#00000000"/>
</vector>
+9
View File
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="128dp"
android:height="128dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:pathData="M18,8h-1L17,6c0,-2.76 -2.24,-5 -5,-5S7,3.24 7,6v2L6,8c-1.1,0 -2,0.9 -2,2v10c0,1.1 0.9,2 2,2h12c1.1,0 2,-0.9 2,-2L20,10c0,-1.1 -0.9,-2 -2,-2zM12,17c-1.1,0 -2,-0.9 -2,-2s0.9,-2 2,-2 2,0.9 2,2 -0.9,2 -2,2zM15.1,8L8.9,8L8.9,6c0,-1.71 1.39,-3.1 3.1,-3.1 1.71,0 3.1,1.39 3.1,3.1v2z"
android:fillColor="#FFFFFF"/>
</vector>
+4 -8
View File
@@ -1,9 +1,5 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="128dp"
android:height="128dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:pathData="M23.64,7c-0.45,-0.34 -4.93,-4 -11.64,-4 -1.5,0 -2.89,0.19 -4.15,0.48L18.18,13.8 23.64,7zM17.04,15.22L3.27,1.44 2,2.72l2.05,2.06C1.91,5.76 0.59,6.82 0.36,7l11.63,14.49 0.01,0.01 0.01,-0.01 3.9,-4.86 3.32,3.32 1.27,-1.27 -3.46,-3.46z"
android:fillColor="#FFFFFF"/>
<vector android:height="128dp" android:width="128dp" android:tint="#FFFFFF"
android:viewportHeight="24.0" android:viewportWidth="24.0"
xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#FF000000" android:pathData="M1,21h22L12,2 1,21zM13,18h-2v-2h2v2zM13,14h-2v-4h2v4z"/>
</vector>
@@ -2,7 +2,6 @@
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingBottom="@dimen/activity_vertical_margin"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
@@ -22,11 +22,11 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/manuallyAddPcText"
android:layout_toLeftOf="@+id/addPcButton"
android:layout_toStartOf="@+id/addPcButton"
android:layout_marginTop="25dp"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_alignParentRight="true"
android:layout_alignParentEnd="true"
android:ems="10"
android:singleLine="true"
android:inputType="textNoSuggestions"
@@ -34,5 +34,15 @@
<requestFocus />
</EditText>
<Button
android:id="@+id/addPcButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="25dp"
android:layout_below="@+id/manuallyAddPcText"
android:layout_alignParentRight="true"
android:layout_alignParentEnd="true"
android:text="@android:string/ok"/>
</RelativeLayout>
@@ -2,10 +2,6 @@
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingBottom="@dimen/activity_vertical_margin"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
tools:context=".AppView" >
<FrameLayout
@@ -27,8 +23,8 @@
android:layout_centerHorizontal="true"
android:layout_alignParentTop="true"
android:gravity="center"
android:paddingTop="0dp"
android:paddingBottom="10dp"
android:paddingTop="5dp"
android:paddingBottom="5dp"
android:textSize="28sp"/>
</RelativeLayout>
+26
View File
@@ -10,4 +10,30 @@
android:layout_height="match_parent"
android:layout_gravity="center" />
<TextView
android:id="@+id/performanceOverlay"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="10dp"
android:layout_marginStart="10dp"
android:layout_marginTop="10dp"
android:layout_gravity="left"
android:textAppearance="?android:attr/textAppearanceSmall"
android:gravity="left"
android:background="#80000000"
android:visibility="gone" />
<TextView
android:id="@+id/notificationOverlay"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginRight="10dp"
android:layout_marginEnd="10dp"
android:layout_marginTop="10dp"
android:layout_gravity="right"
android:textAppearance="?android:attr/textAppearanceLarge"
android:gravity="right"
android:background="#80000000"
android:visibility="gone" />
</merge>
@@ -1,8 +1,8 @@
<!-- Portrait orientation only -->
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingBottom="@dimen/activity_vertical_margin"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
@@ -2,10 +2,6 @@
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
android:paddingBottom="@dimen/activity_vertical_margin"
android:id="@+id/stream_settings"
tools:context=".preferences.StreamSettings">
+10 -14
View File
@@ -1,15 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical" android:layout_width="match_parent"
android:layout_height="match_parent">
<GridView
android:id="@+id/fragmentView"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:numColumns="auto_fit"
android:columnWidth="160dp"
android:stretchMode="spacingWidth"
android:focusable="true"
android:focusableInTouchMode="true"
android:gravity="center"/>
</LinearLayout>
<GridView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/fragmentView"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:numColumns="auto_fit"
android:columnWidth="160dp"
android:stretchMode="spacingWidthUniform"
android:focusable="true"
android:focusableInTouchMode="true"
android:gravity="center"/>
+10 -14
View File
@@ -1,15 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical" android:layout_width="match_parent"
android:layout_height="match_parent">
<GridView
android:id="@+id/fragmentView"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:numColumns="auto_fit"
android:columnWidth="105dp"
android:stretchMode="spacingWidth"
android:focusable="true"
android:focusableInTouchMode="true"
android:gravity="center"/>
</LinearLayout>
<GridView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/fragmentView"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:numColumns="auto_fit"
android:columnWidth="105dp"
android:stretchMode="spacingWidthUniform"
android:focusable="true"
android:focusableInTouchMode="true"
android:gravity="center"/>
+11 -14
View File
@@ -1,15 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical" android:layout_width="match_parent"
android:layout_height="match_parent">
<GridView
android:id="@+id/fragmentView"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:numColumns="auto_fit"
android:columnWidth="160dp"
android:focusable="true"
android:focusableInTouchMode="true"
android:nextFocusLeft="@id/settingsButton"
android:gravity="center"/>
</LinearLayout>
<GridView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/fragmentView"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:numColumns="auto_fit"
android:columnWidth="160dp"
android:stretchMode="spacingWidthUniform"
android:focusable="true"
android:focusableInTouchMode="true"
android:nextFocusLeft="@id/settingsButton"
android:gravity="center"/>
+11 -14
View File
@@ -1,15 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical" android:layout_width="match_parent"
android:layout_height="match_parent">
<GridView
android:id="@+id/fragmentView"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:numColumns="auto_fit"
android:columnWidth="105dp"
android:focusable="true"
android:focusableInTouchMode="true"
android:nextFocusLeft="@id/settingsButton"
android:gravity="center"/>
</LinearLayout>
<GridView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/fragmentView"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:numColumns="auto_fit"
android:columnWidth="105dp"
android:stretchMode="spacingWidthUniform"
android:focusable="true"
android:focusableInTouchMode="true"
android:nextFocusLeft="@id/settingsButton"
android:gravity="center"/>
-2
View File
@@ -61,7 +61,6 @@
<string name="lost_connection">Conexión perdida</string>
<!-- AppList activity -->
<string name="title_applist">Aplicaciones en</string>
<string name="applist_menu_resume">Reanudar sesión</string>
<string name="applist_menu_quit">Cerrar sesión</string>
<string name="applist_menu_quit_and_start">Cerrar juego actual e iniciar</string>
@@ -98,7 +97,6 @@
<string name="title_checkbox_51_surround">Activar sonido 5.1 surround</string>
<string name="summary_checkbox_51_surround">Desmarcar si experimentas problemas de audio. Requiere GFE 2.7 o superior.</string>
<string name="category_gamepad_settings">Configuración de mando</string>
<string name="title_checkbox_multi_controller">Soporte para múltiples mandos</string>
<string name="summary_checkbox_multi_controller">Si no está marcado, todos los mandos aparecen como uno solo</string>
<string name="title_seekbar_deadzone">Ajustar zona muerta del stick analógico</string>
+53 -5
View File
@@ -1,8 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Shortcut strings -->
<string name="scut_deleted_pc">PC supprimé</string>
<string name="scut_not_paired">PC non appairé</string>
<string name="scut_pc_not_found">PC non trouvé</string>
<string name="scut_invalid_uuid">Le PC fourni n\'est pas valide</string>
<string name="scut_invalid_app_id">L\'application fournie n\'est pas valide</string>
<!-- Help strings -->
<string name="help_loading_title">Visionneuse d\'aide</string>
@@ -14,6 +18,7 @@
<string name="pcview_menu_unpair_pc">Désappairer</string>
<string name="pcview_menu_send_wol">Envoyer la requête Wake-On-LAN</string>
<string name="pcview_menu_delete_pc">Supprimer PC</string>
<string name="pcview_menu_details">Voir les détails</string>
<!-- Pair messages -->
<string name="pairing">Appariement…</string>
@@ -47,6 +52,12 @@
<string name="error_404">GFE renvoi une erreur HTTP 404. Assurez-vous que votre PC exécute un GPU pris en charge.
L\'utilisation d\'un logiciel de bureau à distance peut également provoquer cette erreur. Essayez de redémarrer votre machine ou de réinstaller GFE.
</string>
<string name="title_decoding_error">Le décodeur vidéo s\'est écrasé</string>
<string name="message_decoding_error">Moonlight s\'est écrasé en raison d\'une incompatibilité avec le décodeur vidéo de cet appareil. Assurez-vous que GeForce Experience soit mis à jour vers la dernière version sur votre PC. Essayez de régler les paramètres de diffusion si les plantages continuent.</string>
<string name="title_decoding_reset">Paramètres vidéo réinitialiser</string>
<string name="message_decoding_reset">Le décodeur vidéo de votre appareil continue de planter avec les paramètres de diffusion sélectionnés. Vos paramètres de diffusion ont été réinitialisés par défaut.</string>
<string name="error_usb_prohibited">L\'accès USB est interdit par votre appareil. Vérifiez vos paramètres Knox ou MDM.</string>
<string name="unable_to_pin_shortcut">Votre lanceur actuel ne permet pas de créer des raccourcis épinglés.</string>
<!-- Start application messages -->
<string name="conn_establishing_title">Établissement de la connexion</string>
@@ -63,20 +74,27 @@
<!-- General strings -->
<string name="ip_hint">Adresse IP de GeForce PC</string>
<string name="searching_pc">Recherche de PC avec GameStream en cours...\n\n
<string name="searching_pc">Recherche de PC avec GameStream en cours\n\n
Assurez-vous que GameStream est activé dans les paramètres GeForce Experience SHIELD.</string>
<string name="yes">Oui</string>
<string name="no">Non</string>
<string name="lost_connection">Perte de connexion avec le PC</string>
<string name="title_details">Détails</string>
<string name="help">Aide</string>
<string name="delete_pc_msg">Êtes-vous sûr de vouloir supprimer ce PC?</string>
<string name="slow_connection_msg">Connexion lente au PC\nRéduisez votre débit</string>
<string name="poor_connection_msg">Mauvaise connexion au PC</string>
<string name="perf_overlay_text">Video dimensions: %1$s\nDécodeur: %2$s\nEstimation de la fréquence d\'images de l\'ordinateur hôte: %3$.2f FPS\nFréquence d\'images entrantes du réseau: %4$.2f FPS\nTaux de rendu: %5$.2f FPS\nImages envoyé par votre connexion réseau: %6$.2f%%\nTemps moyen de réception: %7$.2f ms\nTemps de décodage moyen: %8$.2f ms</string>
<!-- AppList activity -->
<string name="title_applist">Applications sur</string>
<string name="applist_connect_msg">Connexion au PC…</string>
<string name="applist_menu_resume">Reprise de la session</string>
<string name="applist_menu_quit">Quitter la session</string>
<string name="applist_menu_quit_and_start">Quitter le jeu actuel et démarrer</string>
<string name="applist_menu_cancel">Annuler</string>
<string name="applist_menu_details">Voir les détails</string>
<string name="applist_menu_scut">Créer un raccourci</string>
<string name="applist_menu_tv_channel">Ajouter à la chaîne</string>
<string name="applist_refresh_title">Liste des applications</string>
<string name="applist_refresh_msg">Actualisation des applications…</string>
<string name="applist_refresh_error_title">Erreur</string>
@@ -85,6 +103,7 @@
<string name="applist_quit_success">Fermeture avec succès</string>
<string name="applist_quit_fail">Échec de la fermeture</string>
<string name="applist_quit_confirmation">Voulez-vous vraiment quitter l\'application en cours d\'exécution? Toutes les données non enregistrées seront perdues.</string>
<string name="applist_details_id">ID app:</string>
<!-- Add computer manually activity -->
<string name="title_add_pc">Ajouter un PC manuellement</string>
@@ -93,35 +112,57 @@
<string name="addpc_success">Ajouté avec succès de l\'ordinateur</string>
<string name="addpc_unknown_host">Impossible de résoudre l\'adresse du PC. Assurez-vous que vous n\'avez pas fait une faute de frappe dans l\'adresse.</string>
<string name="addpc_enter_ip">Vous devez entrer une adresse IP</string>
<string name="addpc_wrong_sitelocal">Cette adresse ne semble pas correcte. Vous devez utiliser l\'adresse IP publique de votre routeur pour la diffusion en continu sur Internet..</string>
<!-- Preferences -->
<string name="category_basic_settings">Paramètres de base</string>
<string name="title_resolution_list">Sélectionner la résolution et les FPS à atteindre</string>
<string name="title_resolution_list">Résolution vidéo</string>
<string name="summary_resolution_list">Le réglage de valeurs trop élevées pour votre appareil peut provoquer un retard ou un plantage</string>
<string name="title_fps_list">Fréquence d\'images vidéo</string>
<string name="summary_fps_list">Augmenter pour un flux vidéo plus lisse. Diminution pour de meilleures performances sur les périphériques bas de gamme.</string>
<string name="title_seekbar_bitrate">Sélectionnez le bitrate vidéo à obtenir</string>
<string name="summary_seekbar_bitrate">Bitrate inférieur pour réduire la saccade. Augmentez le bitrate pour augmenter la qualité de l\'image.</string>
<string name="suffix_seekbar_bitrate">Kbps</string>
<string name="title_unlock_fps">Débloquer tous les taux d\'images possibles</string>
<string name="summary_unlock_fps">La diffusion en continu à 90 ou 120 FPS peut réduire la latence sur les périphériques haut de gamme, mais peut provoquer des retards ou des blocages sur les périphériques qui ne peuvent \pas le prendre en charge</string>
<string name="title_checkbox_stretch_video">Étirez la vidéo en plein écran</string>
<string name="title_checkbox_disable_warnings">Désactiver les messages d\'avertissement</string>
<string name="summary_checkbox_disable_warnings">Désactiver les messages d\'avertissement de connexion à l\'écran pendant le streaming</string>
<string name="title_checkbox_enable_pip">Activer le mode observateur dans l\'image</string>
<string name="summary_checkbox_enable_pip">Permet de visualiser le flux (sans le contrôleur) tout en multitâche</string>
<string name="category_audio_settings">Paramètres audio</string>
<string name="title_checkbox_51_surround">Activer son surround 5.1</string>
<string name="summary_checkbox_51_surround">Décochez si vous rencontrez des problèmes audio. Nécessite GFE 2.7 ou supérieur.</string>
<string name="category_gamepad_settings">Paramètres du gamepad</string>
<string name="category_input_settings">Paramètres d\'entrée</string>
<string name="title_checkbox_multi_controller">Prise en charge de plusieurs contrôleurs</string>
<string name="summary_checkbox_multi_controller">Lorsqu\'elle n\'est pas cochée, tous les contrôleurs sont regroupés</string>
<string name="title_checkbox_vibrate_fallback">Emuler le support vibration</string>
<string name="summary_checkbox_vibrate_fallback">Vibre votre appareil pour émuler une vibration si votre manette ne le prend pas en charge</string>
<string name="title_seekbar_deadzone">Régler la zone morte du stick analogique</string>
<string name="suffix_seekbar_deadzone">%</string>
<string name="title_checkbox_xb1_driver">Pilote de contrôleur Xbox 360/One</string>
<string name="summary_checkbox_xb1_driver">Active un pilote USB intégré pour les périphériques sans prise en charge du contrôleur Xbox natif.</string>
<string name="title_checkbox_usb_bind_all">Ignorer le support du contrôleur Android</string>
<string name="summary_checkbox_usb_bind_all">Force le pilote USB de Moonlight à prendre en charge tous les gamepads Xbox pris en charge</string>
<string name="title_checkbox_mouse_emulation">Emulation de la souris via le gamepad</string>
<string name="summary_checkbox_mouse_emulation">Appuyez longuement sur le bouton Start pour faire basculer la manette de jeu en mode souris.</string>
<string name="title_checkbox_mouse_nav_buttons">Activer les boutons de souris arrière et avant</string>
<string name="summary_checkbox_mouse_nav_buttons">L\'activation de cette option peut entraîner un clic droit sur certains périphériques.</string>
<string name="category_on_screen_controls_settings">Paramètres des contrôles à l\'écran</string>
<string name="title_checkbox_show_onscreen_controls">Afficher les commandes à l\'écran</string>
<string name="summary_checkbox_show_onscreen_controls">Afficher la superposition du contrôleur virtuel sur l\'écran tactile</string>
<string name="title_checkbox_vibrate_osc">Activer les vibrations</string>
<string name="summary_checkbox_vibrate_osc">Vibre votre appareil pour émuler les vibrations des commandes à l\'écran</string>
<string name="title_only_l3r3">Montre seulement L3 et R3</string>
<string name="summary_only_l3r3">Cacher tout sauf L3 et R3</string>
<string name="title_reset_osc">Effacer la disposition des commandes à l\'écran sauvegardée</string>
<string name="summary_reset_osc">Rétablit la taille et la position par défaut de tous les contrôles à l\'écran</string>
<string name="dialog_title_reset_osc">Réinitialiser la mise en page</string>
<string name="dialog_text_reset_osc">Êtes-vous sûr de vouloir supprimer la disposition des commandes à l\'écran que vous avez sauvegardée?</string>
<string name="toast_reset_osc_success">Les contrôles à l\'écran sont réinitialisés</string>
<string name="category_ui_settings">Paramètres de l\'interface utilisateur</string>
<string name="title_language_list">Langue</string>
@@ -138,6 +179,13 @@
<string name="summary_checkbox_host_audio">Lire l\'audio de l\'ordinateur et de ce périphérique</string>
<string name="category_advanced_settings">Réglages avancés</string>
<string name="title_disable_frame_drop">Désactiver la suppression d\'image</string>
<string name="summary_disable_frame_drop">Peut réduire les micro-saccades sur certains appareils, mais peut augmenter la latence</string>
<string name="title_video_format">Modifier les paramètres H.265</string>
<string name="summary_video_format">H.265 réduit les exigences de bande passante vidéo, mais requiert un périphérique très récent.</string>
<string name="summary_video_format">H.265 réduit les besoins en bande passante vidéo mais nécessite un périphérique très récent</string>
<string name="title_enable_hdr">Activer le HDR (expérimental)</string>
<string name="summary_enable_hdr">Diffuser du HDR lorsque le jeu et le processeur graphique du PC le prennent en charge. HDR nécessite un GPU série GTX 1000 ou une version ultérieure.</string>
<string name="title_enable_perf_overlay">Activer la superposition de performance</string>
<string name="summary_enable_perf_overlay">Afficher une superposition à l\'écran avec des informations de performance en temps réel pendant la lecture en continu</string>
</resources>
+76 -31
View File
@@ -1,6 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Shortcut strings -->
<string name="scut_deleted_pc">PC eliminato</string>
<string name="scut_not_paired">PC non accoppiato</string>
<!-- Help strings -->
<string name="help_loading_title">Visualizza assistenza</string>
<string name="help_loading_msg">Caricamento pagina di assistenza…</string>
<!-- PC view menu entries -->
<string name="pcview_menu_app_list">Lista applicazioni</string>
<string name="pcview_menu_pair_pc">Accoppia PC</string>
@@ -10,21 +17,22 @@
<!-- Pair messages -->
<string name="pairing">Accoppiamento…</string>
<string name="pair_pc_offline">PC offline</string>
<string name="pair_pc_offline">Il PC è offline</string>
<string name="pair_pc_ingame">PC con applicazione avviata. Devi chiudere l\'applicazione prima dell\'accoppiamento.</string>
<string name="pair_pairing_title">Accoppiamento</string>
<string name="pair_pairing_msg">Inserisci il seguente PIN sul PC:</string>
<string name="pair_incorrect_pin">PIN non corretto</string>
<string name="pair_fail">Accoppiamento fallito</string>
<string name="pair_already_in_progress">Accoppiamento già in corso</string>
<!-- WOL messages -->
<string name="wol_pc_online">PC già avviato</string>
<string name="wol_no_mac">Impossibile risvegliare il PC perchè GFE non ha inviato nessun indirizzo MAC</string>
<string name="wol_no_mac">Impossibile risvegliare il PC perché GFE non ha inviato nessun indirizzo MAC</string>
<string name="wol_waking_pc">Risveglio PC…</string>
<string name="wol_waking_msg">Il PC potrebbe impiegare qualche secondo per risvegliarsi.
Se non succede niente, assicurati che l\'opzione Wake-On-LAN sia configurata correttamente.
Se non succede niente, assicurati che l\'opzione Wake-On-LAN sia configurata correttamente.
</string>
<string name="wol_fail">Invio pacchetto Wake-On-LAN fallito</string>
<string name="wol_fail">Invio pacchetti Wake-On-LAN fallito</string>
<!-- Unpair messages -->
<string name="unpairing">Disaccoppiamento…</string>
@@ -37,33 +45,40 @@
<string name="error_manager_not_running">Il servizio ComputerManager non è avviato. Attendi qualche secondo o riavvia l\'applicazione.</string>
<string name="error_unknown_host">Risoluzione nome host fallita</string>
<string name="error_404">GFE ha ritornato un errore HTTP 404 error. Assicurati che il PC stia usando una GPU supportata.
Usare un software di remote-desktop può causare questo errore. Prova a riavviare il PC o a reinstallare GFE.
Usare un software di desktop remoto può causare questo errore. Prova a riavviare il PC o a reinstallare GFE.
</string>
<string name="title_decoding_error">Il decodificatore video ha smesso di funzionare</string>
<string name="message_decoding_error">Moonlight ha smesso di funzionare per colpa di un problema al decodificatore video di questo dispositivo. Prova a modificare le impostazioni di stream se il problema persiste.</string>
<string name="title_decoding_reset">Ripristino impostazioni video</string>
<string name="message_decoding_reset">Il decodificatore video del dispositivo continua a funzionare in modo anomalo con le impostazioni di streaming selezionate. Le impostazioni di streaming sono state ripristinate ai valori predefiniti.</string>
<string name="error_usb_prohibited">L\'accesso USB è vietato dall\'amministratore del dispositivo. Verifica le impostazioni Knox o MDM.</string>
<!-- Start application messages -->
<string name="conn_establishing_title">Connessione</string>
<string name="conn_establishing_msg">Connessione in corso</string>
<string name="conn_establishing_title">Connessione in corso</string>
<string name="conn_establishing_msg">Avvio connessione</string>
<string name="conn_metered">Attenzione: la rete attiva prevede costi aggiuntivi in base all\'utilizzo!</string>
<string name="conn_client_latency">Latenza frame media client-side:</string>
<string name="conn_client_latency_hw">latenza decoder hardware:</string>
<string name="conn_hardware_latency">Latenza decoder hardware media:</string>
<string name="conn_client_latency">Latenza decodifica fotogrammi media:</string>
<string name="conn_client_latency_hw">latenza decodificatore hardware:</string>
<string name="conn_hardware_latency">Latenza decodificatore hardware media:</string>
<string name="conn_starting">Avvio in corso…</string>
<string name="conn_error_title">Errore connessione</string>
<string name="conn_error_msg">Avvio fallito</string>
<string name="conn_terminated_title">Connessione terminata</string>
<string name="conn_terminated_title">Connessione interrotta</string>
<string name="conn_terminated_msg">La connessione è stata interrotta</string>
<!-- General strings -->
<string name="ip_hint">Indirizzo IP del PC</string>
<string name="searching_pc">Ricerca PC in corso…</string>
<string name="searching_pc">Ricerca di PC con GameStream avviato…\n\n
Assicurati che GameStream sia abilitato nelle impostazioni SHIELD di GeForce Experience.</string>
<string name="yes"></string>
<string name="no">No</string>
<string name="lost_connection">Connessione con il PC persa</string>
<string name="help">Assistenza</string>
<!-- AppList activity -->
<string name="title_applist">Applicazioni su</string>
<string name="applist_menu_resume">Riprendi Sessione</string>
<string name="applist_menu_quit">Chiudi Sessione</string>
<string name="applist_connect_msg">Connessione al PC in corso…</string>
<string name="applist_menu_resume">Riprendi sessione</string>
<string name="applist_menu_quit">Chiudi sessione</string>
<string name="applist_menu_quit_and_start">Chiudi sessione corrente e avvia</string>
<string name="applist_menu_cancel">Annulla</string>
<string name="applist_refresh_title">Lista applicazioni</string>
@@ -76,43 +91,73 @@
<string name="applist_quit_confirmation">Sei sicuro di voler chiudere l\'applicazione avviata? Tutti i dati non salvati saranno persi.</string>
<!-- Add computer manually activity -->
<string name="title_add_pc">Aggiungi PC Manualmente</string>
<string name="title_add_pc">Aggiungi PC manualmente</string>
<string name="msg_add_pc">Connessione al PC in corso…</string>
<string name="addpc_fail">Impossibile connettersi al PC. Assicurati che il firewall del PC sia configurato correttamente.</string>
<string name="addpc_fail">Impossibile connettersi al PC specificato. Assicurati che le porte nel firewall del PC siano configurate correttamente.</string>
<string name="addpc_success">PC aggiunto con successo</string>
<string name="addpc_unknown_host">Impossibile risovere l\'indirizzo del PC. Assicurati di aver scritto correttamente l\'indirizzo.</string>
<string name="addpc_enter_ip">Devi inserire un indirizzo IP</string>
<string name="addpc_wrong_sitelocal">Quell\'indirizzo non sembra corretto. È necessario utilizzare l\'indirizzo IP pubblico del router per lo streaming su Internet.</string>
<!-- Preferences -->
<string name="category_basic_settings">Impostazioni Base</string>
<string name="category_basic_settings">Impostazioni generali</string>
<string name="title_resolution_list">Risoluzione e FPS</string>
<string name="summary_resolution_list">Valori troppo elevati possono causare lag o crash</string>
<string name="title_seekbar_bitrate">Bitrate video</string>
<string name="summary_seekbar_bitrate">Abbassa il bitrate per ridurre lo stuttering; alza il bitrate per aumenteare la qualità dell\'immagine</string>
<string name="title_seekbar_bitrate">Velocità di trasmissione video</string>
<string name="summary_seekbar_bitrate">Abbassa la velocità di trasmissione per ridurre lo stuttering; alzala per migliorare la qualità dell\'immagine</string>
<string name="suffix_seekbar_bitrate">Kbps</string>
<string name="title_checkbox_stretch_video">Forza video in full-screen</string>
<string name="title_checkbox_stretch_video">Forza video a schermo intero</string>
<string name="title_checkbox_disable_warnings">Disabilita messaggi di warning</string>
<string name="summary_checkbox_disable_warnings">Disabilita i messaggi di warning sullo schermo durante lo streaming</string>
<string name="title_checkbox_enable_pip">Abilita modalità spettatore Picture-in-Picture</string>
<string name="summary_checkbox_enable_pip">Permette di osservare (ma non di controllare) la stream in multitasking</string>
<string name="category_gamepad_settings">Impostazioni Gamepad</string>
<string name="title_checkbox_multi_controller">Supporto controller multipli</string>
<string name="summary_checkbox_multi_controller">Quando disabilitato, tutti i controllers appaiono come uno solo</string>
<string name="title_seekbar_deadzone">Aggiusta deadzone degli stick analogici</string>
<string name="category_audio_settings">Impostazioni audio</string>
<string name="title_checkbox_51_surround">Abilita l\'audio 5.1 surround</string>
<string name="summary_checkbox_51_surround">Se riscontri problemi, disabilitalo. Richiede GFE 2.7 o versioni sucessive.</string>
<string name="title_checkbox_multi_controller">Supporto a più controller</string>
<string name="summary_checkbox_multi_controller">Quando disabilitato, tutti i controller appaiono come uno solo</string>
<string name="title_seekbar_deadzone">Regola i punti morti degli stick analogici</string>
<string name="suffix_seekbar_deadzone">%</string>
<string name="title_checkbox_xb1_driver">Driver del controller Xbox 360/One</string>
<string name="summary_checkbox_xb1_driver">Abilita un driver USB integrato per dispositivi senza supporto al controller Xbox nativo</string>
<string name="title_checkbox_usb_bind_all">Sovrascrivi il supporto ai controller su Android</string>
<string name="summary_checkbox_usb_bind_all">Forza i driver USB di Moonlight di assumere il controllo su tutti i controller Xbox supportati</string>
<string name="title_checkbox_mouse_emulation">Emulazione del mouse tramite controller</string>
<string name="summary_checkbox_mouse_emulation">Tenendo premuto il pulsante Start, il controller passerà alla modalità mouse</string>
<string name="category_on_screen_controls_settings">Impostazioni dei controlli a schermo</string>
<string name="title_checkbox_show_onscreen_controls">Mostra controlli a schermo</string>
<string name="summary_checkbox_show_onscreen_controls">Mostra l\'overlay virtuale del controller su schermo</string>
<string name="title_only_l3r3">Mostra solo L3 e R3</string>
<string name="summary_only_l3r3">Nasconde tutti i pulsanti virtuali tranne L3 e R3</string>
<string name="title_reset_osc">Ripristina il layout personalizzato dei controlli a schermo</string>
<string name="summary_reset_osc">Ripristina tutti i controlli a schermo nelle loro posizioni e dimensioni predefinite</string>
<string name="dialog_title_reset_osc">Ripristino del layout</string>
<string name="dialog_text_reset_osc">Sei sicuro di voler ripristinare ai valori predefiniti i layout dei controlli a schermo?</string>
<string name="toast_reset_osc_success">I controlli a schermo sono stati ripristinati</string>
<string name="category_ui_settings">Impostazioni Interfaccia</string>
<string name="category_ui_settings">Impostazioni dell\'interfaccia</string>
<string name="title_language_list">Lingua</string>
<string name="summary_language_list">Lingua da usare in Moonlight</string>
<string name="title_checkbox_list_mode">Usa lista invece della griglia</string>
<string name="summary_checkbox_list_mode">Visualizza applicazioni e computers in una lista invece di una griglia</string>
<string name="summary_checkbox_list_mode">Visualizza le applicazioni e i PC in una lista invece di una griglia</string>
<string name="title_checkbox_small_icon_mode">Usa icone piccole</string>
<string name="summary_checkbox_small_icon_mode">Usa icone piccole nella vista a griglia per avere più oggetti sullo schermo</string>
<string name="summary_checkbox_small_icon_mode">Usa icone piccole nella griglia per avere più oggetti a schermo</string>
<string name="category_host_settings">Impostazioni Host</string>
<string name="category_host_settings">Impostazioni del PC host</string>
<string name="title_checkbox_enable_sops">Ottimizza le impostazioni dei giochi</string>
<string name="summary_checkbox_enable_sops">Permetti a GFE di modificare le impostazioni dei giochi per uno streaming ottimale</string>
<string name="title_checkbox_host_audio">Riproduci audio sul PC</string>
<string name="summary_checkbox_host_audio">Riproduci l\'audio sul computer e su questo dispositivo</string>
<string name="summary_checkbox_host_audio">Riproduce l\'audio sul PC e su questo dispositivo</string>
<string name="category_advanced_settings">Impostazioni Avanzate</string>
<string name="category_advanced_settings">Impostazioni avanzate</string>
<string name="title_disable_frame_drop">Non saltare i fotogrammi</string>
<string name="summary_disable_frame_drop">Potrebbe ridurre il micro-stuttering su alcuni dispositivi, ma può aumentare la latenza</string>
<string name="title_video_format">Modifica impostazioni H.265</string>
<string name="summary_video_format">H.265 riduce i requisiti di larghezza di banda video ma richiede un dispositivo molto recente</string>
<string name="title_enable_hdr">Abilita HDR (sperimentale)</string>
<string name="summary_enable_hdr">Utilizza l\'HDR quando il gioco e la scheda video del PC lo supportano. L\'HDR richiede una scheda video serie GTX 1000 o sucessive.</string>
</resources>
-2
View File
@@ -57,7 +57,6 @@
<string name="lost_connection">コンピュータとの接続が失われました</string>
<!-- AppList activity -->
<string name="title_applist">ゲーム</string>
<string name="applist_menu_resume">セッションを続ける</string>
<string name="applist_menu_quit">セッションを終了する</string>
<string name="applist_menu_quit_and_start">現在のゲームを終了して新しいゲームを始める</string>
@@ -94,7 +93,6 @@
<string name="title_checkbox_51_surround">5.1chサラウンド</string>
<string name="summary_checkbox_51_surround">音声に問題が生じる場合はチェックを外してください。バージョン2.7以降のGFEが必要です</string>
<string name="category_gamepad_settings">ゲームコントローラ</string>
<string name="title_checkbox_multi_controller">複数のゲームコントローラ</string>
<string name="summary_checkbox_multi_controller">チェックを外すと、全てのゲームコントローラが単一の物として認識されます</string>
<string name="title_seekbar_deadzone">アナログゲームコントローラのデッドゾーン</string>
-2
View File
@@ -71,7 +71,6 @@
<string name="help">도움말</string>
<!-- AppList activity -->
<string name="title_applist">앱 사용 가능</string>
<string name="applist_connect_msg">PC에 연결중…</string>
<string name="applist_menu_resume">세션 계속</string>
<string name="applist_menu_quit">세션 종료</string>
@@ -109,7 +108,6 @@
<string name="title_checkbox_51_surround">5.1 서라운드 사운드 활성화</string>
<string name="summary_checkbox_51_surround">오디오 문제가 발생한다면 체크를 해제하세요. GFE 2.7이나 그 이상 버전이 필요합니다.</string>
<string name="category_gamepad_settings">게임패드 설정</string>
<string name="title_checkbox_multi_controller">다중 컨트롤러 지원</string>
<string name="summary_checkbox_multi_controller">이 옵션을 선택하지 않으면 모든 컨트롤러가 하나로 표시됩니다</string>
<string name="title_seekbar_deadzone">아날로그 스틱 데드존 설정</string>
-9
View File
@@ -1,14 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string-array name="resolution_names">
<item>720p 30 FPS</item>
<item>720p 60 FPS</item>
<item>1080p 30 FPS</item>
<item>1080p 60 FPS</item>
<item>4K 30 FPS</item>
<item>4K 60 FPS</item>
</string-array>
<string-array name="decoder_names">
<item>Selecteer Decoder Automatisch</item>
<item>Forceer Software Decoderen</item>
-2
View File
@@ -61,7 +61,6 @@
<string name="lost_connection">Verbinding met PC verloren</string>
<!-- AppList activity -->
<string name="title_applist">Apps op</string>
<string name="applist_menu_resume">Hervat Sessie</string>
<string name="applist_menu_quit">Stop Sessie</string>
<string name="applist_menu_quit_and_start">Stop Huidige Spel en Start</string>
@@ -98,7 +97,6 @@
<string name="title_checkbox_51_surround">Gebruik 5.1 surround sound</string>
<string name="summary_checkbox_51_surround">Gebruik dit niet als er problemen zijn met de audio. Vereist GFE 2.7 of hoger.</string>
<string name="category_gamepad_settings">Gamepad Instellingen</string>
<string name="title_checkbox_multi_controller">Multi-gamepad support</string>
<string name="summary_checkbox_multi_controller">Wanneer uitgevinkt, alle controllers verschijnen als één.</string>
<string name="title_seekbar_deadzone">Pas analoge dodezone aan.</string>
+85 -26
View File
@@ -28,7 +28,7 @@
<!-- Unpair messages -->
<string name="unpairing">Разрыв пары…</string>
<string name="unpair_success">Разрыв пары закончился успешно.</string>
<string name="unpair_success">Разрыв пары закончился успешно</string>
<string name="unpair_fail">Разрыв пары не удался</string>
<string name="unpair_error">Устройство не было спарено</string>
@@ -36,14 +36,14 @@
<string name="error_pc_offline">Компьютер выключен или находится не в сети</string>
<string name="error_manager_not_running">Сервис ComputerManager не запущен. Пожалуйста, подождите несколько секунд или перезапустите приложение.</string>
<string name="error_unknown_host">Не удалось найти хост</string>
<string name="error_404">GFE вернул ошибку HTTP 404. Убедитесь что ваш PC is испольщует поддерживаемый GPU.
Использование программ для удалённого доступа также можнт вызывать эту ошибку. Попробуйте перезагрузить компьютер или переустановить GFE.
<string name="error_404">GFE вернул ошибку HTTP 404. Убедитесь что Ваш PC использует поддерживаемый GPU.
Использование программ для удалённого доступа также может вызывать эту ошибку. Попробуйте перезагрузить компьютер или переустановить GFE.
</string>
<!-- Start application messages -->
<string name="conn_establishing_title">Создание соединения.</string>
<string name="conn_establishing_title">Создание соединения</string>
<string name="conn_establishing_msg">Подключение</string>
<string name="conn_metered">Внимание: Происходит измерение вашего сетевого соединения!</string>
<string name="conn_metered">Внимание: Происходит измерение Вашего сетевого соединения!</string>
<string name="conn_client_latency">Средняя задержка декодирования кадра: </string>
<string name="conn_client_latency_hw">задержка аппаратного декодирования:</string>
<string name="conn_hardware_latency">Средняя задержка апаратного декодирования:</string>
@@ -54,18 +54,19 @@
<string name="conn_terminated_msg">Подключение было прервано</string>
<!-- General strings -->
<string name="ip_hint">IP адресс компьютера с GeForce</string>
<string name="searching_pc">Поиск компьютеров</string>
<string name="ip_hint">IP-адрес компьютера с GeForce</string>
<string name="searching_pc">Поиск компьютеров с запущенным GameStream…\n\n
Убедитесь что GameStream включен в настройках GeForce Experience в разделе SHIELD.</string>
<string name="yes">Да</string>
<string name="no">Нет</string>
<string name="lost_connection">Потеряно соединение с PC</string>
<!-- AppList activity -->
<string name="title_applist">Приложения на</string>
<string name="applist_menu_resume">Возобновить сессию</string>
<string name="applist_menu_quit">Выйти из сессии</string>
<string name="applist_menu_quit_and_start">Выйти из текущей игры и запустить</string>
<string name="applist_menu_cancel">Отмена</string>
<string name="applist_menu_tv_channel">Добавать на канал</string>
<string name="applist_refresh_title">Список приложений</string>
<string name="applist_refresh_msg">Обновление приложений…</string>
<string name="applist_refresh_error_title">Ошибка</string>
@@ -79,49 +80,107 @@
<string name="title_add_pc">Добавление PC вручную</string>
<string name="msg_add_pc">Соединение с PC…</string>
<string name="addpc_fail">Не удалось подключиться к выбранному компьютеру. Удостоверьтесь, что необходимые порты разрешены в настройках брандмауэра.</string>
<string name="addpc_success">Компьютер добавлен успешно.</string>
<string name="addpc_unknown_host">Не удалось найти PC по указанному адресу. Убедитесь, что вы не совершили ошибок во время его написания.</string>
<string name="addpc_success">Компьютер добавлен успешно</string>
<string name="addpc_unknown_host">Не удалось найти PC по указанному адресу. Убедитесь, что Вы не совершили ошибок во время его написания.</string>
<string name="addpc_enter_ip">Вы должны ввести IP адрес</string>
<!-- Preferences -->
<string name="category_basic_settings">Базовые Настройки</string>
<string name="title_resolution_list">Выберите разрешение и частоту кадров.</string>
<string name="summary_resolution_list">Выбор слишком высокого значеня для своего устройства может вызвать тормоза или вылеты.</string>
<string name="title_seekbar_bitrate">Выберите битрейт видео.</string>
<string name="category_basic_settings">Общие Настройки</string>
<string name="title_resolution_list">Выберите разрешение и частоту кадров</string>
<string name="summary_resolution_list">Выбор слишком высокого значеня для своего устройства может вызвать тормоза или вылеты</string>
<string name="title_seekbar_bitrate">Выберите битрейт видео</string>
<string name="summary_seekbar_bitrate">Низкий битрейт уменьшит зависания. Увеличение битрейта улучшит качество изображения.</string>
<string name="suffix_seekbar_bitrate">Kbps</string>
<string name="title_checkbox_stretch_video">Растягивать видео на весь экран</string>
<string name="title_checkbox_disable_warnings">Отключить сообщения с предупреждениями</string>
<string name="summary_checkbox_disable_warnings">Выключить экранные предупреждения о соединении во время стрима.</string>
<string name="summary_checkbox_disable_warnings">Выключить экранные предупреждения о соединении во время трансляции</string>
<string name="category_audio_settings">Аудио Настройки</string>
<string name="title_checkbox_51_surround">Включить объёмный звук 5.1</string>
<string name="summary_checkbox_51_surround">Отключите, если появляются аудио проблемы. Требуется GFE 2.7 или выше.</string>
<string name="category_gamepad_settings">Настройки Гемпада</string>
<string name="title_checkbox_multi_controller">Поддержка нескольких контроллеров</string>
<string name="summary_checkbox_multi_controller">Когда отключена, все контроллеры определяются как один. </string>
<string name="title_seekbar_deadzone">Регулировать мертвую зону аналогового стика.</string>
<string name="summary_checkbox_multi_controller">Когда отключена, все контроллеры определяются как один</string>
<string name="title_seekbar_deadzone">Регулировать мертвую зону аналогового стика</string>
<string name="suffix_seekbar_deadzone">%</string>
<string name="title_checkbox_xb1_driver">Драйвер контроллера от Xbox One</string>
<string name="summary_checkbox_xb1_driver">Включить встроенный USB драйвер для устройств без встроенной поддержки контроллера от Xbox One.</string>
<string name="title_checkbox_xb1_driver">Драйвер контроллеров Xbox 360/One</string>
<string name="summary_checkbox_xb1_driver">Включить встроенный USB драйвер для устройств без собственной поддержки контроллеров Xbox</string>
<string name="category_ui_settings">Настройки Интерфейса</string>
<string name="title_language_list">Язык</string>
<string name="summary_language_list">Язык, который будет использоваться в Moonlight</string>
<string name="title_checkbox_list_mode">Использовать списки вместо сеток.</string>
<string name="summary_checkbox_list_mode">Выводить приложения и компьютеры списком, вместо использования сетки.</string>
<string name="title_checkbox_list_mode">Использовать списки вместо сеток</string>
<string name="summary_checkbox_list_mode">Выводить приложения и компьютеры списком, вместо использования сетки</string>
<string name="title_checkbox_small_icon_mode">Использовать маленькие иконки</string>
<string name="summary_checkbox_small_icon_mode">Использовать маленькие иконки в сетки для увеличения числа элементов, отображаемых на экране.</string>
<string name="summary_checkbox_small_icon_mode">Использовать маленькие иконки в сетке для отображения большего числа элементов на экране</string>
<string name="category_host_settings">Настройки Хоста</string>
<string name="title_checkbox_enable_sops">Оптимизировать игровые настройки</string>
<string name="summary_checkbox_enable_sops">Разрешить GFE изменять настройки игр для оптимальной потоковой передачи</string>
<string name="summary_checkbox_enable_sops">Разрешить GFE изменять настройки игр для оптимальной трансляции</string>
<string name="title_checkbox_host_audio">Проигрывать звук на PC</string>
<string name="summary_checkbox_host_audio">Проигрывать звук на компьютере и текущем устройстве.</string>
<string name="summary_checkbox_host_audio">Проигрывать звук на компьютере и текущем устройстве</string>
<string name="category_advanced_settings">Расширенные Настройки</string>
<string name="title_video_format">Изменить настройки H.265</string>
<string name="summary_video_format">H.265 снижает требования к пропускной способности, но требует очень свежих устройств.</string>
<string name="summary_video_format">H.265 снижает требования к пропускной способности, но требует очень нового устройства</string>
<string name="category_on_screen_controls_settings">Настройки Экранных Кнопок</string>
<string name="title_checkbox_show_onscreen_controls">Показывать экранные кнопки</string>
<string name="summary_checkbox_show_onscreen_controls">Отображать оверлей виртуального контроллера на сенсорном экране</string>
<string name="title_only_l3r3">Показывать только L3 и R3</string>
<string name="summary_only_l3r3">Скрывать все экранные кнопки кроме L3 и R3</string>
<string name="scut_deleted_pc">PC удален</string>
<string name="scut_not_paired">PC не сопряжен</string>
<string name="help_loading_title">Просмотр Помощи</string>
<string name="help_loading_msg">Загрузка страницы помощи…</string>
<string name="pair_already_in_progress">Сопряжение уже в процессе</string>
<string name="help">Помощь</string>
<string name="applist_connect_msg">Подключение к PC…</string>
<string name="title_decoding_error">Сбой Видео Декодера</string>
<string name="message_decoding_error">Произошел сбой Moonlight из-за проблем с видео декодером данного устройства. Попробуйте изменить настройки трансляции если сбои будут продолжаться.</string>
<string name="title_decoding_reset">Видео Настройки Сброшены</string>
<string name="message_decoding_reset">Видео декодер Вашего устройства давал сбои с выбранными настройками. Настройки трансляции были сброшены до значений по умолчанию.</string>
<string name="error_usb_prohibited">USB доступ запрещен администратором устройства. Проверьте настройки Knox или MDM.</string>
<string name="addpc_wrong_sitelocal">Адрес указан неверно. Вы должны ввести публичный IP-адрес Вашего роутера для передачи через интернет.</string>
<string name="title_checkbox_enable_pip">Включить просмотр в режиме \"Картинка в картинке\"</string>
<string name="summary_checkbox_enable_pip">Позволяет просматривать трансляцию (но не управлять ей) во время работы в других приложениях</string>
<string name="title_checkbox_usb_bind_all">Переопределить поддержку контроллеров Android</string>
<string name="summary_checkbox_usb_bind_all">Заставляет USB драйвер Moonlight взять на себя работу со всеми поддерживаемыми Xbox геймпадами</string>
<string name="title_checkbox_mouse_emulation">Эмуляция мыши на геймпаде</string>
<string name="summary_checkbox_mouse_emulation">Долгое нажатие кнопки Start переключит геймпад в режим мыши</string>
<string name="title_reset_osc">Сбросить схему расположения экранных кнопок</string>
<string name="summary_reset_osc">Возвращает все экранные элементы управления к их расположениям по умолчанию</string>
<string name="dialog_title_reset_osc">Сбросить Схему</string>
<string name="dialog_text_reset_osc">Вы уверены что хотите удалить сохранненную схему расположения кнопок?</string>
<string name="toast_reset_osc_success">Экранные элементы управления возвращены к положениям по умолчанию</string>
<string name="title_disable_frame_drop">Никогда не пропускать кадры</string>
<string name="summary_disable_frame_drop">Может уменьшить микрозависания на некоторых устройствах, но также увеличить задержку</string>
<string name="title_enable_hdr">Включить HDR (Экспериментально)</string>
<string name="summary_enable_hdr">Транслировать в HDR если игра и GPU компьютера поддерживают это. HDR требует видеокарты GTX 1000 серии или более новой.</string>
<string name="title_checkbox_vibrate_osc">Включить вибрацию</string>
<string name="title_fps_list">Частота кадров</string>
<string name="applist_menu_details">Детали</string>
<string name="applist_menu_scut">Создать ярлык</string>
<string name="category_input_settings">Настройки ввода</string>
<string name="delete_pc_msg">Вы уверены что хотите удалить этот PC?</string>
<string name="pcview_menu_details">Детали</string>
<string name="poor_connection_msg">Слабое соединение с PC</string>
<string name="title_details">Детали</string>
<string name="title_enable_perf_overlay">Включить отображение статистики</string>
<string name="title_unlock_fps">Разблокировать все возможные частоты обновления</string>
<string name="applist_details_id">ID приложения:</string>
<string name="title_checkbox_vibrate_fallback">Эмуляция виброотдачи</string>
<string name="summary_checkbox_vibrate_osc">Вибрировать устройство для эмуляции виброотдачи при экранном управлении</string>
<string name="summary_checkbox_vibrate_fallback">Вибрировать устройство для эмуляции виброотдачи для геймпадов без поддержки вибрации</string>
<string name="summary_checkbox_mouse_nav_buttons">Включение этой опции может привести к неправильной работе правой кнопки мыши на некоторых устройствах</string>
<string name="scut_pc_not_found">PC не найден</string>
<string name="unable_to_pin_shortcut">Текущий лаунчер не позволяет создавать pinned ярлыки</string>
<string name="title_checkbox_mouse_nav_buttons">Включить кнопки вперед и назад для мыши</string>
<string name="slow_connection_msg">Медленное подключение к PC\nУменьшите битрейт</string>
<string name="summary_unlock_fps">Трансляция со скоростью 90 или 120 кадров в секунду может уменьшить задержку на устройствах высокого класса, но может вызвать задержки или сбой на устройствах без поддержки этого функционала</string>
<string name="summary_enable_perf_overlay">Отображение оверлея на экране с информацией о производительности во время трансляции в режиме реального времени</string>
<string name="perf_overlay_text">Разрешение видео: %1$s\nДекодер: %2$s\nРасчетная частота кадров PC-хоста: %3$.2f FPS\nВходящая частота кадров из сети: %4$.2f FPS\nЧастота кадров при рендеринге: %5$.2f FPS\nОтброшеных кадров вашей сетью: %6$.2f%%\nСреднее время получения: %7$.2f ms\nСреднее время декодирования: %8$.2f ms</string>
<string name="summary_fps_list">Увеличение для более плавного видео потока. Уменьшите для лучшей производительности на более слабых устройствах.</string>
<string name="scut_invalid_uuid">Указанный PC недействителен</string>
<string name="scut_invalid_app_id">Указанное приложение недействительно</string>
</resources>
+2
View File
@@ -7,6 +7,8 @@
-->
<style name="AppBaseTheme" parent="android:Theme.Material">
<!-- API 21 theme customizations can go here. -->
<item name="android:statusBarColor">#212121</item>
<item name="android:navigationBarColor">#212121</item>
</style>
</resources>
+6
View File
@@ -0,0 +1,6 @@
<resources>
<style name="AppBaseTheme" parent="android:Theme.Material">
<item name="android:navigationBarColor">@android:color/transparent</item>
<item name="android:statusBarColor">@android:color/transparent</item>
</style>
</resources>
+23 -2
View File
@@ -71,7 +71,6 @@
<string name="help">帮助</string>
<!-- AppList activity -->
<string name="title_applist">Apps on</string>
<string name="applist_connect_msg"> 连接到电脑中…… </string>
<string name="applist_menu_resume"> 恢复串流 </string>
<string name="applist_menu_quit"> 退出串流 </string>
@@ -109,7 +108,6 @@
<string name="title_checkbox_51_surround"> 启用 5.1 环绕音效 </string>
<string name="summary_checkbox_51_surround"> 如果你的声音听起来有问题请禁用。\n\n需要GeForce Experience 2.7 或更高版本 </string>
<string name="category_gamepad_settings"> 手柄设置 </string>
<string name="title_checkbox_multi_controller"> 启用多手柄支持 </string>
<string name="summary_checkbox_multi_controller"> 如果禁用,所有手柄将会认作一个手柄 </string>
<string name="title_seekbar_deadzone"> 调整摇杆死区 </string>
@@ -138,5 +136,28 @@
<string name="category_advanced_settings"> 高级设置 </string>
<string name="title_video_format"> H.265设置 </string>
<string name="summary_video_format">H.265能降低带宽需求,但是需要设备支持 </string>
<string name="applist_menu_scut">创建快捷方式</string>
<string name="category_input_settings">输入设置</string>
<string name="applist_menu_details">查看详情</string>
<string name="dialog_text_reset_osc">你确定要删除所保存的按钮布局吗?</string>
<string name="dialog_title_reset_osc">重置按钮布局</string>
<string name="delete_pc_msg">你确定要删除这台电脑?</string>
<string name="pcview_menu_details">查看详情</string>
<string name="scut_pc_not_found">电脑无法找到</string>
<string name="toast_reset_osc_success">按钮布局已经重置</string>
<string name="title_fps_list">视频帧数</string>
<string name="title_unlock_fps">解锁所有可用帧数</string>
<string name="title_only_l3r3">只显示[L3]和[R3]</string>
<string name="title_reset_osc">重置已经保存的触摸按钮布局</string>
<string name="title_disable_frame_drop">永不掉帧</string>
<string name="title_enable_hdr">启用 HDR (实验)</string>
<string name="title_checkbox_vibrate_osc">启动震动</string>
<string name="title_details">详情</string>
<string name="title_decoding_reset">重置视频设置</string>
<string name="title_checkbox_mouse_emulation">通过手柄模拟鼠标</string>
<string name="summary_only_l3r3">隐藏所有虚拟按钮除了L3和R3</string>
<string name="title_enable_perf_overlay">启用性能信息</string>
<string name="summary_enable_perf_overlay">在串流中显示实时性能信息</string>
<string name="perf_overlay_text">视频分辨率: %1$s\n解码器: %2$s\n估计主机帧数: %3$.2f FPS\n网络接收帧数: %4$.2f FPS\n渲染帧数: %5$.2f FPS\n网络丢失帧: %6$.2f%%\n平均接收时间: %7$.2f ms\n平均解码时间: %8$.2f ms</string>
</resources>
+32 -7
View File
@@ -71,7 +71,6 @@
<string name="help">幫助</string>
<!-- AppList activity -->
<string name="title_applist">Apps on</string>
<string name="applist_connect_msg"> 連接到電腦中…… </string>
<string name="applist_menu_resume"> 恢復串流 </string>
<string name="applist_menu_quit"> 退出串流 </string>
@@ -98,28 +97,51 @@
<string name="category_basic_settings"> 基本設置 </string>
<string name="title_resolution_list"> 選擇目標解析度和幀數 </string>
<string name="summary_resolution_list"> 過高的設定會引起串流卡頓甚至軟體閃退 </string>
<string name="title_seekbar_bitrate"> 選擇目標視頻碼率 </string>
<string name="title_fps_list">影像幀數</string>
<string name="summary_fps_list">增加以提供更流暢的影像串流. 減少以在較低的配備上獲得較好的效能.</string>
<string name="title_seekbar_bitrate"> 選擇目標影像碼率 </string>
<string name="summary_seekbar_bitrate"> 低碼率減少卡頓,高碼率提高畫質 </string>
<string name="suffix_seekbar_bitrate">Kbps</string>
<string name="title_unlock_fps">解鎖所有可使用的幀率</string>
<string name="summary_unlock_fps">串流於90或120幀下可以減少高級配備的延遲, 但可能導致無法支持的配備出現延遲或崩潰</string>
<string name="title_checkbox_stretch_video"> 將畫面拉伸至全屏 </string>
<string name="title_checkbox_disable_warnings"> 禁用錯誤提示 </string>
<string name="summary_checkbox_disable_warnings"> 串流過程中禁用連接錯誤提示 </string>
<string name="title_checkbox_enable_pip">啟用子母畫面</string>
<string name="summary_checkbox_enable_pip">當多工處理時, 允許觀看串流畫面(無法控制)</string>
<string name="category_audio_settings"> 音頻設置 </string>
<string name="title_checkbox_51_surround"> 啟用 5.1 環繞音效 </string>
<string name="summary_checkbox_51_surround"> 如果你的聲音聽起來有問題請禁用。\n\n需要GeForce Experience 2.7 或更高版本 </string>
<string name="category_gamepad_settings"> 手柄設置 </string>
<string name="category_input_settings">輸入設定</string>
<string name="title_checkbox_multi_controller"> 啟用多手柄支持 </string>
<string name="summary_checkbox_multi_controller"> 如果禁用,所有手柄將會認作一個手柄 </string>
<string name="title_seekbar_deadzone"> 調整搖桿死區 </string>
<string name="title_checkbox_vibrate_fallback">以手機模擬手柄震動</string>
<string name="summary_checkbox_vibrate_fallback">當手柄不支援震動時, 以手機來模擬</string>
<string name="title_seekbar_deadzone"> 調整手柄死區 </string>
<string name="suffix_seekbar_deadzone">%</string>
<string name="title_checkbox_xb1_driver">Xbox 360/One 手柄驅動 </string>
<string name="summary_checkbox_xb1_driver"> 若要在那些沒有原生Xbox手柄驅動的設備上使用Xbox手柄,請勾上此複選框 </string>
<string name="title_checkbox_usb_bind_all">覆蓋Android控制器支援</string>
<string name="summary_checkbox_usb_bind_all">強制 Moonlight USB驅動程式接管所有支持的Xbox手柄</string>
<string name="title_checkbox_mouse_emulation">透過手柄模擬滑鼠</string>
<string name="summary_checkbox_mouse_emulation">長按 [Start] 鍵將會切換至滑鼠模式</string>
<string name="title_checkbox_mouse_nav_buttons">Enable back and forward mouse buttons</string>
<string name="summary_checkbox_mouse_nav_buttons">Enabling this option may break right clicking on some buggy devices</string>
<string name="category_on_screen_controls_settings">設置 </string>
<string name="category_on_screen_controls_settings">設置 </string>
<string name="title_checkbox_show_onscreen_controls"> 啟用虛擬手柄 </string>
<string name="summary_checkbox_show_onscreen_controls"> 將在串流畫面上顯示一層虛擬手柄 </string>
<string name="title_checkbox_vibrate_osc">啟用震動</string>
<string name="summary_checkbox_vibrate_osc">在手機上模擬搖桿震動</string>
<string name="title_only_l3r3">只顯示[L3]及[R3]</string>
<string name="summary_only_l3r3">隱藏所有虛擬按鈕除了L3和R3</string>
<string name="title_reset_osc">重設已經保存的觸控按鈕布局</string>
<string name="summary_reset_osc">將所有觸控按鈕重設為默認之大小和位置</string>
<string name="dialog_title_reset_osc">重設按鈕布局</string>
<string name="dialog_text_reset_osc">你確定要刪除所保存的按鈕布局嗎?</string>
<string name="toast_reset_osc_success">按鈕布局已經重設</string>
<string name="category_ui_settings"> 界面設置 </string>
<string name="title_language_list">語言</string>
@@ -136,7 +158,10 @@
<string name="summary_checkbox_host_audio"> 將在電腦和本設備同時輸出聲音 </string>
<string name="category_advanced_settings"> 高級設置 </string>
<string name="title_disable_frame_drop">永遠不掉幀</string>
<string name="summary_disable_frame_drop">在一些手機上可能可以減少micro-stuttering, 但可能導致延遲</string>
<string name="title_video_format"> H.265設置 </string>
<string name="summary_video_format">H.265能降低寬需求,但是需要設備支持 </string>
<string name="summary_video_format">H.265能降低寬需求,但是需要設備支持 </string>
<string name="title_enable_hdr">啟用 HDR (實驗中)</string>
<string name="summary_enable_hdr">啟用 HDR 當遊戲與電腦的顯示卡支援時. HDR 需要 GTX 1000 系列之顯示卡或更高.</string>
</resources>
+25 -16
View File
@@ -1,24 +1,33 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string-array name="resolution_names">
<item>360p 30 FPS</item>
<item>360p 60 FPS</item>
<item>720p 30 FPS</item>
<item>720p 60 FPS</item>
<item>1080p 30 FPS</item>
<item>1080p 60 FPS</item>
<item>4K 30 FPS</item>
<item>4K 60 FPS</item>
<item>360p</item>
<item>480p</item>
<item>720p</item>
<item>1080p</item>
<item>1440p</item>
<item>4K</item>
</string-array>
<string-array name="resolution_values" translatable="false">
<item>360p30</item>
<item>360p60</item>
<item>720p30</item>
<item>720p60</item>
<item>1080p30</item>
<item>1080p60</item>
<item>4K30</item>
<item>4K60</item>
<item>360p</item>
<item>480p</item>
<item>720p</item>
<item>1080p</item>
<item>1440p</item>
<item>4K</item>
</string-array>
<string-array name="fps_names">
<item>30 FPS</item>
<item>60 FPS</item>
<item>90 FPS</item>
<item>120 FPS</item>
</string-array>
<string-array name="fps_values" translatable="false">
<item>30</item>
<item>60</item>
<item>90</item>
<item>120</item>
</string-array>
<string-array name="language_names" translatable="false">
+2
View File
@@ -3,5 +3,7 @@
<!-- Default screen margins, per the Android Design guidelines. -->
<dimen name="activity_horizontal_margin">16dp</dimen>
<dimen name="activity_vertical_margin">16dp</dimen>
<dimen name="tv_channel_logo_width">80dp</dimen>
<dimen name="tv_channel_logo_height">80dp</dimen>
</resources>
+37 -11
View File
@@ -1,8 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_label" translatable="false">Moonlight</string>
<string name="app_label_root" translatable="false">Moonlight (Root)</string>
<!-- Shortcut strings -->
<string name="scut_deleted_pc">PC deleted</string>
<string name="scut_not_paired">PC not paired</string>
<string name="scut_pc_not_found">PC not found</string>
<string name="scut_invalid_uuid">Provided PC is not valid</string>
<string name="scut_invalid_app_id">Provided App is not valid</string>
<!-- Help strings -->
<string name="help_loading_title">Help Viewer</string>
@@ -14,6 +20,7 @@
<string name="pcview_menu_unpair_pc">Unpair</string>
<string name="pcview_menu_send_wol">Send Wake-On-LAN request</string>
<string name="pcview_menu_delete_pc">Delete PC</string>
<string name="pcview_menu_details">View Details</string>
<!-- Pair messages -->
<string name="pairing">Pairing…</string>
@@ -48,10 +55,11 @@
Using remote desktop software can also cause this error. Try rebooting your machine or reinstalling GFE.
</string>
<string name="title_decoding_error">Video Decoder Crashed</string>
<string name="message_decoding_error">Moonlight has crashed due to a problem with this device\'s video decoder. Try adjusting the streaming settings if the crashes continue.</string>
<string name="message_decoding_error">Moonlight has crashed due to an incompatibility with this device\'s video decoder. Ensure GeForce Experience is updated to the latest version on your PC. Try adjusting the streaming settings if the crashes continue.</string>
<string name="title_decoding_reset">Video Settings Reset</string>
<string name="message_decoding_reset">Your device\'s video decoder continues to crash at your selected streaming settings. Your streaming settings have been reset to default.</string>
<string name="error_usb_prohibited">USB access is prohibited by your device administrator. Check your Knox or MDM settings.</string>
<string name="unable_to_pin_shortcut">Your current launcher does not allow for creating pinned shortcuts.</string>
<!-- Start application messages -->
<string name="conn_establishing_title">Establishing Connection</string>
@@ -73,15 +81,22 @@
<string name="yes">Yes</string>
<string name="no">No</string>
<string name="lost_connection">Lost connection to PC</string>
<string name="title_details">Details</string>
<string name="help">Help</string>
<string name="delete_pc_msg">Are you sure you want to delete this PC?</string>
<string name="slow_connection_msg">Slow connection to PC\nReduce your bitrate</string>
<string name="poor_connection_msg">Poor connection to PC</string>
<string name="perf_overlay_text">Video dimensions: %1$s\nDecoder: %2$s\nEstimated host PC frame rate: %3$.2f FPS\nIncoming frame rate from network: %4$.2f FPS\nRendering frame rate: %5$.2f FPS\nFrames dropped by your network connection: %6$.2f%%\nAverage receive time: %7$.2f ms\nAverage decoding time: %8$.2f ms</string>
<!-- AppList activity -->
<string name="title_applist">Apps on</string>
<string name="applist_connect_msg">Connecting to PC…</string>
<string name="applist_menu_resume">Resume Session</string>
<string name="applist_menu_quit">Quit Session</string>
<string name="applist_menu_quit_and_start">Quit Current Game and Start</string>
<string name="applist_menu_cancel">Cancel</string>
<string name="applist_menu_details">View Details</string>
<string name="applist_menu_scut">Create Shortcut</string>
<string name="applist_menu_tv_channel">Add to Channel</string>
<string name="applist_refresh_title">App List</string>
<string name="applist_refresh_msg">Refreshing apps…</string>
<string name="applist_refresh_error_title">Error</string>
@@ -90,6 +105,7 @@
<string name="applist_quit_success">Successfully quit</string>
<string name="applist_quit_fail">Failed to quit</string>
<string name="applist_quit_confirmation">Are you sure you want to quit the running app? All unsaved data will be lost.</string>
<string name="applist_details_id">App ID:</string>
<!-- Add computer manually activity -->
<string name="title_add_pc">Add PC Manually</string>
@@ -102,16 +118,18 @@
<!-- Preferences -->
<string name="category_basic_settings">Basic Settings</string>
<string name="title_resolution_list">Select resolution and FPS target</string>
<string name="summary_resolution_list">Setting values too high for your device may cause lag or crashing</string>
<string name="title_seekbar_bitrate">Select target video bitrate</string>
<string name="summary_seekbar_bitrate">Lower bitrate to reduce stuttering. Raise bitrate to increase image quality.</string>
<string name="title_resolution_list">Video resolution</string>
<string name="summary_resolution_list">Increase to improve image clarity. Decrease for better performance on lower end devices and slower networks.</string>
<string name="title_fps_list">Video frame rate</string>
<string name="summary_fps_list">Increase for a smoother video stream. Decrease for better performance on lower end devices.</string>
<string name="title_seekbar_bitrate">Video bitrate</string>
<string name="summary_seekbar_bitrate">Increase for better image quality. Decrease to improve performance on slower connections.</string>
<string name="suffix_seekbar_bitrate">Kbps</string>
<string name="title_unlock_fps">Unlock all possible frame rates</string>
<string name="summary_unlock_fps">Streaming at 90 or 120 FPS may reduce latency on high-end devices but can cause lag or crashes on devices that can\'t support it</string>
<string name="title_checkbox_stretch_video">Stretch video to full-screen</string>
<string name="title_checkbox_disable_warnings">Disable warning messages</string>
<string name="summary_checkbox_disable_warnings">Disable on-screen connection warning messages while streaming</string>
<string name="title_checkbox_battery_saver">Battery saver</string>
<string name="summary_checkbox_battery_saver">Uses less battery, but may increase stuttering</string>
<string name="title_checkbox_enable_pip">Enable Picture-in-Picture observer mode</string>
<string name="summary_checkbox_enable_pip">Allows the stream to be viewed (but not controlled) while multitasking</string>
@@ -119,9 +137,11 @@
<string name="title_checkbox_51_surround">Enable 5.1 surround sound</string>
<string name="summary_checkbox_51_surround">Uncheck if you experience audio issues. Requires GFE 2.7 or higher.</string>
<string name="category_gamepad_settings">Gamepad Settings</string>
<string name="title_checkbox_multi_controller">Multiple controller support</string>
<string name="summary_checkbox_multi_controller">When unchecked, all controllers appear as one</string>
<string name="category_input_settings">Input Settings</string>
<string name="title_checkbox_multi_controller">Automatic gamepad presence detection</string>
<string name="summary_checkbox_multi_controller">Unchecking this option forces a gamepad to always be present</string>
<string name="title_checkbox_vibrate_fallback">Emulate rumble support with vibration</string>
<string name="summary_checkbox_vibrate_fallback">Vibrates your device to emulate rumble if your gamepad does not support it</string>
<string name="title_seekbar_deadzone">Adjust analog stick deadzone</string>
<string name="suffix_seekbar_deadzone">%</string>
<string name="title_checkbox_xb1_driver">Xbox 360/One controller driver</string>
@@ -130,10 +150,14 @@
<string name="summary_checkbox_usb_bind_all">Forces Moonlight\'s USB driver to take over all supported Xbox gamepads</string>
<string name="title_checkbox_mouse_emulation">Mouse emulation via gamepad</string>
<string name="summary_checkbox_mouse_emulation">Long pressing the Start button will switch the gamepad into mouse mode</string>
<string name="title_checkbox_mouse_nav_buttons">Enable back and forward mouse buttons</string>
<string name="summary_checkbox_mouse_nav_buttons">Enabling this option may break right clicking on some buggy devices</string>
<string name="category_on_screen_controls_settings">On-screen Controls Settings</string>
<string name="title_checkbox_show_onscreen_controls">Show on-screen controls</string>
<string name="summary_checkbox_show_onscreen_controls">Show virtual controller overlay on touchscreen</string>
<string name="title_checkbox_vibrate_osc">Enable vibration</string>
<string name="summary_checkbox_vibrate_osc">Vibrates your device to emulate rumble for the on-screen controls</string>
<string name="title_only_l3r3">Only show L3 and R3</string>
<string name="summary_only_l3r3">Hide all virtual buttons except L3 and R3</string>
<string name="title_reset_osc">Clear saved on-screen controls layout</string>
@@ -163,5 +187,7 @@
<string name="summary_video_format">H.265 lowers video bandwidth requirements but requires a very recent device</string>
<string name="title_enable_hdr">Enable HDR (Experimental)</string>
<string name="summary_enable_hdr">Stream HDR when the game and PC GPU support it. HDR requires a GTX 1000 series GPU or later.</string>
<string name="title_enable_perf_overlay">Enable performance overlay</string>
<string name="summary_enable_perf_overlay">Display an on-screen overlay with real-time performance information while streaming</string>
</resources>
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<base-config cleartextTrafficPermitted="true">
<trust-anchors>
<certificates src="system" />
</trust-anchors>
</base-config>
</network-security-config>
+41 -8
View File
@@ -5,12 +5,19 @@
<PreferenceCategory android:title="@string/category_basic_settings"
android:key="category_basic_settings">
<ListPreference
android:key="list_resolution_fps"
android:key="list_resolution"
android:title="@string/title_resolution_list"
android:summary="@string/summary_resolution_list"
android:entries="@array/resolution_names"
android:entryValues="@array/resolution_values"
android:defaultValue="720p60" />
android:defaultValue="720p" />
<ListPreference
android:key="list_fps"
android:title="@string/title_fps_list"
android:summary="@string/summary_fps_list"
android:entries="@array/fps_names"
android:entryValues="@array/fps_values"
android:defaultValue="60" />
<com.limelight.preferences.SeekBarPreference
android:key="seekbar_bitrate_kbps"
android:dialogMessage="@string/summary_seekbar_bitrate"
@@ -21,13 +28,13 @@
android:text="@string/suffix_seekbar_bitrate"
android:title="@string/title_seekbar_bitrate" />
<CheckBoxPreference
android:key="checkbox_stretch_video"
android:title="@string/title_checkbox_stretch_video"
android:key="checkbox_unlock_fps"
android:title="@string/title_unlock_fps"
android:summary="@string/summary_unlock_fps"
android:defaultValue="false" />
<CheckBoxPreference
android:key="checkbox_battery_saver"
android:title="@string/title_checkbox_battery_saver"
android:summary="@string/summary_checkbox_battery_saver"
android:key="checkbox_stretch_video"
android:title="@string/title_checkbox_stretch_video"
android:defaultValue="false" />
<CheckBoxPreference
android:key="checkbox_enable_pip"
@@ -42,7 +49,7 @@
android:summary="@string/summary_checkbox_51_surround"
android:defaultValue="false" />
</PreferenceCategory>
<PreferenceCategory android:title="@string/category_gamepad_settings">
<PreferenceCategory android:title="@string/category_input_settings">
<!--com.limelight.preferences.SeekBarPreference
android:key="seekbar_deadzone"
android:defaultValue="15"
@@ -54,6 +61,11 @@
android:title="@string/title_checkbox_multi_controller"
android:summary="@string/summary_checkbox_multi_controller"
android:defaultValue="true" />
<CheckBoxPreference
android:key="checkbox_mouse_nav_buttons"
android:title="@string/title_checkbox_mouse_nav_buttons"
android:summary="@string/summary_checkbox_mouse_nav_buttons"
android:defaultValue="false" />
<CheckBoxPreference
android:key="checkbox_usb_driver"
android:title="@string/title_checkbox_xb1_driver"
@@ -70,6 +82,11 @@
android:title="@string/title_checkbox_mouse_emulation"
android:summary="@string/summary_checkbox_mouse_emulation"
android:defaultValue="true" />
<CheckBoxPreference
android:key="checkbox_vibrate_fallback"
android:title="@string/title_checkbox_vibrate_fallback"
android:summary="@string/summary_checkbox_vibrate_fallback"
android:defaultValue="false" />
</PreferenceCategory>
<PreferenceCategory android:title="@string/category_on_screen_controls_settings"
android:key="category_onscreen_controls">
@@ -78,6 +95,12 @@
android:key="checkbox_show_onscreen_controls"
android:summary="@string/summary_checkbox_show_onscreen_controls"
android:title="@string/title_checkbox_show_onscreen_controls" />
<CheckBoxPreference
android:key="checkbox_vibrate_osc"
android:dependency="checkbox_show_onscreen_controls"
android:title="@string/title_checkbox_vibrate_osc"
android:summary="@string/summary_checkbox_vibrate_osc"
android:defaultValue="true" />
<CheckBoxPreference
android:defaultValue="false"
android:dependency="checkbox_show_onscreen_controls"
@@ -126,6 +149,11 @@
</PreferenceCategory>
<PreferenceCategory android:title="@string/category_advanced_settings"
android:key="category_advanced_settings">
<CheckBoxPreference
android:key="checkbox_disable_warnings"
android:title="@string/title_checkbox_disable_warnings"
android:summary="@string/summary_checkbox_disable_warnings"
android:defaultValue="false" />
<ListPreference
android:key="video_format"
android:title="@string/title_video_format"
@@ -143,5 +171,10 @@
android:title="@string/title_enable_hdr"
android:summary="@string/summary_enable_hdr"
android:defaultValue="false" />
<CheckBoxPreference
android:key="checkbox_enable_perf_overlay"
android:title="@string/title_enable_perf_overlay"
android:summary="@string/summary_enable_perf_overlay"
android:defaultValue="false"/>
</PreferenceCategory>
</PreferenceScreen>
+1 -1
View File
@@ -3,5 +3,5 @@
<!-- Non-root application name -->
<!-- FIXME: We should set extractNativeLibs=false but this breaks installation on the Fire TV 3 -->
<application android:label="Moonlight" />
<application android:label="@string/app_label" />
</manifest>
+1 -1
View File
@@ -5,6 +5,6 @@
<!-- Ensure native libraries are always extracted for root builds,
since we must invoke the evdev_reader binary ourselves -->
<application
android:label="Moonlight (Root)"
android:label="@string/app_label_root"
android:extractNativeLibs="true" />
</manifest>
@@ -55,7 +55,7 @@ public class EvdevCaptureProvider extends InputCaptureProvider {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
// Launch evdev_reader directly via SU
try {
su = Runtime.getRuntime().exec("su -c "+evdevReaderCmd);
su = new ProcessBuilder("su", "-c", evdevReaderCmd).start();
} catch (IOException e) {
reportDeviceNotRooted();
e.printStackTrace();
@@ -151,7 +151,15 @@ public class EvdevCaptureProvider extends InputCaptureProvider {
break;
case EvdevEvent.BTN_SIDE:
listener.mouseButtonEvent(EvdevListener.BUTTON_X1,
event.value != 0);
break;
case EvdevEvent.BTN_EXTRA:
listener.mouseButtonEvent(EvdevListener.BUTTON_X2,
event.value != 0);
break;
case EvdevEvent.BTN_FORWARD:
case EvdevEvent.BTN_BACK:
case EvdevEvent.BTN_TASK:
+3 -1
View File
@@ -2,14 +2,16 @@
buildscript {
repositories {
jcenter()
google()
}
dependencies {
classpath 'com.android.tools.build:gradle:3.0.1'
classpath 'com.android.tools.build:gradle:3.4.2'
}
}
allprojects {
repositories {
jcenter()
google()
}
}
+2 -2
View File
@@ -1,9 +1,9 @@
This file serves to document some of the decoder errata when using MediaCodec hardware decoders on certain devices.
1. num_ref_frames is set to 16 by NVENC which causes decoders to allocate 16+ buffers. This can cause an error or lag on some devices.
- Affected decoders: TI OMAP4, Allwinner A20
- Affected decoders: TI OMAP4 crashes, Allwinner A20, MT8176 lags (HEVC not affected)
2. Some decoders have a huge per-frame latency with the unmodified SPS sent from NVENC. Setting max_dec_frame_buffering fixes this latency issue.
2. Some H.264 decoders have a huge per-frame latency with the unmodified SPS sent from NVENC. Setting max_dec_frame_buffering=1 fixes this latency issue.
- Affected decoders: NVIDIA Tegra 3 and 4, Broadcom VideoCore IV
3. Some decoders strictly require that you pass BUFFER_FLAG_CODEC_CONFIG and crash upon the IDR frame if you don't
@@ -0,0 +1,34 @@
Diese App streamt Spiele, Programme oder den kompletten Desktop von NVIDIA GameStream-kompatiebelen PCs mit NVIDIA GeForce Experience über das Lokale Netzwerk oder Internet. Gleichzeitig werden Maus-, Tastatur- und Gamepad-Eingaben von deinem Android Gerät zum PC übertragen.
Die Streamingqualität kann aufgrund des verwendeten Android Geräts und der Netzwerkgegebenheiten variieren. HDR Unterstützung setzt ein HRD10-fähiges Gerät, eine GTX-1000-series GPU und HDR10-kompatibles Spiele voraus.
'''Merkmale'''
* Open-source und komplett freie Software (keine Werbung, InApp-Käufe, oder "Pro" Version)
* Streamt Spiele unabhängig davon in welchem Store diese gekauft wurden
* Funktioniert im Heimnetzwerk oder über eine Internet bzw. LTE-Verbindung
* Bis zu 4K HDR Streaming mit 120 FPS und 5.1 Sourround Sound
* Tastatur- und Mausznterstützung (mit Android 8.0 oder mit Root-Rechten)
* Unterstützt PS3, PS4, Xbox 360, Xbox One und Android Gamepads
* Force Feedback Unterstützung
* Kooperatives lokales Spielen mit bis zu 4 verbundenen Eingabegeräten
* Maussteuerung via Gamepad durch langes gedrückt halten der Starttaste
'''PC Anforderungen'''
* NVIDIA GeForce GTX/RTX Serie GPU (''GT-Serie und AMD GPUs werden nicht von NVIDIA GameStream unterstützt'')
* Windows 7 oder neuer
* NVIDIA GeForce Experience (GFE) 2.2.2 oder neuer
'''Anleitung zur Schnellkonfiguration'''
# Stellen sie sicher dass GeForce Experience auf ihrem PC installiert ist. Aktivieren sie GameStream in den SHIELD Einstellungen.
# Wählen Sie den PC in Moonlight aus und tippen sie den PIN auf ihrem PC ein.
# Streamen sie los!
Für eine gutes Benutzungserlebnis benötigen sie einen mid- bist high-end WLAN-Router mit einer ungestörten Verbindung zu ihrem Android Gerät (5 GHz wird empfohlen) sowie eine gute Verbindung von ihrem PC zum Router (Ethernet empfohlen).
'''detaillierte Konfigurationsanleitung'''
Besuchen die die vollständige Konfigurationsanleitung https://bit.ly/1skHFjN (Englisch) um:
* Einen PC manuell hinzuzufügen (falls ihr PC nicht automatisch erkannt werden sollte)
* Über das Internet bzw. LTE zu streamen
* Eine Eingabegerät das direkt mit dem PC verbunden ist zu nutzen
* Den kompletten Desktop zu streamen
* Individuelle Apps zum Streamen hinzuzufügen
@@ -0,0 +1 @@
Spiele vom deinem PC auf Android spielen (nur NVIDIA)
+1
View File
@@ -0,0 +1 @@
Moonlight Spiele Streaming
@@ -0,0 +1,5 @@
- Updated to target Android Q SDK
- Requested low latency WiFi behavior on Android Q
- Fixed mouse capture in multi-window mode on Android Q
- Updated visual styles to match gesture navigation on Android Q
- Fixed USB driver issue that could cause player numbers to be wrong
@@ -0,0 +1,7 @@
- Added a performance overlay for real-time performance data
- Added support for launching games directly from the Android TV homescreen
- Added support for zero-configuration Internet streaming on IPv6 networks
- Improved handling of home screen PC and game shortcuts
- Fixed streaming from very old versions of GeForce Experience
- Fixed deleting PCs with duplicate names
- Updated Simplified Chinese translation
@@ -0,0 +1,8 @@
- Optimized edge-to-edge layout for Android Q
- Fixed 5.1 surround sound not always working over the Internet
- Fixed Android TV app icon on Android Pie
- Improved reliability of public IP address detection
- Enabled installation on external storage
- Fixed on-screen overlays covering stream when in PiP mode
- Fixed games never reappearing on Android TV channel if deleted once
- Updated Russian and French translations
@@ -0,0 +1,34 @@
This app streams games, programs, or your full desktop from an NVIDIA GameStream-compatible PC on your local network or the Internet using NVIDIA GeForce Experience. Mouse, keyboard, and controller input is sent from your Android device to the PC.
Streaming performance may vary based on your client device and network setup. HDR requires an HDR10-capable device, GTX 1000-series GPU, and HDR10-enabled game.
'''Features'''
* Open-source and completely free (no ads, IAPs, or "Pro")
* Streams games purchased from any store
* Works on your home network or over the Internet/LTE
* Up to 4K 120 FPS HDR streaming with 5.1 surround sound
* Keyboard and mouse support (with Android 8.0 or rooted device)
* Supports PS3, PS4, Xbox 360, Xbox One, and Android gamepads
* Force feedback support
* Local co-op with up to 4 connected controllers
* Mouse control via gamepad by long-pressing Start
'''PC Requirements'''
* NVIDIA GeForce GTX/RTX series GPU (''GT-series and AMD GPUs aren't supported by NVIDIA GameStream'')
* Windows 7 or later
* NVIDIA GeForce Experience (GFE) 2.2.2 or later
'''Quick Setup Instructions'''
# Make sure GeForce Experience is open on your PC. Turn on GameStream in the SHIELD settings page.
# Tap on the PC in Moonlight and type the PIN on your PC
# Start streaming!
To have a good experience, you need a mid to high-end wireless router with a good wireless connection to your Android device (5 GHz highly recommended) and a good connection from your PC to your router (Ethernet highly recommended).
'''Detailed Setup Instructions'''
See the full setup guide https://bit.ly/1skHFjN for:
* Adding a PC manually (if your PC is not detected)
* Streaming over the Internet or LTE
* Using a controller connected directly to your PC
* Streaming your full desktop
* Adding custom apps to stream

Before

Width:  |  Height:  |  Size: 73 KiB

After

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

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