Compare commits

...

1714 Commits

Author SHA1 Message Date
Cameron Gutman
fe704af62f Version 9.8.6 2021-04-09 19:35:02 -05:00
Cameron Gutman
e74517543d Update common-c for initial GFE 3.22 compatibility 2021-04-09 19:32:39 -05:00
Cameron Gutman
81c815840d Version 9.8.5 2021-04-03 12:39:02 -05:00
Cameron Gutman
e9cd63dc5f Removed deprecated ProGuard option 2021-04-03 12:00:53 -05:00
Cameron Gutman
1ae8f67d93 Add Norwegian Bokmål option to the language list 2021-04-03 11:59:57 -05:00
Cameron Gutman
daa1e10333 Merge remote-tracking branch 'origin/weblate' 2021-04-03 11:47:27 -05:00
Cameron Gutman
a8a356e703 Add Amazon Luna support in Xbox 360 driver 2021-04-03 11:45:02 -05:00
Rener kaka
ca440cc5dd Added translation using Weblate (Kurdish (Central)) 2021-04-02 21:11:43 +02:00
Øyvind Heddeland Instefjord
95a9fb4f62 Translated using Weblate (Norwegian Bokmål)
Currently translated at 90.1% (164 of 182 strings)

Translation: Moonlight Game Streaming/moonlight-android
Translate-URL: https://hosted.weblate.org/projects/moonlight/moonlight-android/nb_NO/
2021-04-01 17:26:57 +02:00
Cameron Gutman
7db9e27112 Update NDK to r22b 2021-03-31 20:07:08 -05:00
Cameron Gutman
03bcdbe3f7 Update moonlight-common-c to pick up AMF HEVC parsing fix 2021-03-31 20:06:53 -05:00
Cameron Gutman
f0762a6213 Version 9.8.4 2021-03-21 21:51:54 -05:00
Allan Nordhøy
67fbc6b3ad Translated using Weblate (Norwegian Bokmål)
Currently translated at 88.4% (161 of 182 strings)

Translation: Moonlight Game Streaming/moonlight-android
Translate-URL: https://hosted.weblate.org/projects/moonlight/moonlight-android/nb_NO/
2021-03-21 10:37:10 +01:00
Jorys Paulin
d9662d7396 Translated using Weblate (French)
Currently translated at 95.6% (174 of 182 strings)

Translation: Moonlight Game Streaming/moonlight-android
Translate-URL: https://hosted.weblate.org/projects/moonlight/moonlight-android/fr/
2021-03-21 10:37:10 +01:00
Nedelcu Constantin Marius Nedelcu
5ccbbf259d Translated using Weblate (Norwegian Bokmål)
Currently translated at 87.3% (159 of 182 strings)

Translation: Moonlight Game Streaming/moonlight-android
Translate-URL: https://hosted.weblate.org/projects/moonlight/moonlight-android/nb_NO/
2021-03-21 06:29:37 +01:00
Allan Nordhøy
179c2f8723 Translated using Weblate (Norwegian Bokmål)
Currently translated at 87.3% (159 of 182 strings)

Translation: Moonlight Game Streaming/moonlight-android
Translate-URL: https://hosted.weblate.org/projects/moonlight/moonlight-android/nb_NO/
2021-03-21 06:29:36 +01:00
Allan Nordhøy
c76e0a40a7 Translated using Weblate (Norwegian Bokmål)
Currently translated at 59.3% (108 of 182 strings)

Translation: moonlight/moonlight-android
Translate-URL: https://hosted.weblate.org/projects/moonlight/moonlight-android/nb_NO/
2021-03-21 02:34:36 +01:00
Artem
03407e528f Translated using Weblate (Russian)
Currently translated at 94.5% (172 of 182 strings)

Translation: moonlight/moonlight-android
Translate-URL: https://hosted.weblate.org/projects/moonlight/moonlight-android/ru/
2021-03-21 02:34:36 +01:00
reloxx13
0c41d742cf Translated using Weblate (German)
Currently translated at 100.0% (182 of 182 strings)

Translation: moonlight/moonlight-android
Translate-URL: https://hosted.weblate.org/projects/moonlight/moonlight-android/de/
2021-03-21 02:34:36 +01:00
Allan Nordhøy
ed2f471a4e Added translation using Weblate (Norwegian Bokmål) 2021-03-21 02:34:36 +01:00
Cameron Gutman
04efec101e Sync Xbox driver VIDs with Linux 5.11 2021-03-20 18:49:34 -05:00
Cameron Gutman
a6c69012cc Add Weblate link and badge 2021-03-20 18:15:40 -05:00
Cameron Gutman
0045c54d8e Reapply a portion of 1d3e42f that should not have been reverted 2021-03-20 11:11:12 -05:00
Cameron Gutman
45436c006f Cancel a pending drag timer before setting a new one 2021-03-20 11:04:34 -05:00
Cameron Gutman
cc183c0da8 Cancel a pending timer before setting a new one 2021-03-20 10:59:47 -05:00
Cameron Gutman
523f1df98b Remove superfluous simulated shift key up/down events
Setting the shift modifier flag alone is sufficient for current GFE versions
2021-03-20 10:38:15 -05:00
Cameron Gutman
5843dff278 Apply new fix for #840 2021-03-20 10:24:06 -05:00
Cameron Gutman
7f24f47978 Revert "Use a global set of modifier flags rather than per-device flags"
This reverts commit 1d3e42f92e.
2021-03-20 10:08:58 -05:00
Cameron Gutman
b1f9fd459e Update NDK to r22 2021-03-20 10:07:08 -05:00
Cameron Gutman
48988eb785 Update AGP to 4.1.3 2021-03-20 10:06:51 -05:00
Cameron Gutman
0045a885b9 Migrate to AppVeyor 2021-03-03 19:56:20 -06:00
Cameron Gutman
0b57f60454 Migrate from travis-ci.org to travis-ci.com 2021-03-03 01:54:20 -06:00
Cameron Gutman
f0857c7da2 Add issue template 2021-03-03 01:41:13 -06:00
Cameron Gutman
15faa2e841 Version 9.8.3 2021-03-02 18:48:12 -06:00
Cameron Gutman
da103f7197 Don't use our built-in Switch Pro mapping on Android 10+ 2021-02-28 16:35:17 -06:00
Cameron Gutman
1d3e42f92e Use a global set of modifier flags rather than per-device flags
Fixes #840
2021-02-28 11:26:35 -06:00
Cameron Gutman
20ced841dd Handle pointer capture on SOURCE_TOUCHPAD devices 2021-02-27 15:48:37 -06:00
Cameron Gutman
54ebd0a796 Fix streaming in the Android 12 emulator 2021-02-27 15:46:59 -06:00
Cameron Gutman
e636a7171b Add explicit android:exported value for Android 12 2021-02-27 15:46:23 -06:00
Cameron Gutman
e8f847065b Version 9.8.2 2021-01-31 21:08:12 -06:00
Cameron Gutman
1c806bb572 Only use the virtual device as a gamepad if at least one gamepad is present 2021-01-31 19:42:41 -06:00
Cameron Gutman
963133598f Add hack to work around https://issuetracker.google.com/issues/163120692 2021-01-31 19:29:57 -06:00
Cameron Gutman
fedaa74c47 Update AGP to 4.1.2 2021-01-31 19:28:42 -06:00
Cameron Gutman
e322baf1d7 Version 9.8.1 2021-01-09 19:42:45 -06:00
Cameron Gutman
173a07cb59 Update ENet 2021-01-09 19:25:21 -06:00
Cameron Gutman
364afff860 Allow display resolution adjustment when streaming at a native resolution 2021-01-09 19:24:21 -06:00
Cameron Gutman
1b59e61b8e Include PC name in the PC context menu header 2020-12-31 16:42:26 -06:00
Cameron Gutman
b1f453f7ba Charge time spent in the decode unit queue to the decoder rather than receive time 2020-12-31 16:35:49 -06:00
Cameron Gutman
175e842feb Support multiple native resolution options 2020-12-30 16:29:07 -06:00
Cameron Gutman
d7a9a37a0e Version 9.8 2020-12-30 13:08:49 -06:00
Cameron Gutman
836b9240de Make native resolution warning more stern 2020-12-30 12:52:05 -06:00
Cameron Gutman
bdac2df4b9 Fixed crash if we get a short read from the Xbox One controller 2020-12-24 11:59:33 -06:00
Cameron Gutman
57b507ad50 Use the game title as the context menu header 2020-12-24 11:50:59 -06:00
Cameron Gutman
35201b69f6 Add specific error text for an early termination 2020-12-24 11:32:10 -06:00
Cameron Gutman
0d138c26e9 Remove the native option if it duplicates a pre-existing resolution 2020-12-23 16:49:18 -06:00
Cameron Gutman
b4a7393dca Normalize resolution orientation on pre-M devices 2020-12-23 16:46:07 -06:00
Cameron Gutman
d86092df1a Update AGP to 4.1.1 2020-12-23 16:23:15 -06:00
Cameron Gutman
b392d7f8e3 Add option to stream at device native resolution
Fixes #155
2020-12-23 16:17:06 -06:00
Cameron Gutman
7cc7953879 Display failing ports when the connection is unsuccessful 2020-12-23 14:30:24 -06:00
Cameron Gutman
7b26852a1f Use LiStringifyPortFlags() instead of coding it ourselves 2020-12-23 14:19:19 -06:00
Cameron Gutman
f26b384697 Add a PC menu header to show PC status 2020-12-13 13:05:36 -06:00
Cameron Gutman
ab0531aa76 Update moonlight-common-c submodule 2020-12-07 20:07:48 -06:00
Cameron Gutman
6873720d81 Fix build 2020-11-28 17:50:26 -06:00
Cameron Gutman
1e30c4a219 Remove "View Apps" and change "View Hidden Apps" to "View All Apps" 2020-11-28 17:28:17 -06:00
Cameron Gutman
0a0e3ff970 Don't trim XML strings
We should display the apps exactly as reported in GFE.
2020-11-21 17:09:34 -06:00
Cameron Gutman
5c42fd86a6 Update moonlight-common-c to avoid QoS on IPv6 2020-11-21 17:06:15 -06:00
Cameron Gutman
16cc829906 Fix some incorrect tap behavior on right clicks 2020-11-10 15:27:48 -06:00
Cameron Gutman
829e7cf33c Allow 2 finger scrolling in relative mode 2020-11-10 15:12:17 -06:00
Cameron Gutman
02bfa90417 Ignore movement from cancelled touches 2020-11-10 15:09:51 -06:00
Daniel
0b2466cf26 fixed some german typos in the UI (#894)
* fixed some german typos

* added more translations from english

* correct order

* typo

* typos
2020-11-10 10:48:57 -06:00
Cameron Gutman
9d8df04c5c Catch IllegalArgumentException when trying to insert an entry to TvContract.Channels.CONTENT_URI
HarmonyOS has FEATURE_LEANBACK but doesn't support this URI
2020-11-10 10:46:39 -06:00
Cameron Gutman
34a1697d50 Revert "Fix crash on HarmonyOS due to broken TV content provider APIs"
This reverts commit ce0b19605a.
2020-11-10 10:44:41 -06:00
Cameron Gutman
17cf711c3d Don't check brand when whitelisting ranchu for HEVC
HarmonyOS also uses "ranchu" as the hardware name, but doesn't use "google" as the brand name
2020-11-08 20:40:59 -06:00
Cameron Gutman
ce0b19605a Fix crash on HarmonyOS due to broken TV content provider APIs
Fixes #883
2020-11-08 20:39:47 -06:00
Cameron Gutman
35bd9ecda3 Version 9.7.7 2020-10-28 21:14:26 -05:00
Cameron Gutman
ca89849dd2 Update moonlight-common-c with QoS fix 2020-10-28 20:58:21 -05:00
Cameron Gutman
ac1cb6d56b Version 9.7.6 2020-10-25 12:44:26 -05:00
Cameron Gutman
dfbffea0fc Disable mouse acceleration on Nvidia Shield TV devices 2020-10-25 12:18:27 -05:00
Cameron Gutman
7ae9c993f1 Version 9.7.5 2020-10-19 23:10:22 -05:00
Cameron Gutman
91d739f8d6 Use the Nvidia button on Shield controllers as a Guide button 2020-10-18 21:14:53 -05:00
Cameron Gutman
f0c625d85c Only emulate buttons that aren't physically present 2020-10-18 21:07:43 -05:00
Cameron Gutman
b5f5e73076 Revert "Remove button emulation"
This reverts commit 092830ed07.
2020-10-18 20:45:11 -05:00
Cameron Gutman
1fb5eff7f1 Update dependencies 2020-10-18 20:38:13 -05:00
Cameron Gutman
5116cfd141 Fix inverted assert condition 2020-10-18 20:08:55 -05:00
Cameron Gutman
e53a1f90b0 Correct some callers of time functions that expect monotonic clocks 2020-10-18 20:05:09 -05:00
Cameron Gutman
766c9628b0 Update moonlight-common-c with MTU test 2020-10-17 21:55:38 -05:00
Cameron Gutman
6a4abdd74c Update to AGP 4.1.0 2020-10-17 21:54:31 -05:00
Cameron Gutman
fc8bc5ba1e Version 9.7.4 2020-10-09 20:06:39 -05:00
Cameron Gutman
0fde5d44c0 Enable HEVC for all Amlogic decoders on API 28+ 2020-10-06 21:40:18 -05:00
Cameron Gutman
dc6b5a3d49 Update AGP to 4.0.2 2020-10-06 21:35:31 -05:00
Cameron Gutman
396522f249 Version 9.7.3 2020-09-07 11:50:03 -07:00
Cameron Gutman
86ab39e4ca Update moonlight-common-c for increased connection reliability 2020-09-06 18:18:41 -07:00
Cameron Gutman
a4c9cb0e55 Version 9.7.2 2020-09-05 10:53:04 -07:00
Cameron Gutman
e6c6feac10 Remove suffix_seekbar_bitrate string 2020-09-04 15:23:32 -07:00
Cameron Gutman
ca0aee58ab Bump bitrate max to 150 Mbps 2020-09-04 15:15:44 -07:00
Cameron Gutman
6391f2c43d Use a 1 Mbps key increment for bitrate 2020-09-04 15:14:30 -07:00
Cameron Gutman
32171bb70c Display bitrate in Mbps 2020-09-04 15:11:24 -07:00
Cameron Gutman
fd6675a3a3 Populate the external IP address when a PC is added manually using an RFC 1918 IPv4 address 2020-08-30 18:39:25 -07:00
bubuleur
9d883978a8 Mise à jour langue française (#865)
* Mise à jour langue française

* Update strings.xml

* Update strings.xml

* Update French

* Update \'
2020-08-30 13:32:05 -07:00
Cameron Gutman
1aae65575c Add warning if no key frames can be received in 10 seconds 2020-08-29 21:27:44 -07:00
Udalov Nikita
c5d58e1aab Update Russian translations (#872)
* Update Russian translations
2020-08-29 19:17:45 -07:00
Cameron Gutman
56394471fa Don't hide games immediately 2020-08-11 18:47:01 -07:00
Cameron Gutman
4cae6959df Update inconclusive test result text 2020-08-09 17:16:51 -07:00
Cameron Gutman
f02d7b4516 Version 9.7.1 2020-08-09 17:13:44 -07:00
Cameron Gutman
f5c83112df Update gitignore 2020-08-09 16:47:16 -07:00
Cameron Gutman
a413dc81c1 Avoid doing client connectivity tests on the main thread 2020-08-09 16:22:50 -07:00
Cameron Gutman
c9eddab191 Remove UDP 7 and add UDP 47009 for WoL 2020-08-09 14:40:44 -07:00
Cameron Gutman
ec1268bd71 Version 9.7 2020-08-09 12:08:50 -07:00
Cameron Gutman
22eb2b5823 Always show the network test option 2020-08-06 22:11:22 -07:00
Cameron Gutman
9669da026f Test network when the connection terminates due to lack of video traffic 2020-08-06 22:01:45 -07:00
Cameron Gutman
7b14e54eab Test network connectivity when adding a PC fails 2020-08-06 20:43:17 -07:00
Cameron Gutman
6b30ee4593 Change connection test domain name 2020-08-06 20:31:15 -07:00
Cameron Gutman
17c47a15da Improve display mode selection algorithm
- Allow the refresh rate to drop if it results in a better match for the stream frame rate
- Allow the resolution to drop for > 60 FPS streams to allow matching a higher refresh rate
2020-08-06 20:14:56 -07:00
Cameron Gutman
8f55517236 Prevent assert when control stream connection fails 2020-08-06 19:13:50 -07:00
Cameron Gutman
41ad086dfa Upgrade to AGP 4.0.1 2020-08-06 19:07:32 -07:00
Cameron Gutman
e19ef7dcae Remove redundant Cancel option in app grid menu 2020-08-04 02:09:33 -07:00
Cameron Gutman
f361265d70 Add automatic network test for failed connection stages 2020-08-01 22:56:32 -07:00
Cameron Gutman
ef72e3ef77 Only show the option to hide the app if it's not running or already hidden 2020-08-01 22:48:22 -07:00
Cameron Gutman
770f1a1ca0 Add network connection test 2020-08-01 22:19:40 -07:00
Cameron Gutman
e8fc91191f Add the option to hide games in the app list
Fixes #640
2020-08-01 18:20:39 -07:00
Cameron Gutman
105ad3317d Pass parent view into grid adapters 2020-08-01 17:52:55 -07:00
Cameron Gutman
22bf4775cd Enable poll() in ENet 2020-07-27 00:12:26 -07:00
Cameron Gutman
5c6be7969a Disable max operating rate trick on all Snapdragon 765G devices
Fixes #783
2020-07-26 22:39:10 -07:00
Cameron Gutman
c6e23f4be2 Update common-c with client connectivity test and select() replacement 2020-07-26 22:06:46 -07:00
Cameron Gutman
05547c22ec Use SecureRandom for PINs 2020-07-12 12:16:11 -07:00
Cameron Gutman
cc7ac79fa6 Version 9.6.4 2020-07-10 18:31:41 -07:00
Cameron Gutman
4c5c27dfc1 Re-enable the max operating rate trick on Android 10 except on the Mi 10 Lite 5G
It still provides nice performance gains on Pixel 2 running Android 10
2020-07-10 18:29:29 -07:00
Cameron Gutman
4aabfbd52e Add missing jlong cast to fix Lint warning 2020-07-07 01:10:10 -05:00
Cameron Gutman
6eab842361 Fix Lint error due to extra translated strings 2020-07-07 01:05:10 -05:00
a6969
b729dfd702 Added Ukrainian language (#857)
* Added Ukrainian language strings.xml

Translated the application into Ukrainian language.
2020-07-07 00:59:42 -05:00
Cameron Gutman
6366840781 Update common-c to remove FEC validation assert that fails on GFE 3.20.4 2020-07-07 00:58:44 -05:00
Cameron Gutman
704a2ee90b Propagate exceptions caused by GFE response parsing errors 2020-07-07 00:57:37 -05:00
Cameron Gutman
484be9bfe6 Wrap and propagate unexpected exceptions 2020-07-07 00:52:11 -05:00
Cameron Gutman
a99e070c26 Fix missing return causing invalid parameters to be passed to LiStartConnection() 2020-07-07 00:47:12 -05:00
Cameron Gutman
bf803f88af Refactor TLS initialization code 2020-07-06 02:32:06 -05:00
Cameron Gutman
9af6febca5 Fix pairing issue due to picking up a final local variable instead of a class member 2020-07-06 02:30:49 -05:00
Cameron Gutman
0101d0a1bd Fix TLS error when connecting to GFE 3.20.4 on Android 4.x 2020-07-06 01:44:35 -05:00
Cameron Gutman
266874609d Fix hostname validation for CA-issued certificates 2020-07-04 20:09:06 -05:00
Cameron Gutman
2ba7feedfc Fix several Lint warnings 2020-07-04 15:41:41 -05:00
Cameron Gutman
43c67b4939 Avoid using max operating rate on Android Q and non-Qualcomm devices 2020-07-01 11:26:40 -05:00
Cameron Gutman
2d9915e43a Enable GWP-ASan on Android 11 2020-07-01 11:07:53 -05:00
Cameron Gutman
2329b41bce Rethrow the original validation error if the cert isn't pinned or self-signed 2020-06-29 11:29:33 -07:00
Cameron Gutman
536496184e Use the default X509TrustManager to validate non-pinned certificates
This allows the certificate to be rotated without re-adding the PC.
2020-06-29 11:20:14 -07:00
Cameron Gutman
429c32477c Version 9.6.1 2020-06-25 22:15:24 -07:00
Cameron Gutman
f5d51b2061 Disable PiP option on Fire OS due to Amazon guidelines 2020-06-24 17:26:58 -07:00
Zero O
2ad1aaa277 Update strings.xml (#850)
update translation
2020-06-24 17:21:54 -07:00
Zero O
3afd32dbc1 Update strings.xml (#851)
update translation
2020-06-24 17:21:45 -07:00
Cameron Gutman
092830ed07 Remove button emulation
It was never well documented to users and it really only makes sense
with much older controllers that don't have Start or Select buttons.
2020-06-23 22:00:56 -07:00
Cameron Gutman
d118a6d3ff Prevent edges of analog sticks from being clipped 2020-06-23 21:48:50 -07:00
Cameron Gutman
fe97ffdc2f Slightly reduce size of analog sticks to allow a gap before the edge of the screen
This reduces false analog stick releases caused when the finger goes off the display's touch area.
2020-06-23 21:36:33 -07:00
Cameron Gutman
964d2ce59c Version 9.6 2020-06-18 23:11:35 -07:00
Cameron Gutman
dc52684cbc Update moonlight-common-c to fix QoS-related connection issues 2020-06-12 22:08:01 -07:00
Cameron Gutman
191bedc56f Improve behavior and description of small box art checkbox 2020-06-11 22:01:48 -07:00
Cameron Gutman
47b2ace7fd New app grid UI 2020-06-11 21:51:07 -07:00
Cameron Gutman
9fb7359a3e Use startAnimation() instead of setAnimation() 2020-06-11 21:47:28 -07:00
Cameron Gutman
4a5de26406 Remove the small PC grid UI 2020-06-11 21:32:39 -07:00
Cameron Gutman
6fa18e126f Remove list view in preparation for grid redesign 2020-06-11 21:21:37 -07:00
Cameron Gutman
1149002e0c Improve PC and game details dialogs 2020-06-11 20:36:59 -07:00
Cameron Gutman
d704cb0b50 Use SoftReferences instead of WeakReferences for the eviction cache 2020-06-11 19:10:43 -07:00
Cameron Gutman
d59e5ae9cf Store the original bitmap dimensions for the box art 2020-06-11 19:08:25 -07:00
Cameron Gutman
4587c1550d Cache WeakReferences to our box art bitmaps after LRU evictions 2020-06-10 23:13:07 -07:00
Cameron Gutman
b5bd329ada Fade in the box art when loading from the network 2020-06-10 22:52:37 -07:00
Cameron Gutman
beccd7a4ac Fade in the box art as we load it 2020-06-10 22:37:54 -07:00
Cameron Gutman
61262fa939 Refactor grid adapters for new grid UI 2020-06-10 22:13:02 -07:00
Cameron Gutman
7c6b006631 Remove OSC rumble option if a vibrator isn't present 2020-06-10 21:15:21 -07:00
Cameron Gutman
dbd149354a Change "crashes" to "instability" 2020-06-10 21:09:24 -07:00
Cameron Gutman
4306ba5004 Add a mapping for the Nintendo Switch Pro controller
Fixes #842
2020-06-10 21:05:08 -07:00
Cameron Gutman
6de370b82f Update for Android 11 2020-06-10 20:31:32 -07:00
Cameron Gutman
45781666b8 Disable the latency toast by default
It causes crashes on the MiBox
2020-06-06 18:24:34 -07:00
Cameron Gutman
538231eb6f Attempt to appease Amazon content review 2020-06-06 17:53:09 -07:00
Cameron Gutman
eb74f87f2c Move PiP and unlock FPS options out of basic settings 2020-06-06 17:44:38 -07:00
Cameron Gutman
59d71ffdcf Don't show PiP option on devices where PiP is disabled 2020-06-06 17:32:26 -07:00
Cameron Gutman
d1b93d4011 Remove vibration option if the device can't vibrate 2020-06-06 17:25:01 -07:00
Cameron Gutman
d8ddf2e740 Update NDK for Travis CI 2020-05-28 22:19:58 -07:00
Cameron Gutman
581327dc8e Improve resolution preference storage to remove 16:9 assumptions 2020-05-28 22:05:57 -07:00
Cameron Gutman
76e4512a0c Update for Android Studio 4.0 2020-05-28 21:50:28 -07:00
Cameron Gutman
efdd55beca Add Download links 2020-05-27 19:47:30 -07:00
Cameron Gutman
2c115649b9 Update README 2020-05-27 19:44:56 -07:00
Cameron Gutman
2ddcc31a93 Update metadata for Quadro streaming 2020-05-27 18:34:59 -07:00
Cameron Gutman
3bcce5b749 Version 9.5.1 2020-05-27 18:34:26 -07:00
Cameron Gutman
80dac27214 Update moonlight-common-c 2020-05-27 00:02:33 -07:00
Cameron Gutman
4a1177d048 Use a better workaround for the GFE 3.20.3 high FPS bug 2020-05-25 19:28:00 -07:00
Cameron Gutman
4725d8f270 Revert "Disable SOPS for streams over 60 FPS for GFE 3.20.3"
This reverts commit 63072aa8e1.
2020-05-25 19:24:33 -07:00
Zero O
07b3528515 Update strings.xml (#833)
update translation
2020-05-20 19:27:47 -07:00
Zero O
d2d1b1ea26 Update strings.xml (#834)
update translation
2020-05-20 19:27:32 -07:00
Cameron Gutman
232b897abc Version 9.5 2020-05-16 21:40:41 -07:00
Cameron Gutman
efd076bc6c Ignore absolute touch events from outside the stream view 2020-05-12 00:20:07 -07:00
Cameron Gutman
cc877480ff Add an option for absolute touch mode 2020-05-11 23:53:49 -07:00
Christoph Papke
363145a284 Optimize button mapping for 8BitDo controllers (#826)
* Optimize button mapping for 8BitDo controllers #825
2020-05-05 16:04:31 -07:00
Cameron Gutman
755571ad33 Switch on-screen control buttons when flip face buttons is enabled 2020-05-04 22:23:03 -07:00
Eero Kelly
39edb55721 Add option to invert A/B X/Y (#824)
* Add option to invert A/B X/Y

* Remove redundant prefConfig
2020-05-04 22:10:35 -07:00
Cameron Gutman
15aa7ecc2e Add a friendly error message when no video traffic is received 2020-05-01 21:54:26 -07:00
Cameron Gutman
ce9e91153e Add special error text for the -1 launch error code 2020-04-25 16:10:44 -07:00
Cameron Gutman
9ee0a46606 Add new init packet to switch out of BT mode 2020-04-24 17:47:31 -07:00
Cameron Gutman
20dc351f4c Fix parsing rare GFE status code of 0xFFFFFFFF 2020-04-23 18:47:01 -07:00
Cameron Gutman
c30c54d562 Version 9.2.1 2020-04-23 18:40:40 -07:00
Cameron Gutman
45ff51c0d2 Fix mouse jumping on Shield devices when clicking or scrolling 2020-04-23 00:13:19 -07:00
Cameron Gutman
5b86e99138 Improve dead zone precision for stylus input 2020-04-22 22:46:05 -07:00
Cameron Gutman
0c72910eb7 Fix tap location for styluses without hover support 2020-04-22 22:00:25 -07:00
Cameron Gutman
3b0f485b41 Version 9.2 2020-04-20 14:44:38 -07:00
Cameron Gutman
2be2c95212 Avoid crashing if we get an invalid status code back from GFE 2020-04-18 22:46:21 -07:00
Cameron Gutman
e7aeeb8bd5 Fix one more place where the HTTP error code was lost 2020-04-18 18:03:29 -07:00
Cameron Gutman
73df93f86a Display the error code correctly for HTTPS errors 2020-04-18 17:47:27 -07:00
Cameron Gutman
9cd4d5e2aa Implement a post-tap deadzone for stylus input 2020-04-18 01:03:49 -07:00
Cameron Gutman
c3b81554f4 Add absolute mouse support for styluses and mice prior to Oreo 2020-04-18 00:02:36 -07:00
Cameron Gutman
6f79c52fc5 Plumb sendMousePosition() through to moonlight-common-c 2020-04-17 22:37:09 -07:00
Cameron Gutman
29bc3e022b Update AGP to 3.6.3 2020-04-17 22:36:19 -07:00
Cameron Gutman
7d03203d83 Add special Start and Select mappings for the ROG Kunai 2020-04-15 23:47:09 -07:00
Cameron Gutman
11dde835d1 Version 9.1 2020-04-14 22:29:41 -07:00
Cameron Gutman
52c47c288c Disable the 7.1 surround sound option prior to Lollipop 2020-04-12 12:28:42 -07:00
Cameron Gutman
63072aa8e1 Disable SOPS for streams over 60 FPS for GFE 3.20.3 2020-04-12 12:13:38 -07:00
Cameron Gutman
4cca3ac922 Update moonlight-common-c to avoid termination delay on GFE 3.20.3 2020-04-12 12:13:04 -07:00
Cameron Gutman
604bc1ec11 Add Romanian translation from KiralyCraft on Discord 2020-04-12 12:04:04 -07:00
Cameron Gutman
5d7fbf3195 Fix indentation of arrays.xml 2020-04-10 22:17:28 -07:00
Zero O
8c56e6f0d4 Update arrays.xml (#813)
translation update
2020-04-10 22:12:18 -07:00
Zero O
2069be7932 Update arrays.xml (#814)
translation update
2020-04-10 22:11:56 -07:00
Zero O
9c1c2991a9 Update strings.xml (#812)
translation update
2020-04-10 21:13:10 -07:00
Zero O
81dabf2713 Update strings.xml (#811)
translation update
2020-04-10 21:12:48 -07:00
Cameron Gutman
27520cb77e Use GetPrimitiveArrayCritical() for audio data to avoid extra copies 2020-04-09 19:12:09 -07:00
Cameron Gutman
f555d3dae0 Version 9.0 2020-04-07 19:42:47 -07:00
Cameron Gutman
70f1a2cacb Fix 7.1 AudioTrack initialization on pre-Lollipop devices 2020-04-07 19:29:07 -07:00
Cameron Gutman
7f15aaa2e5 Update to AGP 3.6.2 2020-04-07 19:22:02 -07:00
Cameron Gutman
e5726205c4 7.1 surround sound is supported now 2020-04-07 19:21:45 -07:00
Cameron Gutman
07fabc0663 Fix CheckJNI abort with rumble values greater than 0x7FFF 2020-04-07 19:21:24 -07:00
Cameron Gutman
800f97ae85 Remove translations for old 5.1 surround sound option 2020-04-04 10:15:03 -07:00
bubuleur
3ee5b284e1 Update french "summary_audio_config_list" (#809) 2020-04-04 10:08:42 -07:00
bubuleur
c0389f0da9 Update french "audio_config_names" (#808) 2020-04-04 10:08:18 -07:00
bubuleur
a7a4d7ded5 Update french 2 (#807)
* Update french 2

* Update strings.xml
2020-04-03 18:13:30 -07:00
bubuleur
87cd974b79 Update French 1 (#806)
* Update French 1

* Update arrays.xml
2020-04-03 18:10:34 -07:00
Cameron Gutman
7faaac31ff Use EF instead of CS7 for DSCP on ENet traffic 2020-04-03 18:04:04 -07:00
Cameron Gutman
7386eb2a78 Add support for 7.1 surround sound 2020-04-03 18:03:01 -07:00
Cameron Gutman
49a1524f4f Refactor audio configuration in preparation for 7.1 surround sound 2020-04-03 17:47:57 -07:00
Cameron Gutman
c957b8b06b Version 8.12 2020-03-29 16:46:42 -07:00
Cameron Gutman
a3a6e14d80 Reduce retransmission delay on packet loss and enable QoS marking on ENet traffic 2020-03-29 16:31:23 -07:00
Cameron Gutman
7231f5468b Version 8.11 2020-03-25 00:07:53 -07:00
Cameron Gutman
4dfb0d7220 Fix crash during crash report generation 2020-03-22 13:48:17 -07:00
Cameron Gutman
2f4f53b048 Fix mouse back button closing the app with mouseNavButtons enabled 2020-03-21 15:34:03 -07:00
Cameron Gutman
b6e8389544 Fix incorrect exception handling in JNI code 2020-03-21 14:30:31 -07:00
Cameron Gutman
d113878613 Use current display refresh rate only for non-TV devices 2020-03-21 13:43:59 -07:00
Cameron Gutman
f7ed7e06db Revert "Calculate FPS using the actual display refresh rate rather than the requested one"
This breaks refresh rate detection on the Shield Android TV.

This reverts commit af5e7a0e33.
2020-03-21 13:31:48 -07:00
Cameron Gutman
977a1d4a3c Fix IllegalArgumentException when trying to repin a disabled shortcut 2020-03-21 13:25:55 -07:00
Cameron Gutman
eefc08db47 Use 10 ms audio samples on low bandwidth connections 2020-03-21 01:01:45 -07:00
Cameron Gutman
ab2b1663d3 Minor tweaks and fixes to OSC opacity options 2020-03-21 00:54:31 -07:00
gotoAndDie
04b8a718e3 Add opacity settings to on-screen controls (#798)
* Restore resize controls, Make buttons oval

* Create new default configuration

* Split Configuration Mode into separate Move and Resize modes

* Add transparency setting for on-screen buttons

* Updated translations for on-screen controls

Co-authored-by: Leo <chun.huang@student.manchester.ac.uk>
2020-03-21 00:41:27 -07:00
Cameron Gutman
37cf260ba6 Merge pull request #799 from gotoAndDie/rt-onefinger
Allow RT/LT and A/B/X/Y/LB/RB to be triggered together with one finger
2020-03-21 00:23:24 -07:00
Cameron Gutman
8f91fe4cd1 Revert "Repeat key down events are needed for proper key repeating"
This key repeat filtering seems to be needed now. See #800.

This reverts commit 53dccbde2a.
2020-03-20 23:49:52 -07:00
Leo
9246ad412f Make it possible to press the RT button and the other buttons with the same finger 2020-03-13 18:48:50 +00:00
Cameron Gutman
1ccbbdd4fb Version 8.10 2020-03-08 19:48:53 -07:00
Cameron Gutman
16cf37994d Only suppress duplicate d-pad events if the hat has received input. Fixes #796 2020-03-04 18:48:14 -08:00
Cameron Gutman
01e84624c2 Remove stale moonlight-common reference from settings.gradle 2020-03-03 00:13:48 -08:00
Cameron Gutman
939cd7cf70 Update OkHttp to 3.12.10 2020-03-02 22:49:44 -08:00
Cameron Gutman
4b11603035 Fix back button on Shield Portable and standardize external/internal classification 2020-03-02 22:47:47 -08:00
Cameron Gutman
ca18b6b052 Update to AGP 3.6.1 2020-03-01 13:12:04 -08:00
Cameron Gutman
3d0d19e561 Pass-through back button on external devices that don't look like gamepads 2020-03-01 12:45:00 -08:00
Cameron Gutman
ae463a8735 Emulated button combos must not be pressed with other buttons 2020-02-26 20:38:53 -08:00
Cameron Gutman
7e797829ae Also destroy the mouse emulation timer on device disconnect 2020-02-26 20:29:28 -08:00
Cameron Gutman
431ed6bc5d Cancel the mouse emulation timer when the stream ends 2020-02-26 20:18:11 -08:00
Cameron Gutman
e9bb711c42 Add Start+Back+LB+RB combo for disconnecting the session 2020-02-26 19:54:53 -08:00
Cameron Gutman
623bc5c156 Fix check for gamepad buttons. Fixes #788 2020-02-26 19:19:43 -08:00
Cameron Gutman
cfefef4619 Downgrade OkHTTP to 3.12.8 due to square/okhttp#5826 2020-02-25 22:40:17 -08:00
Cameron Gutman
4a9a881c1f Add missing else block 2020-02-25 22:26:52 -08:00
Cameron Gutman
13a06d585c Update dependencies 2020-02-25 20:49:30 -08:00
Cameron Gutman
1c8ad64da0 Only set KEY_FRAME_RATE on M+ to reduce compatibility risk 2020-02-25 20:24:18 -08:00
Cameron Gutman
1d8925de57 Fix NDK version in Travis CI build 2020-02-25 00:38:41 -08:00
Cameron Gutman
0eb7e779b8 Update Travis CI to build-tools-29.0.3 2020-02-25 00:25:15 -08:00
Cameron Gutman
a4b86eefe2 Change errorCode from long to int to fix 32-bit platforms 2020-02-24 23:24:22 -08:00
Cameron Gutman
902a58bc70 Improve video decoder init failure message 2020-02-24 23:23:23 -08:00
Cameron Gutman
a34a44f29a Fix crash on Android 5.0 and earlier 2020-02-24 22:05:26 -08:00
Cameron Gutman
454fe80172 Update Gradle and AGP for AS 3.6.0 2020-02-24 21:49:14 -08:00
Cameron Gutman
81b6a8a311 Set the vendor.qti-ext-dec-low-latency.enable Qualcomm vendor extension 2020-02-22 17:06:32 -08:00
Cameron Gutman
3011a5bad7 Use the unmodified FPS value when sending the launch request 2020-02-22 01:28:41 -08:00
Cameron Gutman
dcb7be3acd Use the original FPS value for KEY_FRAME_RATE 2020-02-22 01:18:11 -08:00
Cameron Gutman
68a6b510b1 Set KEY_FRAME_RATE for devices where KEY_OPERATING_RATE silently fails 2020-02-22 01:05:26 -08:00
Cameron Gutman
dca3e89303 Log configured MediaFormat and achievable FPS ranges 2020-02-22 01:04:18 -08:00
Cameron Gutman
bae6fef588 Log the actual input and output formats 2020-02-21 22:02:37 -08:00
Cameron Gutman
37f65e43a5 Add error code on connection failure dialog 2020-02-21 22:01:12 -08:00
Cameron Gutman
8c910101c7 Fix Lint errors on API level 16 2020-02-19 23:53:44 -08:00
Cameron Gutman
112d9c41eb Use KEY_LOW_LATENCY to request low-latency decoding on Android R 2020-02-19 23:40:06 -08:00
Cameron Gutman
c91d1097f6 Set preferMinimalPostProcessing on Android R 2020-02-19 23:29:37 -08:00
Cameron Gutman
105c2c9eef Version 8.9 r2 2020-01-26 10:52:09 -08:00
Cameron Gutman
b754d2de28 Fix crash with OSC disabled 2020-01-26 10:45:12 -08:00
Cameron Gutman
f6425c7ec6 Version 8.9 2020-01-25 20:07:15 -08:00
Cameron Gutman
e690c9b8c8 Fix build error due to Lollipop API 2020-01-19 15:46:47 -08:00
Cameron Gutman
f87cbac77c Fix R3 button X position and move L3+R3 to the lower part of the screen 2020-01-18 23:14:51 -08:00
gotoAndDie
150bd313cf Increase usability of on-screen virtual controller (#782)
* Restore resize controls, Make buttons oval

* Create new default configuration

* Split Configuration Mode into separate Move and Resize modes
2020-01-18 23:13:07 -08:00
Cameron Gutman
bc90cb894c Add German option to language picker and translate French option to French 2020-01-18 22:04:00 -08:00
Cameron Gutman
c51a75a681 Merge branch 'translation-de' of https://github.com/uniqx/moonlight-android 2020-01-18 21:54:05 -08:00
bubuleur
68aa9bd12d Add French translation of arrays.xml 2020-01-18 21:51:21 -08:00
bubuleur
1fb6bf4d70 Update French (#768)
* Update French
2020-01-18 21:46:40 -08:00
Cameron Gutman
b4df3658f1 Merge pull request #764 from ZerOri/master
Update strings.xml
2020-01-18 21:45:52 -08:00
Cameron Gutman
4efb4d9b24 Merge pull request #765 from ZerOri/patch-1
Update strings.xml
2020-01-18 21:45:29 -08:00
Cameron Gutman
d61e893731 Centralize Discord invite links 2020-01-18 10:53:37 -08:00
Cameron Gutman
951e44728e Version 8.8.1 2020-01-03 22:05:58 -06:00
Cameron Gutman
8dcdf73222 Fix accidentally inverted condition for VUI parameter removal 2020-01-03 21:59:25 -06:00
Cameron Gutman
44c3b0af57 Version 8.8 2020-01-02 16:14:15 -06:00
Cameron Gutman
2b295400ac Avoid using RFI for HEVC on newer MediaTek SoCs 2020-01-02 16:13:19 -06:00
Cameron Gutman
aa8d8e93d2 Whitelist newer Bravia devices for HEVC to minimize crashes 2019-12-31 12:59:20 -06:00
Cameron Gutman
89be7eac0e Update AGP to 3.5.3 2019-12-15 12:04:55 -08:00
Cameron Gutman
f3847b932b Leave H.264 SPS VUI parameters in place on devices running API 26+ 2019-12-15 12:04:35 -08:00
Zero O
5e4f37532c Update strings.xml
Complete revision for cht translation according to the latest version
2019-12-05 19:40:58 +08:00
Zero O
f3f5ca74a3 Update strings.xml
Complete revision for Chs translation
2019-12-05 19:31:12 +08:00
Cameron Gutman
e50b7076a1 Version 8.7 2019-12-04 18:21:11 -08:00
Cameron Gutman
36ab5aa1b6 Update common-c to fix logic error in audio duration selection 2019-12-01 20:31:39 -08:00
Cameron Gutman
a0a2b299d9 Merge pull request #758 from duchuule/hotfix1
fix bug where touch hitbox of analog stick is not full circle
2019-12-01 22:29:02 -06:00
Cameron Gutman
14d354fc29 Whitelist all C2 decoders for direct submit and HEVC 2019-12-01 20:20:57 -08:00
Cameron Gutman
342515f916 Force remote streaming optimizations if a VPN is active 2019-12-01 20:05:09 -08:00
Cameron Gutman
5f5944c237 Improve low bandwidth audio performance and fix RTSP issues with broken PMTUD 2019-11-30 22:14:32 -06:00
Cameron Gutman
c025432ad6 Support 20 ms audio frames 2019-11-29 18:04:57 -06:00
Duc Le
171a6437fe fix bug where touch hitbox of analog stick is not full circle 2019-11-26 04:40:22 -06:00
Cameron Gutman
11b3648fac Fix auto-comment line breaks 2019-11-16 12:23:27 -08:00
Cameron Gutman
d1fae89d6d Don't change level_idc for high refresh rate streams 2019-11-10 18:29:31 -08:00
Cameron Gutman
5c06848fe9 Version 8.6 2019-11-10 18:18:11 -08:00
Cameron Gutman
b50e506e58 Attempt to fix line breaks in auto-comment response 2019-11-09 16:34:25 -08:00
Cameron Gutman
59fafa163d Add configuration for auto-comment bot 2019-11-09 15:00:13 -08:00
Cameron Gutman
22d84b5763 Bind to the underlying network when a VPN is connected 2019-11-09 12:57:54 -08:00
Cameron Gutman
6d186892a8 Fix errant touch events after a cancelled gesture 2019-11-09 11:23:50 -08:00
Cameron Gutman
88d6143897 Display a placeholder box art bitmap while loading box art 2019-11-05 00:19:58 -08:00
Cameron Gutman
b729fba75e Update AGP to 3.5.2 2019-11-04 20:59:56 -08:00
Cameron Gutman
c0d3f9fa48 Abort pairing if another pairing attempt is in progress 2019-11-04 20:27:05 -08:00
Cameron Gutman
af5e7a0e33 Calculate FPS using the actual display refresh rate rather than the requested one 2019-11-04 20:22:12 -08:00
Cameron Gutman
371d96ea65 Fix VPN check on KitKat and below 2019-11-04 19:05:34 -08:00
Cameron Gutman
e9e332ff85 Don't update the external IP address when connected to a VPN 2019-11-04 19:00:29 -08:00
Cameron Gutman
e133ac2815 Version 8.5 2019-10-29 22:06:28 -07:00
Cameron Gutman
1dba5d147e Add a hack for massive video latency on Pixel 4 after display mode change 2019-10-29 21:38:06 -07:00
Cameron Gutman
1616c0b022 Fix codec capabilities on devices launching with Q and C2 codecs 2019-10-24 20:20:26 -07:00
Cameron Gutman
bcee2cf0e3 Update moonlight-common-c submodule 2019-10-24 19:57:03 -07:00
Cameron Gutman
3e7ddab0e9 Blacklist 59 FPS on BRAVIA_ATV3 due to crash reports 2019-10-20 00:06:17 -07:00
Cameron Gutman
5da0177356 Convert tabs to spaces 2019-10-19 23:59:33 -07:00
Cameron Gutman
7e21638811 Don't double count USB attached Xbox One controllers 2019-10-16 19:26:24 -07:00
Cameron Gutman
db5b7ab867 Version 8.4.1 2019-10-16 19:10:56 -07:00
Cameron Gutman
3bcc1c84bb Fix crash on controllers with RX and RY but no Z and RZ axes 2019-10-16 19:02:51 -07:00
Cameron Gutman
d46053f8d6 Preserve old DS4 detection behavior on Android 4.3 and below 2019-10-15 21:15:03 -07:00
Cameron Gutman
00a5fed9e9 Update AGP to 3.5.1 2019-10-15 20:58:03 -07:00
Cameron Gutman
b6315a715a Improve support for DualShock 4 and Xbox One controllers on 4.14+ kernels 2019-10-15 20:57:33 -07:00
Cameron Gutman
0da8303468 Don't use the USB driver for Xbox One gamepads on 4.14+ kernels 2019-10-15 20:05:01 -07:00
Cameron Gutman
c821c4684f Allow FFmpeg decoders on Android x86. Closes #630 2019-10-15 00:11:43 -07:00
Cameron Gutman
6bae33f822 Merge pull request #739 from vanitasvitae/patch-1
Fix German short_description
2019-10-15 00:06:12 -07:00
Cameron Gutman
08d4ab67a6 Update moonlight-common-c submodule 2019-10-12 19:50:30 -07:00
Paul Schaub
62203d2f21 Fix German short_description
fixed a typo
2019-10-06 12:44:40 +02:00
Cameron Gutman
4968dcc558 Version 8.3 2019-09-14 20:23:46 -07:00
Cameron Gutman
6d66d1371f Fix TV view padding on Android Q 2019-09-14 20:14:31 -07:00
Cameron Gutman
b87ca71103 Treat all InputDevices as external on the Tinker Board 2019-09-14 20:08:26 -07:00
Cameron Gutman
c251cd2e8f Fix control stream connection error on multi-homed hosts 2019-09-14 14:11:14 -07:00
Cameron Gutman
593616d2d9 Fix layout transitions on foldable devices 2019-09-08 11:11:02 -07:00
Cameron Gutman
a2fc62a4a8 Version 8.2 2019-08-23 19:13:04 -07:00
Cameron Gutman
fd457c5dea Bring up the keyboard when entering the Add PC 2019-08-21 18:45:58 -07:00
Cameron Gutman
64e1ba500c Restore TV-specific padding (but slightly less) 2019-08-20 19:35:43 -07:00
Cameron Gutman
235a0635be Remove moonlight-common subproject 2019-08-20 18:51:13 -07:00
Cameron Gutman
61f8fa7c5a Merge submodule contents for moonlight-common/master 2019-08-20 18:20:48 -07:00
Cameron Gutman
7c3c107381 Remove submodule moonlight-common 2019-08-20 18:20:32 -07:00
Cameron Gutman
555477751f Gradle updates for AS 3.5 2019-08-20 18:07:29 -07:00
Cameron Gutman
9364f43c52 Allow pinning an existing trusted cert for testing 2019-08-20 18:07:12 -07:00
Cameron Gutman
0be3169c2c Update common-c to perform parallel STUN requests 2019-08-20 18:00:43 -07:00
Cameron Gutman
5199d90505 Allow pinning an existing trusted cert for testing 2019-08-20 17:58:37 -07:00
Cameron Gutman
5b5277bf3f Fix grid/list items being occluded by the navbar on Q with gestures off 2019-08-13 22:18:03 -07:00
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
f28f9bc65f Update common-c to fix surround sound and STUN fallback 2019-08-07 23:17:53 -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
2b02af5d98 Update common-c to fix termination error code on GFE 3.19.0.107 2019-07-28 09:50:21 -07:00
Cameron Gutman
613c068523 Add additional ports and addresses for WoL 2019-07-27 17:22:13 -04:00
Cameron Gutman
0b0181f35c Ignore .cxx directory 2019-07-27 16:42:46 -04: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
Michael Pöhn
fdd4c0bbe1 german translation 2019-07-17 14:15:58 +02: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
9997f164b9 Add support for IPv6-only hosts 2019-07-15 01:27:49 -07:00
Cameron Gutman
2f7ac67cb0 Don't consider ULAs global addresses 2019-07-14 15:07:04 -07:00
Cameron Gutman
27f0fd63b3 Add support for IPv6-only mDNS 2019-07-14 14:17:39 -07:00
Cameron Gutman
9f56fdfbb9 Add support for IPv6-only mDNS 2019-07-14 14:17:20 -07:00
Cameron Gutman
83b66b19de Add support for zero configuration IPv6 streaming 2019-07-14 00:21:13 -07:00
Cameron Gutman
9a4f85e752 Resolved services are already handled inline 2019-07-14 00:18:39 -07:00
Cameron Gutman
d00824d49c Add support for discovering IPv6 addresses with mDNS 2019-07-14 00:07:52 -07:00
Cameron Gutman
ba0171221c Upgrade BouncyCastyle to 1.62 2019-07-13 23:57:21 -07:00
Cameron Gutman
68040394fb Update to OkHttp 3.12.3 2019-07-13 23:55:48 -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
Cameron Gutman
936834e396 Don't manually specify a build tools version 2019-07-10 20:12:19 -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
0afda10bcb Fix streaming on old servers 2019-07-05 21:28:21 -07:00
Cameron Gutman
754773420f Generate SHA-256 client certificates instead of SHA-1 2019-07-05 21:23:18 -07:00
Cameron Gutman
71aadfa2f5 Don't request an explict TLS version 2019-07-05 21:22:27 -07:00
Cameron Gutman
f7bfa63145 Ignore reported pairing state if pinned cert is not found 2019-07-05 19:40:11 -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
a1b58ab2fc Target API 29 2019-06-26 17:39:01 -07:00
Cameron Gutman
d32c0e32d0 Remove old Eclipse files 2019-06-26 17:36:22 -07:00
Cameron Gutman
9cd71e2855 Remove TinyRTSP since it is no longer used 2019-06-26 17:35:26 -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
d7bdfb4db9 Fix video on GFE 3.19 2019-05-27 01:41:57 -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
5cca5cd352 Add MoonBridge.getPendingAudioFrames() and MoonBridge.getPendingVideoFrames() 2019-05-07 20:35:31 -07:00
Cameron Gutman
d48e964d05 Use a short[] for the audio buffer to avoid using deprecated AudioTrack functionality 2019-05-07 17:54:47 -07:00
Cameron Gutman
ad94978f98 Removed hardcoded samples per frame 2019-05-07 17:40:43 -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
1d65baa981 Update common-c to fix receive time on frames 2019-04-30 23:18:44 -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
8b7287a5d6 Update common-c with lower video packet overhead 2019-04-27 21:59:01 -07:00
Cameron Gutman
a454b0ab78 Update moonlight-common with perf improvements 2019-04-26 18:37:27 -07:00
Cameron Gutman
c4ae049091 Update common-c with improved video performance and reduced audio bandwidth 2019-04-26 18:35:41 -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
0e4e3d80d2 Update moonlight-common-c for tweaked lossy thresholds and faster exits 2019-04-07 12:14:57 -07:00
Cameron Gutman
59b2449cdc Update GFE error message to be more clear 2019-04-07 12:14:41 -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
640255efb2 Update native libraries (OpenSSL 1.1.1b and libopus 1.3) 2019-03-20 19:55:06 -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
Cameron Gutman
1bffa6bf41 Update moonlight-common-c with connection listener and build warning fixes 2019-03-20 18:50:17 -07:00
Cameron Gutman
69b175573d Move libopus includes to include folder 2019-03-20 18:28:22 -07:00
Cameron Gutman
0eaff9d328 Remove MIPS prebuilts 2019-03-19 21:14:44 -07:00
Cameron Gutman
44905ed774 Use jmDNS from JCenter 2019-03-19 19:29:53 -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
10e0a262f7 Implement rumble support 2019-02-16 16:52:17 -08:00
Cameron Gutman
6a939e7495 Don't display the termination dialog for intended terminations 2019-02-10 02:28:11 -08:00
Cameron Gutman
422e703c2f Update common-c with termination reason propagation 2019-02-10 02:24:51 -08:00
Cameron Gutman
f8ba7cf190 Update common with SOPS fixes 2019-02-09 20:59:59 -08:00
Cameron Gutman
27191da45e Fix SOPS issues causing 720p60 settings on non-standard resolutions and FPS values 2019-02-09 20:59:36 -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
24926c75f1 Update common-c for GFE 3.17 2019-02-05 21:54:39 -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
a83f6f2ef7 Allow caller to provide STUN server 2019-01-04 18:05:01 -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
0220dd921a Stop suppressing exceptions 2019-01-01 23:28:41 -08:00
Cameron Gutman
0f9cba1053 Fix crash due to a null computer uuid 2019-01-01 22:34:27 -08:00
Cameron Gutman
05e4792d6f Update common-c with strict bitrate logic 2019-01-01 19:46:41 -08:00
Cameron Gutman
c9b5c00756 Simplify openHttpConnectionToString() 2019-01-01 19:45:41 -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
6f35b991b7 Change uuid field to String type due to new format UUIDs that fail to parse on GFE 3.16 2018-12-27 23:47:01 -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
a7023f52aa Update common-c with bitrate fix 2018-12-24 19:11:22 -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
752b204be8 Use a shared UID for all Moonlight clients 2018-12-22 21:01:40 -08:00
Cameron Gutman
7d76bf7868 Require cert pinning for HTTPS 2018-12-22 20:13:11 -08:00
Cameron Gutman
564e7c71a6 Require cert pinning for HTTPS 2018-12-22 20:12:31 -08:00
Cameron Gutman
db49077b9b Add cert pinning during pairing 2018-12-21 21:00:53 -08:00
Cameron Gutman
67f01fbdca Add cert pinning during pairing 2018-12-21 20:45:58 -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
02b74fbbc5 Update common-c to help address controller packet loss 2018-11-25 15:01:16 -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
efcfcf88db Remove the 4K display check and just check for GFE 3+ 2018-11-22 02:52:14 -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
68028242b4 Add support for automatic remote streaming detection 2018-11-21 21:19:10 -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
bae3b4a6e8 Update common-c for GFE 3.16 2018-11-15 13:12:20 -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
514e0ca2c9 Rework active address to not be based on reachability and allow for a manual address 2018-10-27 01:50:35 -07:00
Cameron Gutman
c9cf485025 Add Java bindings to STUN code in moonlight-common-c 2018-10-26 23:15:06 -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
16c4b2532d Add support for X1 and X2 mouse buttons 2018-10-05 00:55:08 -07:00
Cameron Gutman
7dea322bbd Update build tools to 28.0.3 2018-09-29 15:37:31 -07:00
Cameron Gutman
a64db9d86f Update build tools to 28.0.3 2018-09-29 15:37:10 -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
14d9f77e4e Update common-c with split encode change 2018-09-22 20:22:36 -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
40c3db0214 Update common-c with FEC fixes 2018-09-18 20:25:10 -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
f7c5039912 Update build tools to 28.0.2 2018-08-12 00:43:39 -07:00
Cameron Gutman
79a75b9d19 Update common with audio fix and game launch fix 2018-08-12 00:40:56 -07:00
Cameron Gutman
3c37c89db8 Update common-c 2018-08-12 00:39:38 -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
6995a27126 Properly handle failed app start 2018-07-28 00:13:52 -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
f61540c099 Update build tools to 28.0.1 2018-07-16 18:24:36 -07:00
Cameron Gutman
6a9f8da14e Update common to reduce syscall overhead 2018-07-16 18:21:48 -07:00
Cameron Gutman
6a6bd9fb0b Update common-c to reduce syscall overhead 2018-07-16 18:18:02 -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
c2a16a9b4a Avoid sending input to the remote PC when running under Monkey 2018-06-20 01:25:45 -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
b64c84a5c3 Update common-c to fix reconnection issue 2018-06-09 21:39:05 -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
42a502eff1 Fix Lint warnings 2018-06-09 18:08:38 -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
46d72b912c Update common-c to fix race condition 2018-06-08 00:22:29 -07:00
Cameron Gutman
722f397819 Target API 28 2018-06-08 00:19:57 -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
8f169b976b Update common-c to fix channel mapping error in 5.1 high quality mode 2018-05-27 20:03:34 -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
53452d22c0 Update common-c with audio improvements 2018-05-27 18:49:13 -07:00
Cameron Gutman
9d4e9631bc Hide symbols from static libraries 2018-05-27 13:48:09 -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
5942545b9c Remove explicit build tools version 2018-03-20 17:27:03 -07:00
Cameron Gutman
89ef16c02e Fix level_idc 31 patch 2018-03-18 00:59:58 -07:00
Cameron Gutman
58b6ed8d00 Update Gradle wrapper 2018-03-11 18:12:01 -07:00
Cameron Gutman
7d01e1a7a4 Fix landscape orientation lock 2018-03-11 15:31:10 -07:00
Cameron Gutman
ab769a1606 Version 5.7.2 2018-03-07 18:53:38 -08:00
Nikita Glazkov
3ac9abbab1 Russian translation (#546)
* Fix russian translation

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

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

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

* Fixed up translation -- Ready for pull request

* Updated Translation to comply with Moonlight update with H265

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

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

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

It might have some typos.

* translated one more sentence

* some fixes

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

* few modifications
2017-05-04 21:35:06 -07:00
Cameron Gutman
b2a36c2c73 Use app context for getting WiFi service to address warnings in new build tools 2017-03-10 22:18:23 -08:00
Cameron Gutman
7978687bfc Update gradle and gradle wrapper 2017-03-10 22:08:09 -08:00
Cameron Gutman
f612ec80e2 Fix active gamepad mask when multi-controller is disabled 2017-02-06 19:26:05 -08:00
Cameron Gutman
7df1a39fcb Update common jar to allow the client to tell the host which controllers are attached 2017-02-04 21:02:11 -08:00
Cameron Gutman
4566c1855b Allow clients to correctly send the number of controllers connected 2017-02-04 21:00:10 -08:00
Cameron Gutman
a539ac62ec Version 4.8.3 2017-01-02 19:03:14 -08:00
Cameron Gutman
fa52e5edc2 Remove automatic disabling of back button due to false-positives 2017-01-02 19:02:30 -08:00
Cameron Gutman
3ca681f050 Set isGame to get lower video processing latency on some Android TVs 2017-01-02 18:52:20 -08:00
Cameron Gutman
8086c3d46b Bump version to 4.8.2 2016-12-13 21:28:45 -08:00
Cameron Gutman
928fca843f Update moonlight-common to support GFE 3.2 2016-12-13 21:27:28 -08:00
Cameron Gutman
25d74785d0 Update build tools to 25.0.2 2016-12-13 20:54:24 -08:00
Cameron Gutman
e12a8e7946 Update Gradle to 2.2.3 2016-12-13 20:51:39 -08:00
Cameron Gutman
195bf8ed55 Apparently we're back to 8 byte first frame headers on GFE 3.2 2016-12-13 20:33:58 -08:00
colin-foster-in-advantage
b14f2ce219 Fixed typo in NAL parser (#311)
Added a missing "()" in the NAL parser script
2016-12-06 09:36:17 -08:00
Cameron Gutman
d31be3d64e Prevent the help activity from reloading across config changes 2016-11-24 11:25:08 -08:00
Cameron Gutman
0704f2aaf6 Set noHistory for the Game activity 2016-11-24 11:23:18 -08:00
Cameron Gutman
832e52ac74 Reload PcView and AppView if the locale changes 2016-11-24 11:22:06 -08:00
Cameron Gutman
f5444551b2 Avoid looping when the thread is trying to be interrupted 2016-11-22 23:20:00 -08:00
Cameron Gutman
3143797b55 Fix transparent background when switching apps in multi-window 2016-11-22 23:18:55 -08:00
Cameron Gutman
cc9b1aeaab Use a MediaCodecInfo object to describe a codec rather than a codec name 2016-11-20 17:56:53 -08:00
Cameron Gutman
3d177e97e4 Add support for displaying the rendered frame times 2016-11-17 23:34:56 -08:00
Cameron Gutman
6c3aaedc83 Version 4.8.1 2016-11-13 20:30:26 -08:00
Cameron Gutman
bf84ebef6d Fix help launch crash with MxPlayer acting as default browser 2016-11-13 20:29:34 -08:00
Cameron Gutman
8991b29329 Run at maximum decoder operating rate on M 2016-11-13 20:26:58 -08:00
Cameron Gutman
fa84575be5 Increment version code 2016-11-11 14:04:28 -08:00
Cameron Gutman
0432d5725b Properly handle Xbox One S controllers with updated firmware 2016-11-11 14:03:20 -08:00
Cameron Gutman
8e7b144339 Increment version code 2016-11-10 19:58:14 -08:00
Cameron Gutman
fc629db653 Improve shortcut handling when the list is full 2016-11-10 19:45:17 -08:00
Cameron Gutman
d5863e1bef Only try twice before initially reporting a PC as offline 2016-11-09 21:08:27 -08:00
Cameron Gutman
c2c3a6b37c Increment version code 2016-11-07 19:00:39 -08:00
Cameron Gutman
e701699dea Improve launcher shortcut backstack and stop leaking a ServiceConnection 2016-11-07 18:58:59 -08:00
Cameron Gutman
17179bd027 Update version to 4.8 2016-11-06 01:08:56 -07:00
Cameron Gutman
b2f210700d Update common with mouse move trick 2016-11-06 01:06:07 -07:00
Cameron Gutman
52678cfe35 Move the mouse cursor 1 pixel each direction when connecting to force the screen to wake up 2016-11-06 01:04:50 -07:00
Cameron Gutman
f0e85c4c53 Fix some additional launcher shortcut issues 2016-11-06 00:43:18 -07:00
Cameron Gutman
92f8425ace Better handle offline PCs in launcher shortcuts 2016-11-06 00:24:04 -07:00
Cameron Gutman
6ad001e8be Update help viewer for Amazon devices 2016-11-06 00:01:37 -07:00
Cameron Gutman
b6e4d5528b Fix and enable launcher shortcuts on Android 7.1 2016-11-05 21:29:59 -07:00
Cameron Gutman
0f0b83badc Properly invalidate pairing state after pairing 2016-11-05 20:20:09 -07:00
Cameron Gutman
453fbb5f58 Use scancode mapping for DS4 for consistent mappings across devices 2016-11-05 20:08:42 -07:00
Cameron Gutman
e7dc3a4c11 Timeout polling results after 30 seconds 2016-11-05 19:56:54 -07:00
Cameron Gutman
d68b2382cf Integrate help buttons into dialogs and the PcView 2016-11-05 19:51:43 -07:00
Cameron Gutman
1b5330323c Fix root mouse capture on Nougat 2016-11-04 00:14:05 -07:00
Cameron Gutman
8aba4888e1 Fix being unable to press the same key down on different gamepads at the same time 2016-10-30 15:26:52 -07:00
Cameron Gutman
1c3b9a3859 Fix race condition in USB driver initialization and cleanup 2016-10-27 18:09:33 -07:00
Cameron Gutman
e8f04f5a3b Cleanup ShortcutHelper 2016-10-26 12:28:29 -07:00
Cameron Gutman
56b814e877 Bump versioncode 2016-10-21 16:22:07 -07:00
Cameron Gutman
628ccd39d6 Fix default context getting picked up falsely during gamepad removal 2016-10-21 16:19:05 -07:00
Cameron Gutman
59db3f9b62 Fix Xbox button behavior quirks on Xbox One S controller 2016-10-21 15:57:00 -07:00
Cameron Gutman
416f922b56 Fix triggers stuck at 50% after controller reconnect 2016-10-21 14:54:21 -07:00
Cameron Gutman
b52a86e6cc Fix app grid isRunning icon not displaying 2016-10-21 14:04:31 -07:00
Cameron Gutman
e523b5069e Version 4.7.3 2016-10-21 13:48:03 -07:00
Cameron Gutman
e8ae8d9807 Manually set pairing state after pairing 2016-10-21 13:47:03 -07:00
Cameron Gutman
64e56a861d Ignore case when sorting apps and PCs 2016-10-21 13:44:59 -07:00
Cameron Gutman
c1bcd09c9b Disable launcher shortcuts pending further work 2016-10-21 12:52:37 -07:00
Cameron Gutman
574258804f Update to AS 2.2.2 and SDK 25 2016-10-21 12:50:04 -07:00
Cameron Gutman
21ea3d8a2b Fix 3rd party Xbox controller d-pads 2016-10-21 12:38:55 -07:00
Cameron Gutman
6de4288a85 Fix running app state on GFE 3.1 2016-10-21 12:28:15 -07:00
Cameron Gutman
61f89a2d4c Remove isRunning property from apps (since it's gone in GFE 3.1) 2016-10-21 12:16:43 -07:00
Cameron Gutman
a107b5e652 Add launcher shortcuts and fix duplicate pairing error 2016-10-20 13:09:24 -07:00
Cameron Gutman
ba398e4073 Handle pairing while a pairing attempt is pending 2016-10-20 00:06:41 -07:00
Cameron Gutman
b02db2c182 Fix JNI build warnings with modern NDKs 2016-10-19 20:47:23 -07:00
Cameron Gutman
f8a04cda7a Version 4.7.2 2016-10-05 18:53:36 -07:00
Cameron Gutman
226e8edefc Update common jar to 55f0114 2016-10-05 18:49:57 -07:00
Cameron Gutman
a14a4a8d60 Add support for GFE 3.0.7 2016-10-05 18:45:19 -07:00
Cameron Gutman
9b90b30a1f Update build tools and version code 2016-09-28 22:31:55 -07:00
Cameron Gutman
2ed245b25a Add some extra text for GFE 3.0, since GameStream isn't on by default 2016-09-28 22:30:05 -07:00
Cameron Gutman
4b769839d0 Update common jar to 3c56730 2016-09-24 20:57:19 -07:00
Cameron Gutman
9caf3b37ac Fix IDR frame requests on Gen 3 servers 2016-09-24 13:21:29 -07:00
Cameron Gutman
e6965605c9 Consolidate string trimming into getXmlString() 2016-09-20 14:37:51 -07:00
Cameron Gutman
5b355a3e73 Fail quitApp() if the session isn't ours 2016-09-20 14:35:06 -07:00
Cameron Gutman
239dd1d5a1 Only display box art progress bar if a network load is required 2016-09-20 12:11:06 -07:00
Cameron Gutman
37509cce9b Update common jar to a76df84 2016-09-20 11:29:13 -07:00
Cameron Gutman
227c71549b Migrate project and NDK build to Android Studio 2.2 2016-09-20 11:26:56 -07:00
Cameron Gutman
92d534a9c3 Merge pull request #15 from nanotech/f/idr-obo
Fix off-by-one when looking for an I-frame NAL
2016-09-11 10:02:55 -07:00
NanoTech
2d08f568e9 Fix off-by-one when looking for an I-frame NAL
Between finding the NAL and checking its type,
reassembleFrame is called and overwrites the
cachedSpecialDesc with the data and NAL type
from the previous NAL. If the only IDR NAL in the
packet is the last NAL in the packet, it gets
missed and the depacketizer is stuck waiting
for it.
2016-09-10 00:52:56 -06:00
Cameron Gutman
a10d8334f3 Update Gradle 2016-08-26 22:02:51 -07:00
Nyaran
f88c9904fb Added support for Spanish language 2016-08-14 11:08:30 +02:00
Cameron Gutman
0fc61e52dd Fix Lint error in translation 2016-08-13 19:24:49 -07:00
Cameron Gutman
5e44c33bb6 Version 4.7 2016-08-13 19:22:29 -07:00
Cameron Gutman
df3655e958 Add support for Xbox One S controller over USB 2016-08-13 19:21:49 -07:00
Cameron Gutman
fe43e13145 Set larger dimensions for vector drawables so the generated PNGs are larger 2016-08-13 19:18:42 -07:00
Cameron Gutman
acd3aad8d9 Add support for mouse emulation with a gamepad 2016-08-13 18:52:39 -07:00
Cameron Gutman
811b4b4f22 Better center overlays on PC view 2016-08-13 17:48:30 -07:00
Cameron Gutman
7db3b9f401 Use Material icons 2016-08-13 16:45:42 -07:00
Cameron Gutman
a5a099cf43 Update common jar with 4K fix 2016-08-13 14:51:07 -07:00
Cameron Gutman
ba605643bb Switch to indeterminate progress bars 2016-08-13 14:42:03 -07:00
Cameron Gutman
a9f7b1aeab Merge branch 'android' 2016-08-13 13:39:36 -07:00
Cameron Gutman
4f53cfcb20 Only use 4K on GFE 3.x 2016-08-13 13:24:31 -07:00
Cameron Gutman
96e98c1abb Update translations 2016-08-13 12:32:49 -07:00
Cameron Gutman
5de6f6ae2b Fix build with Dutch translation 2016-08-13 12:29:08 -07:00
Cameron Gutman
0685722773 Merge branch 'master' of github.com:moonlight-stream/moonlight-android 2016-08-13 12:20:54 -07:00
Cameron Gutman
29df3b2859 Merge branch 'master' of https://github.com/halluci/moonlight-android 2016-08-13 12:14:37 -07:00
DragonSpirit
fc6f859ced Russian Translation Added (#146) 2016-08-13 12:10:02 -07:00
Subject
6b21a5416f Dutch Translation Added (#118)
* Halfway through string translation

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

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

* Spelling fixes. Added ndk.dir
2016-08-13 12:07:20 -07:00
Cameron Gutman
757075b16a Add support for Xbox One S controller connected via Bluetooth 2016-08-13 12:01:52 -07:00
Cameron Gutman
e8903c4d48 Update build tools to 24.0.1 2016-08-13 11:45:38 -07:00
halluci
98262d16ee Japanese translation added:updated 2016-07-17 22:01:49 +09:00
halluci
339506cf10 Japanese translation added 2016-07-17 17:22:30 +09:00
Cameron Gutman
63bd5df09b Prefer Evdev over N native capture since Evdev can capture over the system UI 2016-07-13 23:12:29 -07:00
Cameron Gutman
32af2d0831 Increment version code 2016-06-26 14:05:38 -07:00
Cameron Gutman
242b03d4b5 Add gradle.properties for Dex In-Process 2016-06-20 21:33:39 -07:00
Cameron Gutman
87a62666ac Prefer Shield capture provider over Android N 2016-06-20 20:43:59 -07:00
Cameron Gutman
2dcf5486da Revert "Display the running app first on the app grid"
This reverts commit 36f8cc02cb.
2016-06-20 20:30:47 -07:00
Cameron Gutman
60d3d8b3ae Version to 4.6 2016-06-18 15:17:15 -07:00
Cameron Gutman
e9141d65fe Improve reliability of missing root detection 2016-06-18 14:54:53 -07:00
Cameron Gutman
aae591daec Improve multi-window experience on N 2016-06-18 14:52:20 -07:00
Cameron Gutman
a5ca8a7472 Add a hack to avoid crashing when the app window divider is dragged off of the screen on N multi-window 2016-06-18 14:40:42 -07:00
Cameron Gutman
36f8cc02cb Display the running app first on the app grid 2016-06-18 13:38:37 -07:00
Cameron Gutman
55b9645651 Fix minor Lint issues 2016-06-18 12:38:43 -07:00
Cameron Gutman
d30ecbed5b Update gradle 2016-06-18 11:40:55 -07:00
Cameron Gutman
0bbd27f04c Update common jar 2016-06-18 11:37:00 -07:00
Cameron Gutman
ffd70986b3 Improve mDNS for multi-homed systems 2016-06-18 11:36:36 -07:00
Cameron Gutman
3c53fb7403 Update target SDK to 24 2016-06-18 11:19:04 -07:00
Cameron Gutman
7a81950819 Enable sustained performance mode on N+ when streaming 2016-06-18 11:17:34 -07:00
Cameron Gutman
74f212c702 Add Android N mouse capture support 2016-06-18 11:15:53 -07:00
Cameron Gutman
36be943854 Add support for more Xbox controller models 2016-06-13 22:28:48 -05:00
Cameron Gutman
26a4fc75a5 Add handling for the ADT-1 controller 2016-06-13 21:28:54 -05:00
Cameron Gutman
a5ec5fc265 Select the optimal display mode before streaming 2016-06-13 21:23:00 -05:00
Cameron Gutman
541ac44be4 Add an unified input capture interface 2016-06-13 20:33:43 -05:00
Cameron Gutman
117b555fcd Fix wiki link 2016-05-30 12:22:54 -05:00
Cameron Gutman
a10cd04441 Clarify wording in H.265 settings 2016-05-29 16:39:20 -05:00
Cameron Gutman
b5e89e47b6 Improve mDNS for multi-homed systems 2016-05-29 16:04:55 -05:00
Cameron Gutman
53dccbde2a Repeat key down events are needed for proper key repeating 2016-05-29 15:52:18 -05:00
Cameron Gutman
439afd15fa Cancel the pending IDR frame request if we got a spurious IDR frame after a stream discontinuity 2016-05-27 12:38:27 -05:00
Cameron Gutman
8d2bfecb10 Cancel the pending IDR frame request if we got a spurious IDR frame after a stream discontinuity 2016-05-27 12:38:09 -05:00
Cameron Gutman
7d15c34ed2 Fix debug message 2016-05-22 14:09:04 -05:00
Cameron Gutman
56625dfe4b Bump version to 4.5.10 2016-05-21 18:28:02 -05:00
Cameron Gutman
2eab5a3b7b Update the ENet submodule to include the MTU fix for LTE streaming 2016-05-21 18:09:40 -05:00
Cameron Gutman
f9e811862a Bump version to 4.5.9 2016-05-19 22:39:50 -04:00
Cameron Gutman
25ccc3d0e1 Fix for Xiaomi gamepad mapping 2016-05-19 22:31:14 -04:00
Cameron Gutman
8853bf0670 Bump version to 4.5.8 2016-05-07 21:25:11 -04:00
Cameron Gutman
71fa3a824b Update gradle 2016-05-07 21:20:05 -04:00
Cameron Gutman
56fd50834c Update common jar with the RTP queue changes 2016-05-07 21:19:54 -04:00
Cameron Gutman
48ba812cf6 When combining analog inputs, use the one with the highest magnitude 2016-05-07 21:19:02 -04:00
Cameron Gutman
019dc6d45f Display a warning at stream start if root access is unavailable 2016-05-07 20:59:17 -04:00
Cameron Gutman
4ef1b8dc4c Fix debug message 2016-05-07 20:55:41 -04:00
Cameron Gutman
cbcb784a79 Blacklist Tegra X1's HEVC decoder until the correct SPS fixups are in place 2016-05-07 20:53:45 -04:00
Cameron Gutman
c0d64058fd Return the oldest packet instead of removing it upon RTP queue constraint violation 2016-05-07 20:26:57 -04:00
Cameron Gutman
3c11ff63a7 Return the oldest packet instead of removing it upon RTP queue constraint violation 2016-05-07 20:26:44 -04:00
Cameron Gutman
39fa0258ad Force the Archos Gamepad 2's controller buttons as controller 0 2016-04-23 22:23:43 -04:00
Cameron Gutman
d0dd5bfa8c Combine all controllers with the same controller number before sending controller input 2016-04-23 22:23:01 -04:00
Cameron Gutman
b948c47618 Increment patch level again 2016-04-22 00:15:21 -04:00
Cameron Gutman
18cae8ac53 Use common jar from the android branch (da297b5a89c2b645573f231af3e47752f27fbc79) to fix API 19 issues 2016-04-21 13:33:08 -04:00
Cameron Gutman
537a50bee5 Revert "Some Java crypto providers don't allow IvParameterSpec objects for GCM ciphers, so use GCMParameterSpec instead"
This reverts commit b37d46fae9b89f5435990c75ce540d64efe374f7.
2016-04-21 13:28:55 -04:00
Cameron Gutman
1a58b228a0 Revert "Fallback to IvParameterSpec if GCMParameterSpec is not available"
This reverts commit 8324c2c772a191a726e2027b1487d872897bf184.
2016-04-21 13:28:31 -04:00
Cameron Gutman
0576231dfc Update patch level to 4.5.7.1 2016-04-20 13:52:53 -04:00
Cameron Gutman
6ad35a83dd Update common jar with fix for < API 19 2016-04-20 13:42:57 -04:00
Cameron Gutman
33d4dfc745 Revert "Prevent the small-mode default from changing between portrait and landscape orientations"
This reverts commit 7c1eb80d62.
2016-04-20 13:34:24 -04:00
Cameron Gutman
248a135f86 Fallback to IvParameterSpec if GCMParameterSpec is not available 2016-04-20 13:31:56 -04:00
Cameron Gutman
f3bf63a668 Increment app version 2016-04-19 20:49:40 -04:00
Cameron Gutman
2dbb7395a4 Restart the app view activity when configuration changes are made that could require the grid to be resized. This is much simpler than handling all of the fine edge cases here. 2016-04-19 20:38:05 -04:00
Cameron Gutman
7c1eb80d62 Prevent the small-mode default from changing between portrait and landscape orientations 2016-04-19 20:36:04 -04:00
Cameron Gutman
f2bf093691 Update Gradle 2016-04-19 19:51:00 -04:00
Cameron Gutman
2f002bfa4a Fix being stuck in small-icon mode after resizing to minimum size on Android N 2016-04-19 19:36:10 -04:00
Cameron Gutman
4a19038d54 Update common jar to fix crashes in jnienet 2016-04-19 19:18:13 -04:00
Cameron Gutman
15fb3dd92c Fix mouse scaling to scale by stream view size rather than screen size for better behavior on N and in general 2016-04-19 19:13:57 -04:00
Cameron Gutman
e0982d3961 Fix video stream aspect ratio scaling in multi-window mode on Android N 2016-04-19 18:40:45 -04:00
Cameron Gutman
246fb69050 Fix dangerous connection teardown ordering leading to native crashes in jnienet 2016-04-04 01:18:14 -04:00
Cameron Gutman
a907dd0084 Remove unused (and unsafe) function on the ConnectionStatusListener 2016-04-04 01:16:33 -04:00
Cameron Gutman
fe58361724 Some Java crypto providers don't allow IvParameterSpec objects for GCM ciphers, so use GCMParameterSpec instead 2016-03-30 01:26:14 -04:00
Cameron Gutman
7fb2f15f54 Re-release of 4.5.6 with fixed Gen 4 streaming 2016-03-29 23:37:03 -04:00
Cameron Gutman
f93dbb4116 Update common jar again to fix streaming on Gen 4 and earlier 2016-03-29 23:34:31 -04:00
Cameron Gutman
a0f93a2dc3 Fix audio stream name on Gen 4 and below 2016-03-29 22:03:32 -04:00
Cameron Gutman
bc34fe3a9f Increment version to 4.5.6 2016-03-29 20:35:54 -04:00
Cameron Gutman
bbe49491c1 Update common jar to support GFE 2.11.2.46+ 2016-03-29 20:17:35 -04:00
Cameron Gutman
d5ccb80f26 Update to new Gradle for Android Studio 2.1 2016-03-29 20:15:45 -04:00
Cameron Gutman
82390ec9b9 Fix input encryption IV after controller packets are sent 2016-03-29 19:55:31 -04:00
Cameron Gutman
34ef95926e Fix RTSP handshake on Gen 7 servers 2016-03-29 18:42:15 -04:00
Cameron Gutman
faa0cba39d Fix input encryption on Gen 7 servers 2016-03-29 18:41:57 -04:00
Cameron Gutman
8b395bb29f Pairing support for Gen 7 servers 2016-03-28 18:38:11 -04:00
Cameron Gutman
50fd15379a Fix JNI compilation warnings 2016-03-10 15:28:42 -08:00
Cameron Gutman
ed479f1155 Increment version to 4.5.5 2016-03-08 13:10:04 -08:00
Cameron Gutman
04db9ba714 Update common to fix RTSP handshake timeouts with ENet 2016-03-08 13:07:33 -08:00
Cameron Gutman
31d7f237eb Fix RTSP payload timeout being too aggressive 2016-03-08 13:05:55 -08:00
Cameron Gutman
6a973e3248 Update version code for 4.5.4 r2 2016-03-07 15:01:58 -08:00
Cameron Gutman
96d9e4977b Update to ENet API to support IPv6 2016-03-07 14:37:01 -08:00
Cameron Gutman
ef8c49f135 ENet JNI API update 2016-03-07 14:35:52 -08:00
Cameron Gutman
5a3897f22a Update common jar to fix some ENet crashes 2016-03-07 13:16:55 -08:00
Cameron Gutman
ceef00b79a Fail writePacket if enet_peer_send returns -1 2016-03-07 12:54:06 -08:00
Cameron Gutman
a8c460e715 Close the ENet connection only after threads that might be using it have been killed 2016-03-07 12:51:48 -08:00
Cameron Gutman
94ee24ea11 Update to 4.5.4 2016-03-06 21:52:54 -08:00
Cameron Gutman
1a201f2e94 Update gradle to latest beta 2016-03-06 21:51:48 -08:00
Cameron Gutman
e0c6d41d4b Update libs again to fix duplicate files 2016-03-06 21:51:01 -08:00
Cameron Gutman
d75e42e23d Fix crashing on connecting to Gen 4 after running a Gen 5 stream 2016-03-06 21:29:06 -08:00
Cameron Gutman
44a0ae86d2 Working ENet with new common jar and modified ENet library 2016-03-06 15:55:33 -08:00
Cameron Gutman
b191425112 Finish packet reading for RTSP and control streams 2016-03-06 15:51:15 -08:00
Cameron Gutman
c06a4ab76d Implement RTSP over ENet 2016-03-05 19:10:22 -06:00
Cameron Gutman
b3042312f6 Supply the max packet size to EnetConnection.readPacket() 2016-03-05 18:32:50 -06:00
Cameron Gutman
5fd105c9a9 Implement ENet for control and input streams 2016-03-05 17:56:42 -06:00
Cameron Gutman
06822ad385 Add JNI library for ENet 2016-03-05 17:48:10 -06:00
Cameron Gutman
3be52280ba Update common to disable dynamic resolution switching 2016-02-28 14:52:07 -05:00
Cameron Gutman
306c2d143b Disable resolution switching on Gen 5 servers 2016-02-28 14:12:34 -05:00
Cameron Gutman
5142f978cf Fixed polling resuming in the background in some cases 2016-02-23 23:47:49 -05:00
Cameron Gutman
667ffd4dfd Bump to version 4.5.3.2 2016-02-23 16:33:57 -05:00
Cameron Gutman
17626f1853 Update common to crash in mDNS discovery agent 2016-02-23 16:33:38 -05:00
Cameron Gutman
a71a3e22e6 Rewrite MdnsDiscoveryAgent to treat the singleton JmmDNS object safely 2016-02-23 16:18:59 -05:00
Cameron Gutman
da4fab2f3e Stop using resolver outside of the timer callback 2016-02-23 13:49:35 -05:00
Cameron Gutman
1db84efb68 Revert "Synchronize the mDNS events and timer with the MdnsDiscoveryAgent class instead of the instance object as a workaround for JmDNS issue #49"
This reverts commit d92ad050aa4482cdaaf8f7345222da26c31d84f6.
2016-02-23 10:54:03 -05:00
Cameron Gutman
c1c3af3c66 Synchronize the mDNS events and timer with the MdnsDiscoveryAgent class instead of the instance object as a workaround for JmDNS issue #49 2016-02-23 01:50:21 -05:00
Cameron Gutman
5c79567a2c Bump version to 4.5.3.1 2016-02-20 20:11:26 -05:00
Cameron Gutman
0f5fd9af62 Update common to fix mDNS running passively in the background 2016-02-20 20:11:00 -05:00
Cameron Gutman
99643537d1 Only disable missing translation Lint errors rather than ignoring all Lint errors 2016-02-20 20:10:14 -05:00
Cameron Gutman
4622b9f202 Close the jmDNS object when we're done resolving so it stops listening on the network 2016-02-20 17:11:55 -05:00
Cameron Gutman
47650386e0 Bump version code and update common to fix video issue on H265 2016-02-19 11:35:52 -05:00
Cameron Gutman
47ea158c4c Fix random broken video on H265 2016-02-19 11:27:37 -05:00
Cameron Gutman
aa3fc34646 Update version code and lint options for building releases with Gradle 2.0 2016-02-19 04:11:03 -05:00
Cameron Gutman
92f5f1ac71 Bump to 4.5.3 with support for GFE 2.10.2 2016-02-19 03:58:38 -05:00
Cameron Gutman
d9cb5eacf8 Add support for Generation 5 servers (GFE 2.10.2+) 2016-02-19 03:41:03 -05:00
Cameron Gutman
5718c47be7 Fix the bug causing the 4th controller to be unrecognized 2016-02-19 01:55:09 -05:00
Cameron Gutman
eb739f73c7 Update Gradle and Gradle Wrapper for Android Studio 2.0 2016-02-06 16:44:00 -05:00
Cameron Gutman
20a646106b Fix duplicate file exceptions with newer versions of Gradle 2016-02-06 16:43:11 -05:00
Cameron Gutman
0dc14517cd Bump version to 4.5.2 2016-01-30 05:16:29 -05:00
Cameron Gutman
04713c007b Remove some hacks for Android TV 2016-01-30 05:10:47 -05:00
Cameron Gutman
1cac7660b8 Fix a null pointer exception reported by a user 2016-01-30 04:55:17 -05:00
Cameron Gutman
edb286f9af Hide the mouse on the main thread just to be safe 2016-01-30 04:27:14 -05:00
Cameron Gutman
fb15ff99ca Add support for the NVIDIA relative mouse extensions for Shield devices 2016-01-30 04:21:20 -05:00
Cameron Gutman
a455e75e37 Fix recognition of mouse events on Shield Portable 2016-01-30 04:15:09 -05:00
Cameron Gutman
2b452e51f9 Bump version to 4.5.1 2016-01-28 13:02:46 -05:00
Cameron Gutman
9d2b6f8854 Make nextDeviceId non-static since the lifetime of ControllerHandler is also just the life of the connection 2016-01-28 13:02:30 -05:00
Cameron Gutman
3be10a1b59 Update preference string to include Xbox 360 2016-01-28 12:55:19 -05:00
Cameron Gutman
01950c25a8 Only claim Xbox 360 controllers if the kernel hasn't already 2016-01-28 12:35:16 -05:00
Cameron Gutman
7ad1ebd0e8 Fix Xbox 360 driver 2016-01-28 12:07:11 -05:00
Cameron Gutman
ee01a8b5a0 Turn the XB360 controller LED on at init 2016-01-27 14:00:14 -05:00
Cameron Gutman
23c54f6813 Add support for wired Xbox 360 controllers (pending testing) 2016-01-27 13:45:04 -05:00
Cameron Gutman
ceef4510fb Fix infinite app list loading spinner if the app list is actually empty 2016-01-24 02:51:06 -05:00
Cameron Gutman
042a6b943e Bump version to 4.5 2016-01-20 02:18:22 -05:00
Cameron Gutman
e114b73654 Revert "Fix margins around analog sticks"
This reverts commit 5d84f8af43.
2016-01-20 01:35:30 -05:00
Cameron Gutman
da0a505978 Shrink the text size in the buttons so the start button text fits on the Nexus 9 2016-01-20 01:30:48 -05:00
Cameron Gutman
cb6d4a385c Leave a margin around the d-pad so the selection rectangle doesn't draw over the control itself 2016-01-20 01:12:53 -05:00
Cameron Gutman
2806aee0fc Fix drawing and placement of face buttons 2016-01-20 01:04:06 -05:00
Cameron Gutman
52736f5162 Increase the time allowed for a double click to activate the stick button 2016-01-20 00:28:33 -05:00
Cameron Gutman
6d45ad7fe8 Improve precision of joystick inputs by lifting the deadzone after 150 ms. This way it prevents false inputs when activation the stick buttons but allows for precise movements after confirming that the touch is intended. 2016-01-20 00:28:11 -05:00
Cameron Gutman
2fc53644bc Use a uniform stroke width based on screen size in pixels 2016-01-19 20:26:46 -05:00
Cameron Gutman
b33eaec493 Temporarily disable the config dialog and just map a tap of a controller element to move 2016-01-19 19:58:11 -05:00
Cameron Gutman
63d6f3ac78 Fix snapping into the deadzone when using analog sticks 2016-01-19 19:54:52 -05:00
Cameron Gutman
fd4caac013 Fix erratic joystick movement 2016-01-19 19:44:33 -05:00
Cameron Gutman
ada875cdb0 Highlight the controls red when in configuration mode 2016-01-19 18:52:51 -05:00
Cameron Gutman
49ddfa573d Ignore inputs when the on-screen controls are in configuration mode 2016-01-19 18:31:00 -05:00
Cameron Gutman
b58ac367ee Increase the size of the virtual controller settings button 2016-01-19 18:24:10 -05:00
Cameron Gutman
cf62b4ed95 Select is slightly too long for the button so rename it to Backc 2016-01-19 18:13:16 -05:00
Cameron Gutman
b05c62e141 Fix outside of each d-pad button being cut off by the end of the canvas 2016-01-19 18:01:30 -05:00
Cameron Gutman
095556106c Fix highlighting of selected controller element during configuration 2016-01-19 17:45:14 -05:00
Cameron Gutman
5cdd72a45c Disable printing controller output 2016-01-19 17:35:17 -05:00
Cameron Gutman
5d84f8af43 Fix margins around analog sticks 2016-01-19 17:34:52 -05:00
Cameron Gutman
d9483d9214 Show a nicer configuration toast 2016-01-19 17:30:49 -05:00
Cameron Gutman
250475830f Draw the highlight border after the element so it doesn't get drawn over 2016-01-19 17:08:00 -05:00
Cameron Gutman
b8a0a823e0 Raise d-pad and buttons slightly further from the analog sticks 2016-01-19 16:33:00 -05:00
Cameron Gutman
6a54d669a3 Fix capitalization of preference group 2016-01-19 16:31:06 -05:00
Cameron Gutman
62559c4e66 Merge branch 'master' of https://github.com/hop3l3ss/limelight-android 2016-01-19 16:23:56 -05:00
Cameron Gutman
e04ecaaf7a Rework the face buttons to match the d-pad 2016-01-19 16:23:40 -05:00
Karim
fa4706c95f fix on screen controls category typo 2016-01-09 12:56:39 +01:00
Karim
7067c0e02e show onscreen controls settings only on touchscreen devices 2016-01-09 12:49:12 +01:00
Cameron Gutman
d7d90e8e49 Encode the surround sound information in the launch request parameters 2016-01-08 01:01:58 -06:00
Cameron Gutman
cc71ce6180 Fix crash in XB1 controller driver on Fire HD 6 after controller removal 2016-01-07 22:52:17 -06:00
Cameron Gutman
f409a3583c Fix direct submit behavior in decoders since the addition of HEVC 2016-01-07 18:51:02 -06:00
Cameron Gutman
ac7504e017 Bump version to 4.0.4 2016-01-07 16:08:08 -06:00
Cameron Gutman
345bd3f7c1 Hide on-screen controls preference until bugs are resolved 2016-01-07 16:01:33 -06:00
Cameron Gutman
2e2960ec69 Disable on-screen controls by default 2016-01-07 12:57:59 -06:00
Cameron Gutman
e93b103d1e Fix ConcurrentModificationException in virtual controller code 2016-01-07 12:57:37 -06:00
Cameron Gutman
22977a4c5b Use a socket for communication from EvdevReader to Moonlight rather than stdin/stdout. On some devices, fwrite(stdout) hangs for unknown reasons. 2016-01-07 12:49:30 -06:00
Cameron Gutman
7da5d5322b Cache Paint objects instead of allocation in draw method 2016-01-07 02:23:34 -06:00
Cameron Gutman
49e2c40ba4 Add LB and RB buttons to virtual controller 2016-01-07 01:06:22 -06:00
Cameron Gutman
8041a004c2 Remove text from d-pad as it tends to get in the way of visuals on screen 2016-01-07 01:00:15 -06:00
Cameron Gutman
db62d78e04 On-screen controls: Fix functionality of Select button and rename Play to Start 2016-01-07 00:45:30 -06:00
Cameron Gutman
bd79318b1e Cleanup new virtual controller code 2016-01-07 00:30:45 -06:00
Cameron Gutman
2736bd9165 Android Studio auto-reformat of new virtual controller code 2016-01-07 00:24:39 -06:00
Cameron Gutman
b6bd48584f Refactor to match other preference conventions 2016-01-07 00:20:46 -06:00
Cameron Gutman
7b4f3c975a Fix on-screen controls not showing up on 16:9 devices 2016-01-07 00:15:33 -06:00
Cameron Gutman
b165fadc55 Remove unused file 2016-01-07 00:14:16 -06:00
Cameron Gutman
274e0d0557 Merge branch 'master' into virtualcontroller_master
Conflicts:
	app/app.iml
	app/build.gradle
	app/libs/limelight-common.jar
	app/src/main/java/com/limelight/Game.java
	app/src/main/java/com/limelight/binding/input/ControllerHandler.java
	app/src/main/java/com/limelight/binding/video/MediaCodecHelper.java
	app/src/main/java/com/limelight/computers/ComputerDatabaseManager.java
	app/src/main/java/com/limelight/preferences/PreferenceConfiguration.java
	app/src/main/jni/evdev_reader/evdev_reader.c
	app/src/main/res/xml/preferences.xml
	limelight-android.iml
	limelight_vc.iml
	moonlight-android.iml
2016-01-07 00:01:03 -06:00
Cameron Gutman
7594e51a18 Fix SQL injection vulnerability and crashes when an apostrophe is present in a computer name 2016-01-06 15:17:30 -06:00
Cameron Gutman
bf22819b53 Update common with timeouts for RTSP handshake 2016-01-06 13:08:18 -06:00
Cameron Gutman
3dea4b15e0 Fix support for kernels that output 24-byte input events 2016-01-06 13:05:51 -06:00
Cameron Gutman
5836b3292b Only grab event devices 2016-01-06 12:36:09 -06:00
Cameron Gutman
a8fd49a234 Fix possible segmentation fault or memory corruption if EVIOCGRAB fails and the cleanup is executed before the device entry is inserted into the list 2016-01-06 12:35:45 -06:00
Cameron Gutman
006ad72eb2 Check the stdin poll() return value before reading 2016-01-05 19:53:23 -06:00
Cameron Gutman
dc254e1ee5 Some S6s have back buttons on the device called sec_touchkey so also ignore back presses on those too 2016-01-05 00:27:19 -06:00
Cameron Gutman
4d420b29cb Also timeout RTSP if no response is received on an established connection for 10 seconds 2016-01-04 23:55:58 -06:00
Cameron Gutman
0b9e7aa05b Standardize connection timeouts to 10 seconds 2016-01-04 23:54:25 -06:00
Cameron Gutman
b0d31a4d35 Update version for 4.0.3 r2 2016-01-04 09:30:56 -06:00
Cameron Gutman
24155feea4 Update common with proper HEVC fix for r2 of 4.0.3 2016-01-04 09:29:22 -06:00
Cameron Gutman
8c663cc84a Replace the HEVC detection hack with a proper solution based on examining the RTSP DESCRIBE response 2016-01-04 07:39:07 -06:00
Cameron Gutman
db0a4e35c6 Bump to 4.0.3 2016-01-03 16:35:21 -06:00
Cameron Gutman
68ef98d346 Update common to fix broken mobile 900-series GPU detection for H.265 2016-01-03 16:29:02 -06:00
Cameron Gutman
8d1417c636 Improve HEVC check to add Titan X support and more importantly stop requesting H.265 on mobile GPUs that may not support it 2016-01-03 16:01:28 -06:00
Karim
f23bb9fac1 improve virtual controller:
* add digital 8-Way pad
  * add on screen element size and position configuration
  * begin with cleanup
2016-01-03 11:12:43 +01:00
Cameron Gutman
d20dde0b6d Print a message when the EvdevReader starts 2016-01-02 19:42:40 -06:00
Cameron Gutman
f76b30d109 Fix exceptions in onStop when the connection is aborted due to lack of H.264 support 2016-01-02 18:28:01 -06:00
Cameron Gutman
ee1a047cde Remove several decoders from the whitelist based on some user-reported issues 2016-01-02 18:16:12 -06:00
Cameron Gutman
b91ab53219 Connection reuse is broken in GFE's server (requests always get reissued on a new connection) so just turn it off 2016-01-02 17:34:29 -06:00
Cameron Gutman
6eeb7ae5b2 Add UUID on serverinfo and add a TODO for HTTP serverinfo 2016-01-02 16:50:22 -06:00
Cameron Gutman
0436179020 Use UUID string in pairing and switch to HTTP for pairing (like Shield Hub) 2016-01-02 16:47:10 -06:00
Cameron Gutman
c92cae51c8 Add UUID to URL queries (like recent Shield Hub version do) 2016-01-02 16:30:24 -06:00
Cameron Gutman
57da68c0e2 Remove uniqueId parameter from NvHTTP.getServerInfo 2016-01-02 16:28:58 -06:00
hop3l3ss
4c533fedfd Merge pull request #1 from ruqqq/master
Merge https://github.com/limelight-stream/limelight-android
2015-12-31 11:44:42 +01:00
Faruq Rasid
f8ab7b8e13 Merge https://github.com/limelight-stream/limelight-android 2015-12-31 10:14:30 +08:00
Cameron Gutman
46c5eaf0e1 Fix a user-reported crash in USB code 2015-12-23 14:03:55 -06:00
Cameron Gutman
e7e73aa1d2 Bump version to 4.0.2 2015-12-21 15:28:38 -08:00
Cameron Gutman
394221f3df Use file locks to synchronize stdout instead of a pthread mutex 2015-12-21 15:07:37 -08:00
Cameron Gutman
7d2647f830 Set the shutdown flag before killing the reader 2015-12-21 15:04:22 -08:00
Cameron Gutman
563c90a8c4 Build native binaries for all modern ABIs 2015-12-21 15:03:14 -08:00
Cameron Gutman
0e0352fdd6 Disable HEVC on NVIDIA hardware until the 16 frame buffering problem can be solved 2015-12-21 15:02:50 -08:00
Cameron Gutman
d6a8db97d8 Rewrite root input capturing to be compatible with Android 6.0 (and be much more secure in general) 2015-12-19 23:55:34 -08:00
Cameron Gutman
05f8fa21de Update version 2015-12-17 03:35:51 -08:00
Cameron Gutman
ab8779086b Fix broken video on Galaxy S5 and Note III 2015-12-17 03:35:39 -08:00
Cameron Gutman
ed8305b199 Revert "Blacklist the whole device from HEVC decoding if Qualcomm's HEVC hybrid decoder is found"
This reverts commit 3c2dd88fd3.
2015-12-17 02:23:22 -08:00
Cameron Gutman
1def825c7f Bump version to 4.0.1 2015-12-16 19:32:08 -08:00
Cameron Gutman
3c9b5d3b17 Update common 2015-12-16 19:31:24 -08:00
Cameron Gutman
3c2dd88fd3 Blacklist the whole device from HEVC decoding if Qualcomm's HEVC hybrid decoder is found 2015-12-16 19:20:00 -08:00
Cameron Gutman
0e21d5e166 Enable Amlogic and Rockchip decoders for HEVC 2015-12-16 18:46:37 -08:00
Cameron Gutman
8c221bd786 Remove the decoder option preference 2015-12-16 18:36:20 -08:00
Cameron Gutman
3b1fcdfb10 Display an error dialog if we can't find an H.264 decoder 2015-12-16 18:30:53 -08:00
Cameron Gutman
9bb91e1085 Remove FFMPEG decoding and supporting code 2015-12-16 18:21:11 -08:00
Cameron Gutman
98bee122fe Don't report any HEVC decoders on pre-Lollipop devices 2015-12-16 17:14:16 -08:00
Cameron Gutman
67dc2ef9ab Properly handle 4K detection on GFE 2.8 2015-12-16 16:39:38 -08:00
Cameron Gutman
6aaa9a83a6 Bump version to 4.0 2015-12-13 13:31:37 -08:00
Cameron Gutman
2eaea8ce7c Update common 2015-12-13 13:26:00 -08:00
Cameron Gutman
f5ded03b9b Add a line break to avoid "H.264" being split between 2 lines in the toast 2015-12-13 13:25:53 -08:00
Cameron Gutman
f509a4b3ab Don't use HEVC on the Shield Tablet by default (until further performance testing) 2015-12-13 13:20:11 -08:00
Cameron Gutman
6459579f15 Change decoder failure warning 2015-12-13 13:04:07 -08:00
Cameron Gutman
5112179fca Check for GFE version attribute before enabling 4K 2015-12-13 12:18:42 -08:00
Cameron Gutman
3f46485382 Add support for streaming H.265 from Maxwell 2 cards 2015-12-12 21:11:08 -08:00
Cameron Gutman
b640564689 Slicing seems to cause some artifacting issues, so I'm disabling it for now 2015-12-12 21:06:38 -08:00
Cameron Gutman
763f8938b3 Lower bitrate to 75% of original when streaming H.265 2015-12-12 21:06:19 -08:00
Cameron Gutman
4c67631ea5 Add negotiation logic for 4K and H.265 2015-12-12 17:18:15 -08:00
Cameron Gutman
920154b4b6 Add support for requesting an H265 stream (negotiation TBD) 2015-12-12 02:16:05 -08:00
Cameron Gutman
d8c7d10ed6 Add H265 support to depacketizer 2015-12-12 01:32:07 -08:00
Cameron Gutman
adcffa62d8 Refactor depacketizer to avoid H264 and AVC references -- no behavior changes 2015-12-12 00:06:37 -08:00
Cameron Gutman
2c5e6c0788 Merge branch '4k' 2015-12-11 23:48:29 -08:00
Cameron Gutman
a7d4a04ac2 Missed the SPS replay code when fixing the Annex B escape sequence issues 2015-11-20 18:57:23 -08:00
Cameron Gutman
d199c1b6c4 Merge branch 'master' into 4k
Conflicts:
	app/build.gradle
	app/libs/limelight-common.jar
2015-11-11 17:36:08 -08:00
Cameron Gutman
92f24d20db Bump version to 3.1.13 2015-11-11 17:28:42 -08:00
Cameron Gutman
0dd43df7aa Update common for GFE 2.8 support 2015-11-11 17:28:32 -08:00
Cameron Gutman
1675586a29 Add uses-feature for USB host 2015-11-11 17:28:10 -08:00
Cameron Gutman
a1e511b19a Remove ACCESS_SUPERUSER permission since it's deprecated in SuperSU 2015-11-11 17:27:55 -08:00
Cameron Gutman
260d716eb8 Fix broken app launching and resuming on GFE 2.8 2015-11-11 16:28:58 -08:00
Cameron Gutman
5606ed1308 Update version to 3.5-beta4 2015-11-08 19:18:06 -08:00
Cameron Gutman
a301575dd7 Merge branch 'master' into 4k
Conflicts:
	app/build.gradle
	app/src/main/java/com/limelight/preferences/PreferenceConfiguration.java
2015-11-08 19:08:13 -08:00
Cameron Gutman
e89e803d54 Zero controller values before removing a controller 2015-11-08 19:05:22 -08:00
Cameron Gutman
4486a126ad Fix some listener bugs in the XB1 driver 2015-11-08 19:03:12 -08:00
Cameron Gutman
d740e7a521 Add an Xbox One controller driver developed based on the xpad driver in the Linux kernel 2015-11-08 16:12:18 -08:00
Cameron Gutman
cb8eab443c Bump version to 3.5-beta3 2015-10-31 18:34:31 -07:00
Cameron Gutman
fe3b649fe9 Bump version to 3.1.12 2015-10-31 17:07:55 -07:00
Cameron Gutman
51c85a1b10 Merge branch 'master' into 4k
Conflicts:
	app/libs/limelight-common.jar
2015-10-31 16:46:46 -07:00
Cameron Gutman
7223efb9f8 Update common to fix video corruption bugs 2015-10-31 16:45:40 -07:00
Cameron Gutman
c3296cce3d Use setFixedSize if aspect ratios are compatible. This seems necessary for 4K video. 2015-10-31 16:25:15 -07:00
Cameron Gutman
74ea87676e Merge branch 'master' into 4k
Conflicts:
	app/libs/limelight-common.jar
2015-10-31 15:44:36 -07:00
Cameron Gutman
fc1c26b5d7 Fix video corruption caused by the first packet in the IDR frame getting chained multiple times, resulting in the reference count of other packets in the I-frame returning to zero prematurely 2015-10-31 15:29:52 -07:00
Cameron Gutman
df59c99f80 Reference count packets in the RTP queue so they don't get overwritten while queued 2015-10-31 15:27:33 -07:00
Cameron Gutman
5ef20aba21 Decrease polling period and increase polls before declaring the machine offline. Try requesting the app list again every 2 seconds if the app list has not been received yet. 2015-10-28 01:36:35 -07:00
Cameron Gutman
54eaee3f79 Use a lock to prevent serverinfo polling on a machine while applist is pending 2015-10-28 01:15:09 -07:00
Cameron Gutman
4c82da1f5c Update common with image quality improvements 2015-10-28 00:42:24 -07:00
Cameron Gutman
080dc01c21 Use a reference resolution rather than the actual stream resolution when scaling mouse movement 2015-10-28 00:24:26 -07:00
Cameron Gutman
f09fbf4ba6 Fix incorrect usage of SeqParameterSet.read() by feeding it possibly escaped Annex B NALUs 2015-10-28 00:24:16 -07:00
Cameron Gutman
8a465edad9 We might as well just keep the bitrate constant rather than doing all the complex logic to decide on a minimum. The dynamic scaling behavior is awful anyway. 2015-10-27 00:18:18 -07:00
Cameron Gutman
9d1510f14d Use setFixedSize if aspect ratios are compatible. This seems necessary for 4K video. 2015-10-27 00:04:28 -07:00
Cameron Gutman
62ea92335d Use a reference resolution rather than the actual stream resolution when scaling mouse movement 2015-10-26 23:59:53 -07:00
Cameron Gutman
9b9020b512 Adjust bitrate lower bounds to match the default resolution bitrate to fix image quality issues at the very beginning of the stream 2015-10-26 17:33:01 -07:00
Cameron Gutman
d1e2822b92 Update version to 3.5-beta1 2015-10-23 14:47:35 -07:00
Cameron Gutman
533cb747df Fix incorrect usage of SeqParameterSet.read() by feeding it possibly escaped Annex B NALUs 2015-10-23 14:46:03 -07:00
Cameron Gutman
33a0f9c97f Add 4K resolutions 2015-10-23 13:52:09 -07:00
Cameron Gutman
ef9a442718 Add 5.1 options in settings 2015-10-23 13:49:12 -07:00
Cameron Gutman
b9ac48532f Throw an exception if an invalid audio configuration is specified 2015-10-23 13:17:32 -07:00
Cameron Gutman
ad10413714 Update decoder code 2015-10-19 22:37:46 -07:00
Cameron Gutman
886ef425e6 Finish 5.1 surround sound support 2015-10-18 15:25:16 -07:00
Cameron Gutman
c9014da186 Transition to Opus Multistream Decoder API 2015-10-17 17:16:58 -07:00
Cameron Gutman
fbd61d2a21 Transition to the Opus Multistream Decoder API 2015-10-17 17:15:22 -07:00
Cameron Gutman
c025f9f02b Reduce code duplication 2015-10-17 15:44:24 -07:00
Cameron Gutman
b737acedb0 Bump version to 3.1.11 2015-10-15 02:00:29 -07:00
Cameron Gutman
f15bfe3038 Add support for mouse drag using long press 2015-10-15 01:50:05 -07:00
Cameron Gutman
8938f51292 Fix weird stair-stepping upward mouse movement on devices with a low scaling factor caused by rounding error (Nexus 9) 2015-10-15 01:48:31 -07:00
Cameron Gutman
4b92b8f714 Fix bug allowing computer polling to continue when the stream is resumed from the PcView activity 2015-10-15 00:55:05 -07:00
Cameron Gutman
5f13b9bca4 Don't set constraints 4 & 5 when using baseline profile hack 2015-10-13 19:29:36 -07:00
Cameron Gutman
2f219aac6f Only apply the constrained high profile SPS modification to Intel devices to avoid crashing other devices 2015-10-12 20:54:50 -07:00
Cameron Gutman
1d9efb30e2 Update to version 3.1.10 2015-10-11 17:11:48 -07:00
Cameron Gutman
ed7be00881 Update IML file 2015-10-11 17:11:08 -07:00
Cameron Gutman
a6003f6bff Remove MediaTek decoders from the decoders that need bitstream restrictions. The correct fix was to lower level_idc to reduce the required buffering. On newer MediaTek chipsets, sending bitstream restrictions actually slows down decoding by a factor of 3. 2015-10-11 16:57:37 -07:00
Cameron Gutman
4619045375 Revert "Update common to increase SSL handshake timeout"
This reverts commit 57b0da1a3a.
2015-10-11 14:54:45 -07:00
Cameron Gutman
469dcab5c7 Revert "Allow more time for the SSL handshake to take place"
This reverts commit dfde7b136e37fc9d8249bf758858b9da6c61c489.
2015-10-11 14:54:18 -07:00
Cameron Gutman
e61b8f1b34 Try a TCP connection before trying HTTPS to quickly eliminate transport layer connectivity issues 2015-10-11 14:39:02 -07:00
Cameron Gutman
79b6ec839a Fix machines becoming unreachable after they report IP addresses that they can't be contacted with 2015-10-11 14:16:38 -07:00
Cameron Gutman
fd12e30c53 Set constraint flags corresponding to Constrained High Profile on KitKat and higher. Fixes Nexus Player high latency on Android 6.0. 2015-10-10 23:28:48 -07:00
Cameron Gutman
87a9ca4318 Make touchscreen and stylus support more robust (supporting Bluetooth stylus in 6.0 and hopefully fixing broken touchscreen input on some devices) 2015-10-10 19:17:19 -07:00
Cameron Gutman
3f64411174 Only reload the PcView activity if UI settings were changed 2015-10-10 18:53:10 -07:00
Cameron Gutman
57b0da1a3a Update common to increase SSL handshake timeout 2015-10-10 18:15:01 -07:00
Cameron Gutman
fd1cb52f5f Allow more time for the SSL handshake to take place 2015-10-10 18:13:11 -07:00
Cameron Gutman
7d3e74a67f Allow the offline context menu to be opened when the PC state is unknown 2015-10-10 18:10:57 -07:00
Cameron Gutman
d704e322df Use RGB_565 for box art to reduce image size in memory 2015-10-10 18:09:51 -07:00
Cameron Gutman
f598153818 Small improvements to Media Codec DR 2015-10-10 15:17:24 -07:00
Cameron Gutman
f395a0c170 Fix warning 2015-10-10 15:04:49 -07:00
Cameron Gutman
654b33d27f Update build tools version to 23.0.1 2015-10-10 15:04:16 -07:00
Cameron Gutman
6c12da96c9 Add patched Jcodec library built from master a5d138efec2e940897e7e3d91a63a1f58abedd95 with changes from https://github.com/jcodec/jcodec/pull/90 2015-10-10 15:03:47 -07:00
Cameron Gutman
1a6f639b81 Fix discovery issues when adding a PC 2015-10-10 14:43:29 -07:00
Cameron Gutman
59a00a38c9 Limit box art assets to 5 MB each to prevent OOM crashes 2015-10-10 14:43:17 -07:00
Cameron Gutman
2beee168e3 Update README.md with additional download links 2015-08-30 12:37:23 -07:00
Cameron Gutman
a92bbc7e5a Increment version to 3.1.9 2015-08-18 00:44:01 -07:00
Cameron Gutman
fbc921dd07 Update build files 2015-08-18 00:39:01 -07:00
Cameron Gutman
59c6c3d777 Target Android 6.0 SDK 2015-08-18 00:38:49 -07:00
Cameron Gutman
e7ab61c8d0 Use jmDNS 3.4.2 which works properly on Android 6.0 2015-08-17 23:14:20 -07:00
Cameron Gutman
5175e68b99 Fix some issues with the new behavior of jmDNS 3.4.2 2015-08-17 22:38:01 -07:00
Cameron Gutman
64aa01b2cf Move to vanilla jmDNS 3.4.2 which fixes a bunch of mDNS issues and doesn't require a patch to work 2015-08-17 19:13:38 -07:00
Cameron Gutman
7023760782 Use monotonic system time for rendering timestamps. This is required now in Android 6.0 since these timestamps are propagated to the codec. 2015-08-17 18:40:25 -07:00
Cameron Gutman
63964ba6a7 Use monotonic system time for all timestamps 2015-08-17 18:33:05 -07:00
Cameron Gutman
932ce435b5 Remove unnecessary buffer clear 2015-08-17 18:26:33 -07:00
Cameron Gutman
af384d88f7 Handle potentially poisoned bitmap cache caused by truncated images from the server 2015-08-17 17:59:15 -07:00
Cameron Gutman
792846ddad Update MediaCodec renderer to avoid deprecated features on Lollipop 2015-08-17 17:52:57 -07:00
Cameron Gutman
1187d9c78c Update libraries 2015-08-14 09:29:56 -07:00
Cameron Gutman
e82683b0f4 Build for Java 1.7 compliance 2015-08-13 20:21:55 -07:00
Cameron Gutman
4b2299ed02 Update OkHttp and Okio libraries 2015-08-13 20:21:41 -07:00
Cameron Gutman
37db9ab072 Update common with latest fixes 2015-08-12 00:39:07 -07:00
Cameron Gutman
4a4f89a992 Revert the encoding bitrate hack 2015-08-12 00:05:54 -07:00
Cameron Gutman
d3a7bba666 Fix an RTP queue bug that can cause extended packet delays when the last packet ages out of the queue 2015-08-11 22:49:10 -07:00
Cameron Gutman
8bd6582d07 Fix off by one in reference frame invalidation 2015-08-11 21:33:06 -07:00
Cameron Gutman
875089305b Start frame number must be 0 for proper invalidation if the first frame is dropped 2015-08-11 21:26:35 -07:00
Cameron Gutman
c19ff71c9a Add experimental reference frame invalidation support 2015-08-11 21:12:34 -07:00
Cameron Gutman
36c320a584 Fix skipping first video packet 2015-08-11 08:42:21 -07:00
Cameron Gutman
fb40060560 Upgrade common jar to incorporate IDR frame fix 2015-08-04 23:46:20 -07:00
Cameron Gutman
a4f4887647 Upgrade build tools and libraries 2015-08-04 23:46:03 -07:00
Cameron Gutman
316b8c56f1 Improve IDR frame requests for Gen 4 servers to use the proper IDR frame request packet 2015-08-04 22:22:53 -07:00
Cameron Gutman
f1d7f556fd Bump to version 3.1.8 2015-07-21 18:03:37 -07:00
Cameron Gutman
1e70e1d329 GFE 2.5.11 update to fix black screen on Fire TV Stick 2015-07-18 17:06:41 -07:00
Cameron Gutman
2cf3855d35 Use the default vqos.bw.flags value sent by GFE 2015-07-18 17:05:15 -07:00
Cameron Gutman
e02a009635 Add support for the Razer Serval controller. The start and select buttons are manually handled for devices without a mapping for them. The back button is ignored so it can be used to exit the stream. 2015-07-18 00:46:25 -07:00
Cameron Gutman
bd6ff35603 Update to 3.1.7 2015-06-15 10:37:58 -07:00
Cameron Gutman
1cb7727dc7 Update common 2015-06-15 10:28:31 -07:00
Cameron Gutman
0c73e3d0ae Only propagate a decoder exception if it happens at the beginning of a stream 2015-06-15 10:28:09 -07:00
Cameron Gutman
13ec16c606 Update for GFE 2.4.5.54 support. The HTTPS /serverinfo query is now only available to paired clients. As a result, we catch the cert validation error and failover to HTTP. It's ugly but I don't see another way to do it. 2015-06-15 10:22:00 -07:00
Cameron Gutman
7d150e7e89 Fix a bug in RTP queue found when porting to moonlight-common-c 2015-06-15 09:51:59 -07:00
Cameron Gutman
7d61948d91 Take Moonlight out of messages displayed to the user for third party app support 2015-06-15 09:51:15 -07:00
Cameron Gutman
6371d364e1 Lint warning cleanup 2015-05-29 23:22:40 -05:00
Cameron Gutman
ded9c9140d Handle being online but not having a known reachability 2015-05-29 23:20:04 -05:00
Cameron Gutman
7c8a108e28 Use the leanback feature on API 21+ devices 2015-05-29 23:18:56 -05:00
Cameron Gutman
2a18ffcdba Update to Gradle 1.2.3 2015-05-19 10:10:18 -05:00
Cameron Gutman
381d0d5e81 Add support for multi-window functionality on Samsung devices 2015-05-10 00:02:04 -05:00
Cameron Gutman
be126acfd1 Update version info to 3.1.6 2015-05-05 20:52:53 -04:00
Cameron Gutman
fc2f5cfe4d Manually pass through Samsung capacitive buttons 2015-05-05 20:20:37 -04:00
Cameron Gutman
9878902a89 Use IDs to track controllers instead of descriptors. Fixes #64 2015-05-05 20:08:58 -04:00
Cameron Gutman
f1230d46f3 Android Studio 1.2 and Grade 1.2.2 update 2015-05-05 20:02:53 -04:00
Michelle Bergeron
d8822392f1 Link to site 2015-05-03 23:36:19 -07:00
Cameron Gutman
1d9cf71517 Total Eclipse of the Lime 2015-04-21 21:50:40 -04:00
Cameron Gutman
73de3cc91d Renaming projects to Moonlight 2015-04-21 21:43:27 -04:00
Cameron Gutman
2160e87fef Fix division by zero in ARC 2015-03-31 20:29:22 -04:00
Cameron Gutman
88249ba8aa Enable direct submission for ARC 2015-03-31 19:59:16 -04:00
Cameron Gutman
2856617fb3 Only release controller numbers if they were reserved 2015-03-31 19:58:47 -04:00
Cameron Gutman
d822980d5a Fix missing close of Closeables caught by StrictMode 2015-03-29 23:25:00 -04:00
Cameron Gutman
b5ba59b413 Fix database reference leak 2015-03-29 23:06:32 -04:00
Cameron Gutman
d71cbe344a Fix missing closes of Closeables caught by StrictMode 2015-03-29 23:00:41 -04:00
Cameron Gutman
1148e0163c Only assign a controller number when a valid controller input has been received. Fixes misdetection of other input devices as controllers (issue #65). 2015-03-29 22:54:48 -04:00
Cameron Gutman
cf36c7adb1 Increment version 2015-03-25 02:33:46 -04:00
Cameron Gutman
eac6998e17 Update the latency message strings to be more clear that this isn't end to end latency 2015-03-25 01:20:55 -04:00
Cameron Gutman
17afbffdb5 Include the time it takes to get an input buffer in the frame latency calculation 2015-03-25 01:08:23 -04:00
Cameron Gutman
072a439c2d Update common and decode unit API 2015-03-25 00:32:22 -04:00
Cameron Gutman
dbe01a17d2 Enable slicing again on GFE 2.4 to reduce latency on Qualcomm devices 2015-03-25 00:31:30 -04:00
Cameron Gutman
b3503cdede Reduce GCs and CPU usage by avoiding HashSet and LinkedList usage in the depacketizer. Also avoid atomic ref count operations for direct submit decoders. 2015-03-25 00:14:48 -04:00
Cameron Gutman
1ac6439690 Use an unsynchronized buffer list to cut down on overhead when using direct submit video renderers 2015-03-09 01:38:22 -05:00
Cameron Gutman
c481841ddf Change VideoDecoderRenderer to an abstract class so future interface changes can be made without breaking clients 2015-03-09 01:37:08 -05:00
Cameron Gutman
678269c561 Create a new UnsynchronizedPopulatedBufferList implementation that can be used for better direct submit performance 2015-03-09 01:35:53 -05:00
Cameron Gutman
83e874cdb6 Fix off-by-one to make printed buffers nicer 2015-03-08 22:21:11 -05:00
Cameron Gutman
90fc5797d5 Fix NvApp constructor 2015-02-27 14:11:15 -05:00
Cameron Gutman
fcfcce88dd Use an NvApp in the StreamConfiguration so it can be directly used by NvConnection 2015-02-27 14:08:39 -05:00
Cameron Gutman
fb8fc54bb1 Use App IDs for app lookups and deprecate the old name-based lookup function 2015-02-27 13:29:41 -05:00
Cameron Gutman
a4ec619e5a Add back the capability to get box art with NvHTTP 2015-02-27 01:15:21 -05:00
Cameron Gutman
8fb2622b66 Throw an exception if the received app list XML was incomplete 2015-02-26 17:58:03 -05:00
Cameron Gutman
e6527de786 Trim strings retrieved from the applist XML 2015-02-25 19:56:56 -05:00
Cameron Gutman
3f7e4d817f Allow verbose mode to be enabled at runtime 2015-02-25 19:56:20 -05:00
Cameron Gutman
6eaabc84aa Improve verbose debugging 2015-02-25 19:55:16 -05:00
Cameron Gutman
814557435d Print a better message if attempting to quit another device's stream 2015-02-25 19:54:35 -05:00
Cameron Gutman
2d5328fc24 Pass through the correct refresh rate now that people are doing sensible things with it 2015-02-13 17:07:14 -05:00
Cameron Gutman
95c82c5cc5 Fix a nasty bug in multiple controller support that could cause phantom inputs on controller 0 2015-02-12 21:06:37 -05:00
Cameron Gutman
fe907b0271 Handle NumberFormatExceptions from parseInt() 2015-02-11 17:24:27 -05:00
Cameron Gutman
7ae74a6a18 Never return an NvApp object that has no app ID assigned 2015-02-09 00:10:56 -05:00
Cameron Gutman
9772049295 Initialize the app name to empty string because it may not be present 2015-02-09 00:09:40 -05:00
Karim Mreisi
1d6b5a35bd Merge https://github.com/limelight-stream/limelight-android 2015-02-03 21:52:02 +01:00
Karim Mreisi
1ff6ee14ac fix analogstick, add minimum range and press deadzone, add movement touch to digital buttons depending on layers 2015-02-03 21:51:27 +01:00
Cameron Gutman
ec39f22ad8 Fix packet type for legacy controller packets and privatize some constants to prevent this bug from happening again 2015-02-02 18:06:05 -05:00
Cameron Gutman
51343a171d Add multiple controller support 2015-02-01 04:37:35 -05:00
Cameron Gutman
9988330613 Add some new common functions for app list caching 2015-01-30 18:27:15 -05:00
Karim Mreisi
d2e51e97c0 square analog stick for testing 2015-01-28 08:25:22 +01:00
Karim Mreisi
9f94465979 add virtual controller element abstraction class 2015-01-28 07:12:20 +01:00
Karim Mreisi
d83526ff5c add analog stick double click event, add button long press event, add virtual controller settings draft 2015-01-26 09:38:52 +01:00
Cameron Gutman
8235717502 Fix streaming from GFE 2.1.x 2015-01-25 21:33:24 -05:00
Cameron Gutman
3d29d76cd4 Fix broken streaming on Gen 4 servers 2015-01-25 21:21:58 -05:00
Cameron Gutman
cde5367f38 Tweak network packet loss message and threshold algorithm 2015-01-25 18:37:07 -05:00
Cameron Gutman
466463ebc3 Add SDP backwards compatibility 2015-01-25 18:12:25 -05:00
Cameron Gutman
aee255a6ee Backwards compatibility for video and control stream to GFE 2.1.x 2015-01-25 17:59:23 -05:00
Cameron Gutman
daf7598774 Use a single context object instead of passing around tons of objects. Start of GFE 2.1.x backwards compatibility. 2015-01-25 17:34:28 -05:00
Cameron Gutman
6de5cf8925 Disable deadzone scaling because it's pretty broken 2015-01-25 17:02:00 -05:00
Karim Mreisi
1d6b7e1b2e fix digital button/pad mouse movement, add selct & start button 2015-01-25 09:21:37 +01:00
Karim Mreisi
1c9458d056 fix digital button revoke event, update colors 2015-01-24 11:46:31 +01:00
Karim Mreisi
4e29f2ae8b add real digital pad and new digital buttons 2015-01-24 10:26:28 +01:00
Karim Mreisi
69321636b5 add LB and RB 2015-01-23 07:30:08 +01:00
Karim Mreisi
d190b254bd Merge https://github.com/limelight-stream/limelight-android 2015-01-23 06:57:51 +01:00
Cameron Gutman
52e5817327 Fix flags so bitrate scales up properly on GFE 2.2.2 2015-01-22 14:51:28 -05:00
Cameron Gutman
79ddfc65d5 Remove unused import 2015-01-22 14:17:33 -05:00
Cameron Gutman
6acb1fd92a Initial support for GFE 2.2.2+ 2015-01-22 14:13:02 -05:00
Karim Mreisi
005a96f3d3 fix not implemented toast message 2015-01-22 09:01:30 +01:00
Karim Mreisi
e39e0910a1 add virtual controller configuration screen 2015-01-22 08:59:55 +01:00
Karim Mreisi
56a6cee8f2 add touch controls 2015-01-22 08:06:14 +01:00
Cameron Gutman
bd2a1b8886 Improve streaming QoS 2014-12-02 00:54:52 -08:00
Cameron Gutman
021cfd1737 Lower the maximum RTP queuing delay to 10 ms instead of 40 ms to reduce the number of frames dropped when a packet is lost 2014-11-27 21:52:03 -06:00
Cameron Gutman
5fc438a0be Update XPP3 and OkHttp libraries 2014-11-23 23:26:12 -08:00
Cameron Gutman
3a3ac83ab5 Change timing of video initialization to prevent an ICMP port unreachable message on start that could tear down a NAT hole 2014-11-20 19:04:58 -08:00
Cameron Gutman
ff4570abac Reorder video stream initialization to vastly reduce the chance we'd miss the initial IDR frame 2014-11-19 10:42:29 -08:00
Cameron Gutman
c819f2f0e3 Request a new IDR frame immediately if we've been waiting for one for 120 frames 2014-11-19 10:40:39 -08:00
Cameron Gutman
c2cdb1264d Remove equals and hashCode from computer details 2014-11-17 19:00:07 -08:00
Cameron Gutman
c0e95ea18b Add count methods to PopulatedBufferList 2014-11-16 12:04:43 -08:00
Cameron Gutman
f21a81d7ac Add support back for specifying max packet size 2014-11-15 01:23:33 -08:00
Cameron Gutman
67c726a141 Lower the rate of UDP pings to 2 per second rather than 10 per second 2014-11-15 00:29:00 -08:00
Cameron Gutman
ff5f50e3ec Assign the frame start time when we get a slow path frame 2014-11-13 22:17:02 -08:00
Cameron Gutman
022a08f5a1 Throw proper exceptions when an HTTP request fails 2014-11-13 21:56:07 -08:00
Cameron Gutman
52f81274f0 Disable FEC to remove padding from the video stream 2014-11-11 17:42:18 -08:00
Cameron Gutman
35a50209be Revert "Fix waiting for IDR frames"
This reverts commit 37f0abfcd1e1490315e2f321db7d313d91e29cc6.
2014-11-11 17:04:48 -08:00
Cameron Gutman
9728c136f5 Revert "Also use the slow path for the last packet in each frame because it may be padded"
This reverts commit f2e7995747a5ed195926e88ee494602f6a09d9cf.
2014-11-11 16:59:42 -08:00
Cameron Gutman
bacd1d81fd Build with Java 1.6 2014-11-11 12:59:20 -08:00
Cameron Gutman
f36227b506 Fix waiting for IDR frames 2014-11-11 01:09:15 -08:00
Cameron Gutman
debd840db4 Start the video renderer earlier to give it time to warm up 2014-11-11 00:49:18 -08:00
Cameron Gutman
1531629fcd Also use the slow path for the last packet in each frame because it may be padded 2014-11-10 23:59:23 -08:00
Cameron Gutman
7bc325fa08 Add an Android TLS hack to fix GFE 2.1.4 and migrate to OkHttp classes for HTTPS 2014-11-10 21:58:03 -08:00
Cameron Gutman
2cc2d05c2f Display the app name instead of "starting app" 2014-11-06 20:48:08 -08:00
Cameron Gutman
1df03e137b Make network packet loss toast more tolerable 2014-11-06 20:40:33 -08:00
Cameron Gutman
b0169b0edf Use a builder for StreamConfiguration to avoid further breaking changes to the constructor. Add support for local audio playback mode. 2014-11-02 20:52:09 -08:00
Cameron Gutman
ae79f03e61 Add a hashCode function for ComputerDetails 2014-11-02 14:22:03 -08:00
Cameron Gutman
77a587abe8 Fix box art URL 2014-10-22 21:24:11 -04:00
Cameron Gutman
7f587dc389 Thread priority tweaks: Ensure renderer threads have higher priorities than the receive threads. Increase the priority of the resync thread to just below the video renderer so the control packet can be emitted ASAP. Lower the priority of the loss stats thread. Increase the priority of the input thread slightly above normal. 2014-10-17 17:03:58 -07:00
Cameron Gutman
8dbb03114d Improve axis scaling algorithm to fix cardinal direction issues 2014-10-16 21:16:12 -07:00
Cameron Gutman
415e96dec6 Fix endianness of integer SDP attributes. Fix transferProtocol attributes. 2014-10-10 22:15:21 -07:00
Cameron Gutman
abc7f135f3 Prevent a decoder stall from causing corruption of queued decode units 2014-10-10 21:15:50 -07:00
Cameron Gutman
4b93207def Improve ByteBufferDescriptor print functionality 2014-10-10 21:13:05 -07:00
Cameron Gutman
f004ae6a41 Use the entry's queue time rather than calling currentTimeMillis() again 2014-10-10 18:21:41 -07:00
Cameron Gutman
ac6120adc4 Send control messages as a single TCP packet because the streamer on the PC can choke if it doesn't receive the header and body at the same time 2014-10-10 18:14:43 -07:00
Cameron Gutman
d9c2d58519 Don't bind to the RTP ports explicitly. GFE doesn't force us to use these port numbers anymore. 2014-10-07 23:14:48 -07:00
Cameron Gutman
22865b8af4 Don't localize the PIN string 2014-10-05 15:46:09 -07:00
Cameron Gutman
431ba06742 Scale controller axis values to match how Xinput reads them on the PC. Clients that support this must have proper deadzone support (incorrect deadzones will be noticeable) and set ControllerPacket.enableAxisScaling 2014-10-01 20:22:06 -07:00
Cameron Gutman
ed8857552b Raise read timeout to 5 seconds and connect timeout to 3 seconds to avoid some spurious timeouts 2014-09-27 20:05:33 -07:00
Cameron Gutman
5f42ca66fe Add read timeouts to HTTP requests that should come back immediately if GFE is working properly 2014-09-27 15:45:13 -07:00
Cameron Gutman
c9d003ca6d Use a single byte buffer to serialize input packets 2014-09-27 14:58:51 -07:00
Cameron Gutman
330f40cc18 Disable resolution scaling for now 2014-09-20 02:34:53 -07:00
Cameron Gutman
44366db4d5 Properly fix the timer crash 2014-09-20 02:34:06 -07:00
Cameron Gutman
93bf28b87d Fix weird double cancel crash. This might need more investigation later. 2014-09-19 22:22:22 -07:00
Cameron Gutman
bd9c6834b7 Add support for adaptive resolution changes 2014-09-19 22:04:48 -07:00
Cameron Gutman
96ad2bcdef Update SDP generator for GFE 2.1.2 2014-09-19 19:37:37 -07:00
Cameron Gutman
14c03f0b37 Improve connection negotiation speed by caching serverinfo 2014-09-19 18:57:20 -07:00
Cameron Gutman
5bd30fe3dc Suppress connection warnings until 150 frames have come in 2014-09-17 01:58:41 -07:00
Cameron Gutman
5be499887d Fix a resource leak that was causing Limelight-Embedded to hang instead of exiting after using mDNS discovery 2014-09-16 19:17:25 -07:00
Cameron Gutman
7124963c56 Remove unnecessary byte buffer allocations in the most frequent control stream packets 2014-09-15 01:02:33 -07:00
Cameron Gutman
4377808896 Remove unnecessary byte buffer allocations in the input path 2014-09-14 23:58:42 -07:00
Cameron Gutman
50f8f78b8d Add support for vertical mouse scrolling 2014-09-03 22:52:48 -07:00
Cameron Gutman
1bb9a13c17 Generate a better message when a decoder fails to initialize 2014-09-03 20:41:21 -07:00
Cameron Gutman
e7f1b822f7 Rename button constants to something reasonable 2014-09-01 14:02:53 -07:00
Cameron Gutman
c9eee2e075 Rename NvController -> ControllerStream. Use a dedicated input thread rather than a thread pool for processing input. Batch analog axis updates and mouse moves. 2014-08-20 22:27:25 -07:00
Cameron Gutman
6556b3eb9b Make input packet fields package protected for batching 2014-08-20 21:25:08 -07:00
Cameron Gutman
cc92f3829e Fix sequencing errors that lead to drops in audio or video for potentially long periods under the right conditions 2014-08-09 03:39:14 -07:00
Cameron Gutman
33ffbe151f Lower the DU limit back to 15 because 30 can exhaust the video ring buffer 2014-08-06 15:55:02 -07:00
Cameron Gutman
60db0ff775 Wait for an IDR frame after dropping video data due to the decoder being too slow 2014-08-06 15:54:35 -07:00
Cameron Gutman
7ecac185ac Remove an old debug message 2014-08-06 15:53:38 -07:00
Cameron Gutman
b76495fa8f Send the newer loss stats packet 2014-08-05 22:28:04 -07:00
Cameron Gutman
463d4ad3fd Only generate 1 packet of FEC data per frame. Disable video resolution scaling on packet loss. 2014-08-04 10:20:49 -07:00
Cameron Gutman
0f0e41d5a4 Implement an RTP queue to handle out of order video and audio packets 2014-08-03 17:59:10 -07:00
Cameron Gutman
2d55562dd3 Improve tolerance to dropped packets 2014-07-31 15:20:43 -07:00
Cameron Gutman
cbe40fde92 Prevent network degradation from changing stream resolution 2014-07-31 14:06:30 -07:00
Cameron Gutman
97e62fdd34 Properly detect packet loss within a frame 2014-07-31 10:42:16 -07:00
Cameron Gutman
aa799342e5 Video stream updates for GFE 2.1.1 2014-07-31 01:32:15 -07:00
Cameron Gutman
ae8cb18f63 Remote input encryption changes for GFE 2.1.1 2014-07-31 00:22:17 -07:00
Cameron Gutman
8f53b6f233 RTSP changes to support GFE 2.1.1 2014-07-31 00:20:56 -07:00
Cameron Gutman
9e385215ce Add support for selecting sops on or off 2014-07-21 22:49:57 -07:00
Cameron Gutman
378fbedfa4 Remove/change some messages 2014-07-21 22:49:28 -07:00
Cameron Gutman
c84e063114 Update the packet loss message 2014-07-19 22:49:47 -07:00
Cameron Gutman
8d316ce9f0 Add fill screen flag for decoder renderer 2014-07-19 21:45:19 -07:00
Cameron Gutman
0019b93e8d Stop GFE from changing game settings 2014-07-19 21:42:16 -07:00
Cameron Gutman
2d66c6fb53 Let the bitrate scale from 1Mb to the maximum 2014-07-19 18:07:16 -07:00
Cameron Gutman
b629f674ca Improve audio robustness to packet reordering and duplication 2014-07-19 17:19:06 -07:00
Cameron Gutman
670622dfd7 Reduce FEC data to 1 packet at max to lower bandwidth requirements 2014-07-19 16:02:17 -07:00
Cameron Gutman
f35c2ead0f Fix video path with no renderer 2014-07-13 18:21:50 -07:00
Cameron Gutman
6b7b797089 Increase DU limits for bursty situations. Fix decode unit leak in audio path. 2014-07-12 19:16:39 -07:00
Cameron Gutman
15d4f6354d Fix mDNS recognition of PCs running GFE 2.1 2014-07-12 16:15:24 -07:00
Cameron Gutman
77cea99b35 "Fix" decode unit leak 2014-07-12 14:07:18 -07:00
Cameron Gutman
70b50bd096 Remove the remaining allocations in the AV paths 2014-07-12 13:37:53 -07:00
Cameron Gutman
c2401e7a75 Remove object allocations from audio decoding path 2014-07-12 12:21:57 -07:00
Cameron Gutman
b63c6223b0 Remove old MAC address finding code 2014-07-12 12:19:55 -07:00
Cameron Gutman
bcf10cc0b2 Remove the create function for MdnsDiscoveryAgent because it can't throw IOException anymore 2014-07-04 15:09:46 -07:00
Cameron Gutman
aaa73eb196 Fix a bug in the update function 2014-07-04 14:39:28 -07:00
Cameron Gutman
707c7a1a53 Fix interface changes while mDNS resolution is running 2014-07-04 12:12:50 -07:00
Cameron Gutman
dcb3e1c0e4 Fix a few parsing issues with the serverinfo XML 2014-07-04 11:49:48 -07:00
Cameron Gutman
894110ba08 Wake on LAN support. Many fixes for Limelight Android 2.5. 2014-07-03 23:30:29 -07:00
Cameron Gutman
a4dceb0b74 Add functions for decoders to return latency statistics 2014-06-30 21:10:27 -07:00
Cameron Gutman
1c82fdf048 Add flags back to the decode unit because TI OMAP devices need them 2014-06-30 21:03:16 -07:00
Cameron Gutman
ec303e485f Add a function to retrieve box art for an NvApp 2014-06-29 23:49:06 -07:00
Cameron Gutman
1cdcc6d190 Add mDNS support with a patched version of jmDNS 3.4.1 2014-06-29 23:48:03 -07:00
Cameron Gutman
ef1f44f873 Merge branch 'master' of github.com:s0ckz/limelight-common
Conflicts:
	src/com/limelight/nvstream/StreamConfiguration.java
2014-06-29 12:25:10 -07:00
Cameron Gutman
00e81e87de Allow renderer initialization to indicate failure 2014-06-29 11:38:27 -07:00
Cameron Gutman
2f082b9f85 Fix (currently) harmless bug in initial frame parsing 2014-06-29 11:24:20 -07:00
Cameron Gutman
c9e5230e37 Allow configuration of maximum packet size 2014-06-22 17:06:28 -07:00
Cameron Gutman
86e2657613 Stop allocating RtpPacket and VideoPacket objects in the reassembly path 2014-06-22 13:52:40 -07:00
s0ckz
c23470af40 Merge with upstream 2014-06-22 00:37:32 -03:00
Cameron Gutman
6c5ec3d2e9 Store the timestamp of the first packet received in the decode unit 2014-06-19 19:09:00 -07:00
Cameron Gutman
38423a9f37 Refactor the video decoding path so the DecoderRenderer handles pulling decode units instead of dedicating a separate thread for this 2014-06-19 18:28:48 -07:00
Cameron Gutman
7b10e52808 Set the receive thread's priority to maximum 2014-06-15 20:38:28 -07:00
Cameron Gutman
890ee846f7 Move Base64 encoding responsibilities out to the CryptoProviders for Android 2014-06-15 20:11:34 -07:00
Cameron Gutman
6a92ea74fc Cleanup and bugfix pairing code. 2014-06-15 18:54:32 -07:00
Cameron Gutman
07cf96c5ce Add support for secure pairing and input encryption to fix GFE 2.1 compatibility.
TODO:
Needs a LimelightCryptoProvider implementation for each platform to work.
Untested (and probably broken) on Android.
Needs more testing in general, especially in corner cases.
2014-06-15 04:40:47 -07:00
s0ckz
636c5f17f5 Different apps support and bug resolved
There was a bug that prevented the app from running again if it was
minimized. My solution is try to quit it before starting it again.
2014-05-30 20:05:46 -03:00
Cameron Gutman
4a2ee91700 Update Jar 2014-05-23 00:06:22 -04:00
Cameron Gutman
8e9d605248 Clamp to min = max so we can eliminate the time taken to scale the bitrate up to the ceiling 2014-05-12 20:00:52 -05:00
Cameron Gutman
a4e6738353 Update the A/V loss message 2014-05-12 19:42:00 -05:00
Cameron Gutman
0f815a0085 Fix variable names in the ConnectionStatusListener interface 2014-05-12 19:41:04 -05:00
Cameron Gutman
0c8c108bd1 Add support for sending proper packet loss statistics for server-side bandwidth scaling 2014-05-11 18:49:20 -05:00
Cameron Gutman
04941212cd Tweak some config to improve QoS when streaming 1080p30 over a so-so connection 2014-05-11 13:51:35 -04:00
Cameron Gutman
9ad45a3ca3 Update TinyRTSP Jar to work on Java 1.6 2014-05-11 13:40:58 -04:00
Cameron Gutman
176c8e9b93 Remove the deviceName field to fix a warning 2014-05-11 13:33:51 -04:00
Cameron Gutman
f537588228 Don't use the devicename parameter when pairing anymore. It's not using since GFE 2.0.0 and it can cause problems if invalid characters get added to the URL 2014-05-11 13:31:04 -04:00
Cameron Gutman
e593c04001 Increase the size of the receive buffer for video data 2014-05-11 13:29:51 -04:00
Cameron Gutman
1095d7808c Increase the size of the video ring buffer for high bitrate streaming 2014-05-11 13:29:20 -04:00
Cameron Gutman
d29dccba69 Stick to the older minimum when not using 1080p60 2014-05-07 23:56:38 -04:00
Cameron Gutman
a4098919b9 Add support for selecting maximum stream bitrate 2014-05-07 02:11:46 -04:00
Cameron Gutman
09e7ff0582 Update TinyRTSP 2014-05-07 00:23:16 -04:00
Cameron Gutman
09177be8f7 Drop the maximum bitrate of 720p60 to 13 Mbps 2014-05-07 00:22:57 -04:00
Cameron Gutman
eaa08bada4 Convert byte[] to char[] manually since IBM437 isn't available on some platforms 2014-05-07 00:22:32 -04:00
Cameron Gutman
5f93d55dab Don't convert IP string back into an InetAddress just leave it an InetAddress 2014-05-06 23:54:03 -04:00
Cameron Gutman
dfc0d518f8 Display an error if the PC is running a GFE version less than 2.0.1. 2014-05-06 23:40:09 -04:00
Cameron Gutman
eace3a0bf0 Update config to fix 720p60 and 1080p 2014-05-06 23:31:36 -04:00
Cameron Gutman
92adbe0983 Update control protocol for GFE 2.0.1 2014-05-06 23:31:09 -04:00
Cameron Gutman
aadbc3dd01 Update TinyRTSP 2014-05-06 21:31:36 -04:00
Cameron Gutman
d01a28c57f Initial work on updating for GFE 2.0.1's new RTSP-based handshake protocol 2014-05-06 21:18:57 -04:00
Cameron Gutman
4ee99a78b2 GFE 2.0.1 appears to have a bug where it drops all paired devices when the serverinfo query is sent with a uniqueid argument. Stop doing that to fix Limelight with GFE 2.0.1. 2014-05-06 10:21:17 -04:00
Cameron Gutman
bd9b37a5a0 Fix IPv6 incompatibility in HTTP code due to using raw IPv6 addresses in string format 2014-04-13 20:22:53 -04:00
Cameron Gutman
7947d8b75d Add the config hack to make 1080p30 work with the CPU decoder 2014-04-07 18:52:17 -04:00
Cameron Gutman
3408e467d5 Fix starting AV stream on GFE 2.0 2014-04-07 18:51:57 -04:00
Cameron Gutman
a0237a19d9 Remove duplicate code 2014-04-07 18:51:01 -04:00
Cameron Gutman
eb15599c01 Only increment the ring index if data was successfully queued 2014-03-17 14:20:43 -04:00
Cameron Gutman
8c9d0d171c Use ring buffers for audio and video handling to remove the last large allocations that were happening very frequently 2014-03-17 13:38:49 -04:00
Cameron Gutman
a39f4c5eab Use a single decoded audio buffer for direct submit audio renderers 2014-03-16 18:13:42 -04:00
Cameron Gutman
239dffcbdf Slightly reduce memory/GC pressure by using a smaller per-packet buffer of 1050 bytes instead of 1500 bytes 2014-03-13 21:53:52 -04:00
Cameron Gutman
3af3df0544 Reduce GC pressure significantly by using a single 100 byte buffer for all audio data instead of allocating 1500 bytes for each audio packet received 2014-03-13 21:53:10 -04:00
Cameron Gutman
7e30d043eb Fix handling packet loss in the second of a split frame 2014-03-13 01:31:47 -04:00
Cameron Gutman
4cbaee6806 Change the transient message when loss is experienced to clarify the actions the user should take 2014-03-03 19:41:23 -05:00
Cameron Gutman
8297ca7e85 Change LimeLog.info() back to calling Logger.info() internally so info messages are displayed by default 2014-03-03 19:40:02 -05:00
Cameron Gutman
da1c350067 Tweak warning thresholds after reducing the amount of frame invalidations requested 2014-02-27 02:07:53 -05:00
Cameron Gutman
c8c7512600 Hack around an issue where data gets added after the frame is thrown away 2014-02-27 01:31:07 -05:00
Aaron Neyer
9b6e12497e Make info log fine so it doesnt go to stdout 2014-02-26 20:53:11 -05:00
Cameron Gutman
50e7deeb32 Merge branch 'master' into logs
Conflicts:
	src/com/limelight/nvstream/av/audio/AudioDepacketizer.java
	src/com/limelight/nvstream/av/video/VideoDepacketizer.java
	src/com/limelight/nvstream/control/ControlStream.java
2014-02-26 16:22:04 -05:00
Cameron Gutman
7e3acd0213 Merge pull request #4 from irtimmer/config_javadoc
Config tuples meanings
2014-02-26 12:14:13 -05:00
Cameron Gutman
e60420cb2c Update depacketizer to do reference frame invalidation more like the official streamer. This should reduce the frequency of IDR requests by waiting for network stabilization before requesting the IDR frames. We still request IDR frames because reference frame invalidation still doesn't work well. 2014-02-26 12:12:06 -05:00
Iwan Timmer
c733be5611 Add Javadoc about config tuples 2014-02-26 16:17:25 +01:00
Cameron Gutman
4fbe93e62d Set flags on the decode units that indicate what type of data the frame contains 2014-02-26 01:00:17 -05:00
Cameron Gutman
bc2ca0b386 Increase resilience to packet loss. IDR frames are no longer requested if error correction data was lost. A maximum of one IDR frame is requested per corrupt frame. Error correction data is used to recover from the loss of a single-packet frame. 2014-02-24 12:54:03 -05:00
Cameron Gutman
ccc3eeebe8 Remove the depacketizer thread again... 2014-02-19 20:59:31 -05:00
Cameron Gutman
63ee6ef79a Add support for direct submission of buffers to the renderers without a separate thread 2014-02-19 20:36:53 -05:00
Cameron Gutman
cf3ac50d22 Increase the propensity for generating a loss warning 2014-02-19 20:36:12 -05:00
Cameron Gutman
cdf634dc41 Display messages if we detect that the device is having issues streaming 2014-02-19 19:03:51 -05:00
Cameron Gutman
21116f90a7 Remove unused imports 2014-02-17 19:22:49 -05:00
Cameron Gutman
29dd0e172c Fix the new config tuples to work with our current control stream code 2014-02-17 19:18:34 -05:00
Cameron Gutman
2d5083179c Revert "Remove depacketizer thread"
This reverts commit a2a4463c0b684fa54212fe497ac2a8931ebd8821.
2014-02-17 16:14:03 -05:00
Cameron Gutman
a96de39b28 Use packet flags to determine where frames end and begin instead of the packet index 2014-02-17 15:17:20 -05:00
Cameron Gutman
c93812179f Updated config code for artifact-free 1080p60 streaming 2014-02-17 14:27:21 -05:00
Cameron Gutman
26809c4b6b Decode parameter set NALs with the slow path so the SPS fixup hack still works 2014-02-17 13:57:15 -05:00
Cameron Gutman
0cce5b021e New video depacketizer that runs in O(1) time 2014-02-17 13:39:18 -05:00
Cameron Gutman
d54c1b07ce Merge pull request #3 from irtimmer/direct_decode
Reassemble NAL's early
2014-02-17 12:14:55 -05:00
Cameron Gutman
1a38cc2c0c Update config based on Shield OTA 68 2014-02-04 09:10:18 -05:00
Aaron Neyer
e188e1dd04 Merge branch 'master' of github.com:limelight-stream/limelight-common into logs
* 'master' of github.com:limelight-stream/limelight-common:
  Revert "Lower queued decode unit limit to resync faster if the renderers get behind. Lower the audio receive buffer size since it was unneccessarily large."
  Update README.md
  Revert the DU_LIMIT changes due to variance in platform audio rendering speed
  Lower queued decode unit limit to resync faster if the renderers get behind. Lower the audio receive buffer size since it was unneccessarily large.
  Drop code compliance to Java 1.5. Minor annotation changes for 1.5 compliance.
  Remove depacketizer thread
  add gpl
2014-02-02 20:24:50 -05:00
Iwan Timmer
ae18e00b13 Reassemble NAL's early 2014-02-03 00:30:44 +01:00
Cameron Gutman
c18b6ec00b Revert "Lower queued decode unit limit to resync faster if the renderers get behind. Lower the audio receive buffer size since it was unneccessarily large."
This reverts commit 255d65b148c21d1f4e9415922013f6ff91f5236a.
2014-01-24 15:33:18 -05:00
Michelle Bergeron
53c7bb0338 Update README.md
Fix broken link to limelight-android
2014-01-24 13:25:03 -05:00
Cameron Gutman
dfa3be78e4 Revert the DU_LIMIT changes due to variance in platform audio rendering speed 2014-01-22 17:08:01 -05:00
Cameron Gutman
932bb1145b Lower queued decode unit limit to resync faster if the renderers get behind. Lower the audio receive buffer size since it was unneccessarily large. 2014-01-22 17:01:37 -05:00
Cameron Gutman
46f4f5ccbe Merge pull request #2 from irtimmer/removethread
Remove depacketizer thread
2014-01-22 13:53:55 -08:00
Cameron Gutman
7f851c46f4 Drop code compliance to Java 1.5. Minor annotation changes for 1.5 compliance. 2014-01-22 13:38:24 -05:00
Iwan Timmer
82ae042f1c Remove depacketizer thread 2014-01-22 15:23:38 +01:00
Aaron Neyer
8ca3aab363 add gpl 2014-01-14 10:03:07 -05:00
Aaron Neyer
f95cd60cfd Created LimeLog as a wrapper around javas logger 2014-01-12 20:23:18 -05:00
Cameron Gutman
ab1e47edb4 Fix retreiving an empty MAC address string on some devices 2014-01-12 19:48:31 -05:00
Cameron Gutman
6a695d2c72 Fix H264 decoding artifacts when a frame has fewer than 3 bytes of padding at the end 2014-01-11 14:27:46 -05:00
Cameron Gutman
421d73b28a Display a more detailed error when attempting to resume another device's session 2014-01-11 10:43:29 -05:00
Cameron Gutman
042f67506c Remove the SPS hack from common, since it introduces compatibility problems for some devices 2014-01-10 00:27:35 -06:00
Cameron Gutman
96e5513cdb Fix resync request packet to hopefully keep working longer 2014-01-10 00:00:09 -06:00
Cameron Gutman
cc30752eb7 Only use a socket timeout when handshaking on the control stream. 2014-01-09 23:48:59 -06:00
Cameron Gutman
ade061bf3c Throw a GfeHttpResponseException if an HTTP response has an error code 2014-01-09 23:43:59 -06:00
Cameron Gutman
616945a963 Display the exception message when a stage fails 2014-01-09 23:40:57 -06:00
Cameron Gutman
3201fac36c Fix import warnings 2014-01-08 20:45:18 -06:00
Cameron Gutman
84551df36a Merge pull request #1 from irtimmer/master
Use a byte buffer for audio to minimize buffer copy's
2014-01-08 18:19:25 -08:00
Iwan Timmer
b32899f101 Use a byte buffer for audio to minimize buffer copy's 2014-01-07 20:40:21 +01:00
Cameron Gutman
b4a0f81eda Fixup bitstream_restriction_flag and max_dec_frame_buffering in the SPS to fix decoding latency issues on Tegra and the Raspberry Pi 2014-01-06 22:52:11 -06:00
Cameron Gutman
95d035f00b Wait for control stream threads to terminate. Terminate resync thread that was retaining the NvConnection object and all its children. 2014-01-04 20:07:14 -06:00
Cameron Gutman
666fbecc01 Use a 60 FPS redraw rate even if the stream is 30 FPS to reduce stutter 2013-12-29 01:41:17 -05:00
Cameron Gutman
6f8306cc18 Call the connectionTerminated() callback instead of trying to handle termination ourselves 2013-12-29 01:19:56 -05:00
Cameron Gutman
b0d5b9c767 Remove AvcDecoder from the shared code 2013-12-28 16:42:02 -05:00
Cameron Gutman
3648c0f26a Fix print vs println typo 2013-12-28 16:41:39 -05:00
Cameron Gutman
f1b4fdd8b0 Restore the old O(N) video depacketizer to fix artifacting. Add warnings for when the decode unit queue overflows. 2013-12-27 00:05:05 -05:00
Cameron Gutman
12658f4fb0 Update VideoDecoderRenderer interface with redrawRate and additional configuration flags 2013-12-26 17:28:37 -05:00
Cameron Gutman
ed92f9d28e Simply discard input events that occur when the thread pool has been shutdown (during connection closure) 2013-12-20 15:12:35 -05:00
Cameron Gutman
a1440621f9 Fix a race that could crash a few threads in the input thread pool 2013-12-20 15:06:56 -05:00
Cameron Gutman
48f8a05bae Increase the default receive buffers for the RTP sockets to avoid dropping packets while the receive thread is suspended by the OS scheduler. Windows uses particularly small (8KB) receive buffers by default which this should work around. 2013-12-19 14:50:50 -05:00
Cameron Gutman
4701c22b67 Create a StreamConfiguration class and use it to send correct information about the requested resolution and refresh rate to the various streaming components and the target PC. 2013-12-19 04:24:45 -05:00
Cameron Gutman
7f841c1fca Allow reuse of the UDP ports to avoid conflicts with GFE 2013-12-11 23:33:52 -05:00
Aaron Neyer
4f56dce9f7 add readme 2013-12-11 21:12:43 -05:00
Diego Waxemberg
4e9fb1bbce no longer print each time we send a keyboard packet 2013-12-09 11:56:20 -05:00
Diego Waxemberg
da47b43ad3 added support for all 3 mouse buttons 2013-12-08 16:25:41 -05:00
Diego Waxemberg
87152e6403 added support for keyboard modifier keys 2013-12-08 14:44:01 -05:00
Diego Waxemberg
895c123b13 removed keyboard packet print statment 2013-12-07 22:14:35 -05:00
Diego Waxemberg
35476e2c28 added support for sending keyboard key presses 2013-12-07 21:21:34 -05:00
Diego Waxemberg
29909e07e8 added render api for limelight-pc 2013-12-07 20:09:57 -05:00
Cameron Gutman
9ac103187f Fix an off-by-one in fallback interface selection 2013-12-05 19:27:46 -05:00
Cameron Gutman
ce1494895e Commit of common limelight core with bindings based on HEAD of RenderScript-Renderer 2013-12-05 12:57:49 -05:00
Cameron Gutman
41d2f6b0e2 Create .gitignore 2013-12-05 12:54:41 -05:00
813 changed files with 53425 additions and 100023 deletions

48
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View File

@@ -0,0 +1,48 @@
---
name: Bug report
about: Follow the troubleshooting guide before reporting a bug
---
**READ ME FIRST!**
If you're here because something basic is not working (like gamepad input, video, or similar), it's probably something specific to your setup, so make sure you've gone through the Troubleshooting Guide first: https://github.com/moonlight-stream/moonlight-docs/wiki/Troubleshooting
If you still have trouble with basic functionality after following the guide, join our Discord server where there are many other volunteers who can help (or direct you back here if it looks like a Moonlight bug after all). https://moonlight-stream.org/discord
**Describe the bug**
A clear and concise description of what the bug is.
**Steps to reproduce**
Any special steps that are required for the bug to appear.
**Screenshots**
If applicable, add screenshots to help explain your problem. If the issue is related to video glitching or poor quality, please include screenshots.
**Affected games**
List the games you've tried that exhibit the issue. To see if the issue is game-specific, try streaming Steam Big Picture with Moonlight and see if the issue persists there.
**Other Moonlight clients**
- Does the issue occur when using Moonlight on PC or iOS?
**Moonlight settings (please complete the following information)**
- Have any settings been adjusted from defaults?
- If so, which settings have been changed?
- Does the problem still occur after reverting settings back to default?
**Gamepad-related issues (please complete if problem is gamepad-related)**
- Do you have any gamepads connected to your host PC directly?
- If gamepad input is not working, does it work if you use Moonlight's on-screen controls?
- Does the problem still remain if you stream the desktop and use https://html5gamepad.com to test your gamepad?
- Instructions for streaming the desktop can be found here: https://github.com/moonlight-stream/moonlight-docs/wiki/Setup-Guide
**Device details (please complete the following information)**
- Android version: [e.g. Android 10]
- Device model: [e.g. Samsung Galaxy S21]
**Server PC details (please complete the following information)**
- OS: [e.g. Windows 10 1809]
- GeForce Experience version: [e.g. 3.16.0.140]
- Nvidia GPU driver: [e.g. 417.35]
- Antivirus and firewall software: [e.g. Windows Defender and Windows Firewall]
**Additional context**
Anything else you think may be relevant to the issue or special about your specific setup.

1
.github/ISSUE_TEMPLATE/config.yml vendored Normal file
View File

@@ -0,0 +1 @@
blank_issues_enabled: false

View File

@@ -0,0 +1,17 @@
---
name: Feature request
about: Suggest an idea for this project
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.

4
.github/auto-comment.yml vendored Normal file
View File

@@ -0,0 +1,4 @@
issuesOpened: >
If this is a question about Moonlight or you need help troubleshooting a streaming problem, please use the help channels on our [Discord server](https://moonlight-stream.org/discord) instead of GitHub issues. There are many more people available on Discord to help you and answer your questions.<br /><br />
This issue tracker should only be used for specific bugs or feature requests.<br /><br />
Thank you, and happy streaming!

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

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

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

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

13
.gitignore vendored
View File

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

3
.gitmodules vendored Normal file
View File

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

View File

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

View File

@@ -1,59 +1,35 @@
#Limelight
# Moonlight Android
Limelight 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.
[![AppVeyor Build Status](https://ci.appveyor.com/api/projects/status/232a8tadrrn8jv0k/branch/master?svg=true)](https://ci.appveyor.com/project/cgutman/moonlight-android/branch/master)
[![Translation Status](https://hosted.weblate.org/widgets/moonlight/-/moonlight-android/svg-badge.svg)](https://hosted.weblate.org/projects/moonlight/moonlight-android/)
Limelight will allow you to stream your full collection of games from your Windows PC to your Android device,
[Moonlight for Android](https://moonlight-stream.org) is an open source implementation of NVIDIA's GameStream, as used by the NVIDIA Shield.
Moonlight for Android will allow you to stream your full collection of games from your Windows PC to your Android device,
whether in your own home or over the internet.
[Limelight-pc](https://github.com/limelight-stream/limelight-pc) is also currently in development for Windows, OS X and Linux. Versions for [iOS](https://github.com/limelight-stream/limelight-ios) and [Windows and Windows Phone](https://github.com/limelight-stream/limelight-windows) are also in development.
Moonlight also has a [PC client](https://github.com/moonlight-stream/moonlight-qt) and [iOS/tvOS client](https://github.com/moonlight-stream/moonlight-ios).
Check our [wiki](https://github.com/limelight-stream/limelight-android/wiki) for more detailed information or a troubleshooting guide.
You can follow development on our [Discord server](https://moonlight-stream.org/discord) and help translate Moonlight into your language on [Weblate](https://hosted.weblate.org/projects/moonlight/moonlight-android/).
##Features
## Downloads
* [Google Play Store](https://play.google.com/store/apps/details?id=com.limelight)
* [Amazon App Store](https://www.amazon.com/gp/product/B00JK4MFN2)
* [F-Droid](https://f-droid.org/packages/com.limelight)
* [APK](https://github.com/moonlight-stream/moonlight-android/releases)
* Streams any of your games from your PC to your Android device
* Full gamepad support for MOGA, Xbox 360, PS3, OUYA, and Shield
* Automatically finds GameStream-compatible PCs on your network
## Building
* Install Android Studio and the Android NDK
* Run git submodule update --init --recursive from within moonlight-android/
* In moonlight-android/, create a file called local.properties. Add an ndk.dir= property to the local.properties file and set it equal to your NDK directory.
* Build the APK using Android Studio or gradle
##Installation
* Download and install Limelight for Android from
[Google Play](https://play.google.com/store/apps/details?id=com.limelight)
* Download [GeForce Experience](http://www.geforce.com/geforce-experience) and install on your Windows PC
##Requirements
* [GameStream compatible](http://shield.nvidia.com/play-pc-games/) computer with GTX 600/700 series GPU
* Android device running 4.1 (Jelly Bean) or higher
* High-end wireless router (802.11n dual-band recommended)
##Usage
* Turn on GameStream in the GFE settings
* If you are connecting from outside the same network, turn on internet
streaming
* When on the same network as your PC, open Limelight and tap on your PC in the list
* Accept the pairing confirmation on your PC
* Tap your PC again to view the list of apps to stream
* Play games!
##Contribute
This project is being actively developed at [XDA Developers](http://forum.xda-developers.com/showthread.php?t=2505510)
1. Fork us
2. Write code
3. Send Pull Requests
Check out our [website](http://limelight-stream.com) for project links and information.
##Authors
## Authors
* [Cameron Gutman](https://github.com/cgutman)
* [Diego Waxemberg](https://github.com/dwaxemberg)
* [Aaron Neyer](https://github.com/Aaronneyer)
* [Andrew Hennessy](https://github.com/yetanothername)
Limelight is the work of students at [Case Western](http://case.edu) and was
Moonlight is the work of students at [Case Western](http://case.edu) and was
started as a project at [MHacks](http://mhacks.org).

View File

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

View File

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

Binary file not shown.

Binary file not shown.

Binary file not shown.

6
app/lint.xml Normal file
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
app/proguard-rules.pro vendored Normal file
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.**

View File

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

View File

@@ -4,78 +4,158 @@
<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-feature android:name="android.hardware.touchscreen" android:required="false" />
<uses-feature android:name="android.hardware.wifi" android:required="false" />
<uses-feature android:name="android.hardware.gamepad" android:required="false" />
<uses-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"
android:required="false" />
<uses-feature
android:name="android.hardware.wifi"
android:required="false" />
<uses-feature
android:name="android.hardware.gamepad"
android:required="false" />
<uses-feature
android:name="android.hardware.usb.host"
android:required="false" />
<uses-feature
android:name="android.software.leanback"
android:required="false" />
<!-- Disable legacy input emulation on ChromeOS -->
<uses-feature
android:name="android.hardware.type.pc"
android:required="false"/>
<application
android:allowBackup="true"
android:icon="@drawable/ic_launcher"
android:theme="@style/AppTheme" >
<!-- Launcher for traditional devices -->
android:fullBackupContent="@xml/backup_rules"
android:networkSecurityConfig="@xml/network_security_config"
android:isGame="true"
android:banner="@drawable/atv_banner"
android:appCategory="game"
android:icon="@mipmap/ic_launcher"
android:roundIcon="@mipmap/ic_launcher"
android:installLocation="auto"
android:gwpAsanMode="always"
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"
android:required="false" />
<meta-data
android:name="com.sec.android.support.multiwindow"
android:value="true" />
<!-- Samsung DeX support requires explicit placement of android:resizeableActivity="true"
in each activity even though it is implied by targeting API 24+ -->
<activity
android:name=".PcView"
android:configChanges="mcc|mnc|locale|touchscreen|keyboard|keyboardHidden|navigation|screenLayout|fontScale|uiMode|orientation|screenSize|smallestScreenSize|layoutDirection">
android:exported="true"
android:resizeableActivity="true"
android:configChanges="mcc|mnc|touchscreen|keyboard|keyboardHidden|navigation|screenLayout|fontScale|uiMode|orientation|screenSize|smallestScreenSize|layoutDirection">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
<category android:name="android.intent.category.MULTIWINDOW_LAUNCHER" />
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
<category android:name="tv.ouya.intent.category.APP" />
</intent-filter>
</activity>
<!-- Launcher for Android TV devices -->
<!-- Small hack to support launcher shortcuts without relaunching over and over again when the back button is pressed -->
<activity
android:name=".PcViewTv"
android:logo="@drawable/atv_banner"
android:icon="@drawable/atv_banner"
android:name=".ShortcutTrampoline"
android:noHistory="true"
android:exported="true"
android:resizeableActivity="true"
android:configChanges="mcc|mnc|locale|touchscreen|keyboard|keyboardHidden|navigation|screenLayout|fontScale|uiMode|orientation|screenSize|smallestScreenSize|layoutDirection">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
</intent-filter>
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value="com.limelight.PcView" />
</activity>
<activity
android:name=".AppView"
android:configChanges="mcc|mnc|locale|touchscreen|keyboard|keyboardHidden|navigation|screenLayout|fontScale|uiMode|orientation|screenSize|smallestScreenSize|layoutDirection" >
android:resizeableActivity="true"
android:configChanges="mcc|mnc|touchscreen|keyboard|keyboardHidden|navigation|screenLayout|fontScale|uiMode|orientation|screenSize|smallestScreenSize|layoutDirection">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value="com.limelight.PcView" />
</activity>
<activity
android:name=".preferences.StreamSettings"
android:label="Streaming Settings" >
android:resizeableActivity="true"
android:label="Streaming Settings">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value="com.limelight.PcView" />
</activity>
<activity
android:name=".preferences.AddComputerManually"
android:label="Add Computer Manually" >
android:resizeableActivity="true"
android:windowSoftInputMode="stateVisible"
android:label="Add Computer Manually">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value="com.limelight.PcView" />
</activity>
<!-- This will fall back to sensorLandscape at runtime on Android 4.2 and below -->
<activity
android:name=".Game"
android:screenOrientation="sensorLandscape"
android:configChanges="mcc|mnc|locale|touchscreen|keyboard|keyboardHidden|navigation|screenLayout|fontScale|uiMode|orientation|screenSize|smallestScreenSize|layoutDirection"
android:screenOrientation="userLandscape"
android:noHistory="true"
android:supportsPictureInPicture="true"
android:resizeableActivity="true"
android:launchMode="singleTask"
android:excludeFromRecents="true"
android:theme="@style/StreamTheme"
android:configChanges="mcc|mnc|locale|touchscreen|keyboard|keyboardHidden|navigation|screenLayout|fontScale|uiMode|orientation|screenSize|smallestScreenSize|layoutDirection" >
android:preferMinimalPostProcessing="true">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value="com.limelight.AppView" />
<!-- Special metadata for NVIDIA Shield devices to prevent input buffering
and most importantly, opt out of mouse acceleration while streaming -->
<meta-data
android:name="com.nvidia.immediateInput"
android:value="true" />
<meta-data
android:name="com.nvidia.rawCursorInput"
android:value="true" />
</activity>
<service
android:name=".discovery.DiscoveryService"
android:label="mDNS PC Auto-Discovery Service" />
<service
android:name=".computers.ComputerManagerService"
android:label="Computer Management Service" />
<service
android:name=".binding.input.driver.UsbDriverService"
android:label="Usb Driver Service" />
<activity
android:name=".HelpActivity"
android:resizeableActivity="true"
android:configChanges="mcc|mnc|touchscreen|keyboard|keyboardHidden|navigation|screenLayout|fontScale|uiMode|orientation|screenSize|smallestScreenSize|layoutDirection">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value="com.limelight.PcView" />
</activity>
</application>
</manifest>
</manifest>

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

View File

@@ -1,38 +1,37 @@
package com.limelight;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.StringReader;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.UUID;
import com.limelight.binding.PlatformBinding;
import com.limelight.computers.ComputerManagerListener;
import com.limelight.computers.ComputerManagerService;
import com.limelight.grid.AppGridAdapter;
import com.limelight.nvstream.http.ComputerDetails;
import com.limelight.nvstream.http.GfeHttpResponseException;
import com.limelight.nvstream.http.NvApp;
import com.limelight.nvstream.http.NvHTTP;
import com.limelight.nvstream.http.PairingManager;
import com.limelight.preferences.PreferenceConfiguration;
import com.limelight.ui.AdapterFragment;
import com.limelight.ui.AdapterFragmentCallbacks;
import com.limelight.utils.CacheHelper;
import com.limelight.utils.Dialog;
import com.limelight.utils.ServerHelper;
import com.limelight.utils.ShortcutHelper;
import com.limelight.utils.SpinnerDialog;
import com.limelight.utils.UiHelper;
import android.app.Activity;
import android.app.AlertDialog;
import android.app.Service;
import android.content.ComponentName;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.ServiceConnection;
import android.content.SharedPreferences;
import android.content.res.Configuration;
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;
@@ -43,13 +42,17 @@ 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;
private ShortcutHelper shortcutHelper;
private ComputerDetails computer;
private ComputerManagerService.ApplistPoller poller;
@@ -57,14 +60,23 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
private String lastRawApplist;
private int lastRunningAppId;
private boolean suspendGridUpdates;
private boolean inForeground;
private boolean showHiddenApps;
private HashSet<Integer> hiddenAppIds = new HashSet<>();
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;
private final static int HIDE_APP_ID = 7;
public final static String HIDDEN_APPS_PREF_FILENAME = "HiddenApps";
public final static String NAME_EXTRA = "Name";
public final static String UUID_EXTRA = "UUID";
public final static String NEW_PAIR_EXTRA = "NewPair";
public final static String SHOW_HIDDEN_APPS_EXTRA = "ShowHiddenApps";
private ComputerManagerService.ComputerManagerBinder managerBinder;
private final ServiceConnection serviceConnection = new ServiceConnection() {
@@ -79,32 +91,63 @@ 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());
PreferenceConfiguration.readPreferences(AppView.this),
computer, localBinder.getUniqueId(),
showHiddenApps);
} catch (Exception e) {
e.printStackTrace();
finish();
return;
}
appGridAdapter.updateHiddenApps(hiddenAppIds, true);
// Now make the binder visible. We must do this after appGridAdapter
// is set to prevent us from reaching updateUiWithServerinfo() and
// touching the appGridAdapter prior to initialization.
managerBinder = localBinder;
// Load the app grid with cached data (if possible).
// This must be done _before_ startComputerUpdates()
// so the initial serverinfo response can update the running
// icon.
populateAppGridWithCache();
// Start updates
startComputerUpdates();
// Load the app grid with cached data (if possible)
populateAppGridWithCache();
runOnUiThread(new Runnable() {
@Override
public void run() {
if (isFinishing() || isChangingConfigurations()) {
return;
}
getFragmentManager().beginTransaction()
.replace(R.id.appFragmentContainer, new AdapterFragment())
.commitAllowingStateLoss();
// Despite my best efforts to catch all conditions that could
// cause the activity to be destroyed when we try to commit
// I haven't been able to, so we have this try-catch block.
try {
getFragmentManager().beginTransaction()
.replace(R.id.appFragmentContainer, new AdapterFragment())
.commitAllowingStateLoss();
} catch (IllegalStateException e) {
e.printStackTrace();
}
}
});
}
}.start();
}
@@ -114,21 +157,43 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
}
};
@Override
public void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
// If appGridAdapter is initialized, let it know about the configuration change.
// If not, it will pick it up when it initializes.
if (appGridAdapter != null) {
// Update the app grid adapter to create grid items with the correct layout
appGridAdapter.updateLayoutWithPreferences(this, PreferenceConfiguration.readPreferences(this));
try {
// Reinflate the app grid itself to pick up the layout change
getFragmentManager().beginTransaction()
.replace(R.id.appFragmentContainer, new AdapterFragment())
.commitAllowingStateLoss();
} catch (IllegalStateException e) {
e.printStackTrace();
}
}
}
private void startComputerUpdates() {
if (managerBinder == null) {
// Don't start polling if we're not bound or in the foreground
if (managerBinder == null || !inForeground) {
return;
}
managerBinder.startPolling(new ComputerManagerListener() {
@Override
public void notifyComputerUpdated(ComputerDetails details) {
public void notifyComputerUpdated(final ComputerDetails details) {
// Do nothing if updates are suspended
if (suspendGridUpdates) {
return;
}
// Don't care about other computers
if (!details.uuid.toString().equalsIgnoreCase(uuidString)) {
if (!details.uuid.equalsIgnoreCase(uuidString)) {
return;
}
@@ -146,6 +211,24 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
return;
}
// Close immediately if the PC is no longer paired
if (details.state == ComputerDetails.State.ONLINE && details.pairState != PairingManager.PairState.PAIRED) {
AppView.this.runOnUiThread(new Runnable() {
@Override
public void run() {
// Disable shortcuts referencing this PC for now
shortcutHelper.disableComputerShortcut(details,
getResources().getString(R.string.scut_not_paired));
// Display a toast to the user and quit the activity
Toast.makeText(AppView.this, getResources().getText(R.string.scut_not_paired), Toast.LENGTH_SHORT).show();
finish();
}
});
return;
}
// App list is the same or empty
if (details.rawAppList == null || details.rawAppList.equals(lastRawApplist)) {
@@ -164,12 +247,15 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
try {
updateUiWithAppList(NvHTTP.getAppListByReader(new StringReader(details.rawAppList)));
updateUiWithServerinfo(details);
if (blockingLoadSpinner != null) {
blockingLoadSpinner.dismiss();
blockingLoadSpinner = null;
}
} catch (Exception ignored) {}
} catch (XmlPullParserException | IOException e) {
e.printStackTrace();
}
}
});
@@ -197,29 +283,52 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
String locale = PreferenceConfiguration.readPreferences(this).language;
if (!locale.equals(PreferenceConfiguration.DEFAULT_LANGUAGE)) {
Configuration config = new Configuration(getResources().getConfiguration());
config.locale = new Locale(locale);
getResources().updateConfiguration(config, getResources().getDisplayMetrics());
}
// Assume we're in the foreground when created to avoid a race
// between binding to CMS and onResume()
inForeground = true;
shortcutHelper = new ShortcutHelper(this);
UiHelper.setLocale(this);
setContentView(R.layout.activity_app_view);
UiHelper.notifyNewRootView(this);
showHiddenApps = getIntent().getBooleanExtra(SHOW_HIDDEN_APPS_EXTRA, false);
uuidString = getIntent().getStringExtra(UUID_EXTRA);
String labelText = getResources().getString(R.string.title_applist)+" "+getIntent().getStringExtra(NAME_EXTRA);
TextView label = (TextView) findViewById(R.id.appListText);
setTitle(labelText);
label.setText(labelText);
SharedPreferences hiddenAppsPrefs = getSharedPreferences(HIDDEN_APPS_PREF_FILENAME, MODE_PRIVATE);
for (String hiddenAppIdStr : hiddenAppsPrefs.getStringSet(uuidString, new HashSet<String>())) {
hiddenAppIds.add(Integer.parseInt(hiddenAppIdStr));
}
String computerName = getIntent().getStringExtra(NAME_EXTRA);
TextView label = findViewById(R.id.appListText);
setTitle(computerName);
label.setText(computerName);
// Bind to the computer manager service
bindService(new Intent(this, ComputerManagerService.class), serviceConnection,
Service.BIND_AUTO_CREATE);
}
private void updateHiddenApps(boolean hideImmediately) {
HashSet<String> hiddenAppIdStringSet = new HashSet<>();
for (Integer hiddenAppId : hiddenAppIds) {
hiddenAppIdStringSet.add(hiddenAppId.toString());
}
getSharedPreferences(HIDDEN_APPS_PREF_FILENAME, MODE_PRIVATE)
.edit()
.putStringSet(uuidString, hiddenAppIdStringSet)
.apply();
appGridAdapter.updateHiddenApps(hiddenAppIds, hideImmediately);
}
private void populateAppGridWithCache() {
try {
// Try to load from cache
@@ -227,7 +336,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();
@@ -259,6 +368,10 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
protected void onResume() {
super.onResume();
// Display a decoder crash notification if we've returned after a crash
UiHelper.showDecoderCrashDialog(this);
inForeground = true;
startComputerUpdates();
}
@@ -266,36 +379,49 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
protected void onPause() {
super.onPause();
inForeground = false;
stopComputerUpdates();
}
private int getRunningAppId() {
int runningAppId = -1;
for (int i = 0; i < appGridAdapter.getCount(); i++) {
AppObject app = (AppObject) appGridAdapter.getItem(i);
if (app.app.getIsRunning()) {
runningAppId = app.app.getAppId();
break;
}
}
return runningAppId;
}
@Override
public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) {
super.onCreateContextMenu(menu, v, menuInfo);
AdapterContextMenuInfo info = (AdapterContextMenuInfo) menuInfo;
AppObject selectedApp = (AppObject) appGridAdapter.getItem(info.position);
int runningAppId = getRunningAppId();
if (runningAppId != -1) {
if (runningAppId == selectedApp.app.getAppId()) {
menu.setHeaderTitle(selectedApp.app.getAppName());
if (lastRunningAppId != 0) {
if (lastRunningAppId == selectedApp.app.getAppId()) {
menu.add(Menu.NONE, START_OR_RESUME_ID, 1, getResources().getString(R.string.applist_menu_resume));
menu.add(Menu.NONE, QUIT_ID, 2, getResources().getString(R.string.applist_menu_quit));
}
else {
menu.add(Menu.NONE, START_WTIH_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, START_WITH_QUIT, 1, getResources().getString(R.string.applist_menu_quit_and_start));
}
}
// Only show the hide checkbox if this is not the currently running app or it's already hidden
if (lastRunningAppId != selectedApp.app.getAppId() || selectedApp.isHidden) {
MenuItem hideAppItem = menu.add(Menu.NONE, HIDE_APP_ID, 3, getResources().getString(R.string.applist_menu_hide_app));
hideAppItem.setCheckable(true);
hideAppItem.setChecked(selectedApp.isHidden);
}
menu.add(Menu.NONE, VIEW_DETAILS_ID, 4, 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, 5, getResources().getString(R.string.applist_menu_scut));
}
}
}
}
@@ -309,7 +435,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
@@ -330,8 +456,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() {
@@ -346,7 +471,28 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
}, null);
return true;
case CANCEL_ID:
case VIEW_DETAILS_ID:
Dialog.displayDialog(AppView.this, getResources().getString(R.string.title_details), app.app.toString(), false);
return true;
case HIDE_APP_ID:
if (item.isChecked()) {
// Transitioning hidden to shown
hiddenAppIds.remove(app.app.getAppId());
}
else {
// Transitioning shown to hidden
hiddenAppIds.add(app.app.getAppId());
}
updateHiddenApps(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:
@@ -365,19 +511,19 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
AppObject existingApp = (AppObject) appGridAdapter.getItem(i);
// There can only be one or zero apps running.
if (existingApp.app.getIsRunning() &&
if (existingApp.isRunning &&
existingApp.app.getAppId() == details.runningGameId) {
// This app was running and still is, so we're done now
return;
}
else if (existingApp.app.getAppId() == details.runningGameId) {
// This app wasn't running but now is
existingApp.app.setIsRunning(true);
existingApp.isRunning = true;
updated = true;
}
else if (existingApp.app.getIsRunning()) {
else if (existingApp.isRunning) {
// This app was running but now isn't
existingApp.app.setIsRunning(false);
existingApp.isRunning = false;
updated = true;
}
else {
@@ -407,10 +553,6 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
AppObject existingApp = (AppObject) appGridAdapter.getItem(i);
if (existingApp.app.getAppId() == app.getAppId()) {
// Found the app; update its properties
if (existingApp.app.getIsRunning() != app.getIsRunning()) {
existingApp.app.setIsRunning(app.getIsRunning());
updated = true;
}
if (!existingApp.app.getAppName().equals(app.getAppName())) {
existingApp.app.setAppName(app.getAppName());
updated = true;
@@ -424,6 +566,12 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
if (!foundExistingApp) {
// This app must be new
appGridAdapter.addApp(new AppObject(app));
// We could have a leftover shortcut from last time this PC was paired
// or if this app was removed then added again. Enable those shortcuts
// again if present.
shortcutHelper.enableAppShortcut(computer, app);
updated = true;
}
}
@@ -444,6 +592,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;
@@ -465,9 +614,8 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
@Override
public int getAdapterFragmentLayoutId() {
return PreferenceConfiguration.readPreferences(this).listMode ?
R.layout.list_view : (PreferenceConfiguration.readPreferences(AppView.this).smallIconMode ?
R.layout.app_grid_view_small : R.layout.app_grid_view);
return PreferenceConfiguration.readPreferences(AppView.this).smallIconMode ?
R.layout.app_grid_view_small : R.layout.app_grid_view;
}
@Override
@@ -480,19 +628,22 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
AppObject app = (AppObject) appGridAdapter.getItem(pos);
// Only open the context menu if something is running, otherwise start it
if (getRunningAppId() != -1) {
if (lastRunningAppId != 0) {
openContextMenu(arg1);
} else {
ServerHelper.doStart(AppView.this, app.app, computer, managerBinder);
}
}
});
UiHelper.applyStatusBarPadding(listView);
registerForContextMenu(listView);
listView.requestFocus();
}
public class AppObject {
public static class AppObject {
public final NvApp app;
public boolean isRunning;
public boolean isHidden;
public AppObject(NvApp app) {
if (app == null) {

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -0,0 +1,25 @@
package com.limelight;
import java.io.IOException;
import java.util.logging.FileHandler;
import java.util.logging.Logger;
public class LimeLog {
private static final Logger LOGGER = Logger.getLogger(LimeLog.class.getName());
public static void info(String msg) {
LOGGER.info(msg);
}
public static void warning(String msg) {
LOGGER.warning(msg);
}
public static void severe(String msg) {
LOGGER.severe(msg);
}
public static void setFileHandler(String fileName) throws IOException {
LOGGER.addHandler(new FileHandler(fileName));
}
}

View File

@@ -2,15 +2,14 @@ package com.limelight;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.Locale;
import com.limelight.binding.PlatformBinding;
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;
@@ -18,20 +17,26 @@ import com.limelight.nvstream.http.PairingManager;
import com.limelight.nvstream.http.PairingManager.PairState;
import com.limelight.nvstream.wol.WakeOnLanSender;
import com.limelight.preferences.AddComputerManually;
import com.limelight.preferences.GlPreferences;
import com.limelight.preferences.PreferenceConfiguration;
import com.limelight.preferences.StreamSettings;
import com.limelight.ui.AdapterFragment;
import com.limelight.ui.AdapterFragmentCallbacks;
import com.limelight.utils.Dialog;
import com.limelight.utils.HelpLauncher;
import com.limelight.utils.ServerHelper;
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;
import android.content.ServiceConnection;
import android.content.res.Configuration;
import android.opengl.GLSurfaceView;
import android.os.Build;
import android.os.Bundle;
import android.os.IBinder;
import android.preference.PreferenceManager;
@@ -49,11 +54,17 @@ 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;
public class PcView extends Activity implements AdapterFragmentCallbacks {
private RelativeLayout noPcFoundLayout;
private PcGridAdapter pcGridAdapter;
private ShortcutHelper shortcutHelper;
private ComputerManagerService.ComputerManagerBinder managerBinder;
private boolean freezeUpdates, runningPolling;
private boolean freezeUpdates, runningPolling, inForeground, completeOnCreateCalled;
private final ServiceConnection serviceConnection = new ServiceConnection() {
public void onServiceConnected(ComponentName className, IBinder binder) {
final ComputerManagerService.ComputerManagerBinder localBinder =
@@ -83,20 +94,30 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
}
};
@Override
public void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
// Reinitialize views just in case orientation changed
initializeViews();
// Only reinitialize views if completeOnCreate() was called
// before this callback. If it was not, completeOnCreate() will
// handle initializing views with the config change accounted for.
// This is not prone to races because both callbacks are invoked
// in the main thread.
if (completeOnCreateCalled) {
// Reinitialize views just in case orientation changed
initializeViews();
}
}
private final static int APP_LIST_ID = 1;
private final static int PAIR_ID = 2;
private final static int UNPAIR_ID = 3;
private final static int WOL_ID = 4;
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 final static int FULL_APP_LIST_ID = 9;
private final static int TEST_NETWORK_ID = 10;
private void initializeViews() {
setContentView(R.layout.activity_pc_view);
@@ -106,9 +127,13 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
// Set default preferences if we've never been run
PreferenceManager.setDefaultValues(this, R.xml.preferences, false);
// Set the correct layout for the PC grid
pcGridAdapter.updateLayoutWithPreferences(this, PreferenceConfiguration.readPreferences(this));
// Setup the list view
ImageButton settingsButton = (ImageButton) findViewById(R.id.settingsButton);
ImageButton addComputerButton = (ImageButton) findViewById(R.id.manuallyAddPc);
ImageButton settingsButton = findViewById(R.id.settingsButton);
ImageButton addComputerButton = findViewById(R.id.manuallyAddPc);
ImageButton helpButton = findViewById(R.id.helpButton);
settingsButton.setOnClickListener(new OnClickListener() {
@Override
@@ -123,12 +148,18 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
startActivity(i);
}
});
helpButton.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
HelpLauncher.launchSetupGuide(PcView.this);
}
});
getFragmentManager().beginTransaction()
.replace(R.id.pcFragmentContainer, new AdapterFragment())
.commitAllowingStateLoss();
noPcFoundLayout = (RelativeLayout) findViewById(R.id.no_pc_found_layout);
noPcFoundLayout = findViewById(R.id.no_pc_found_layout);
if (pcGridAdapter.getCount() == 0) {
noPcFoundLayout.setVisibility(View.VISIBLE);
}
@@ -142,30 +173,69 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
String locale = PreferenceConfiguration.readPreferences(this).language;
if (!locale.equals(PreferenceConfiguration.DEFAULT_LANGUAGE)) {
Configuration config = new Configuration(getResources().getConfiguration());
config.locale = new Locale(locale);
getResources().updateConfiguration(config, getResources().getDisplayMetrics());
// Assume we're in the foreground when created to avoid a race
// between binding to CMS and onResume()
inForeground = true;
// Create a GLSurfaceView to fetch GLRenderer unless we have
// a cached result already.
final GlPreferences glPrefs = GlPreferences.readPreferences(this);
if (!glPrefs.savedFingerprint.equals(Build.FINGERPRINT) || glPrefs.glRenderer.isEmpty()) {
GLSurfaceView surfaceView = new GLSurfaceView(this);
surfaceView.setRenderer(new GLSurfaceView.Renderer() {
@Override
public void onSurfaceCreated(GL10 gl10, EGLConfig eglConfig) {
// Save the GLRenderer string so we don't need to do this next time
glPrefs.glRenderer = gl10.glGetString(GL10.GL_RENDERER);
glPrefs.savedFingerprint = Build.FINGERPRINT;
glPrefs.writePreferences();
LimeLog.info("Fetched GL Renderer: " + glPrefs.glRenderer);
runOnUiThread(new Runnable() {
@Override
public void run() {
completeOnCreate();
}
});
}
@Override
public void onSurfaceChanged(GL10 gl10, int i, int i1) {
}
@Override
public void onDrawFrame(GL10 gl10) {
}
});
setContentView(surfaceView);
}
else {
LimeLog.info("Cached GL Renderer: " + glPrefs.glRenderer);
completeOnCreate();
}
}
private void completeOnCreate() {
completeOnCreateCalled = true;
shortcutHelper = new ShortcutHelper(this);
UiHelper.setLocale(this);
// Bind to the computer manager service
bindService(new Intent(PcView.this, ComputerManagerService.class), serviceConnection,
Service.BIND_AUTO_CREATE);
pcGridAdapter = new PcGridAdapter(this,
PreferenceConfiguration.readPreferences(this).listMode,
PreferenceConfiguration.readPreferences(this).smallIconMode);
pcGridAdapter = new PcGridAdapter(this, PreferenceConfiguration.readPreferences(this));
initializeViews();
}
private void startComputerUpdates() {
if (managerBinder != null) {
if (runningPolling) {
return;
}
// Only allow polling to start if we're bound to CMS, polling is not already running,
// and our activity is in the foreground.
if (managerBinder != null && !runningPolling && inForeground) {
freezeUpdates = false;
managerBinder.startPolling(new ComputerManagerListener() {
@Override
@@ -215,6 +285,10 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
protected void onResume() {
super.onResume();
// Display a decoder crash notification if we've returned after a crash
UiHelper.showDecoderCrashDialog(this);
inForeground = true;
startComputerUpdates();
}
@@ -222,6 +296,7 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
protected void onPause() {
super.onPause();
inForeground = false;
stopComputerUpdates(false);
}
@@ -241,19 +316,33 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
AdapterContextMenuInfo info = (AdapterContextMenuInfo) menuInfo;
ComputerObject computer = (ComputerObject) pcGridAdapter.getItem(info.position);
if (computer.details.reachability == ComputerDetails.Reachability.UNKNOWN) {
startComputerUpdates();
return;
// Add a header with PC status details
menu.clearHeader();
String headerTitle = computer.details.name + " - ";
switch (computer.details.state)
{
case ONLINE:
headerTitle += getResources().getString(R.string.pcview_menu_header_online);
break;
case OFFLINE:
menu.setHeaderIcon(R.drawable.ic_pc_offline);
headerTitle += getResources().getString(R.string.pcview_menu_header_offline);
break;
case UNKNOWN:
headerTitle += getResources().getString(R.string.pcview_menu_header_unknown);
break;
}
menu.setHeaderTitle(headerTitle);
// Inflate the context menu
if (computer.details.reachability == ComputerDetails.Reachability.OFFLINE) {
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));
}
else if (computer.details.pairState != PairState.PAIRED) {
menu.add(Menu.NONE, PAIR_ID, 1, getResources().getString(R.string.pcview_menu_pair_pc));
menu.add(Menu.NONE, DELETE_ID, 2, getResources().getString(R.string.pcview_menu_delete_pc));
}
else {
if (computer.details.runningGameId != 0) {
@@ -261,21 +350,25 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
menu.add(Menu.NONE, QUIT_ID, 2, getResources().getString(R.string.applist_menu_quit));
}
menu.add(Menu.NONE, APP_LIST_ID, 3, getResources().getString(R.string.pcview_menu_app_list));
// FIXME: We used to be able to unpair here but it's been broken since GFE 2.1.x, so I've replaced
// 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, FULL_APP_LIST_ID, 4, getResources().getString(R.string.pcview_menu_app_list));
}
menu.add(Menu.NONE, TEST_NETWORK_ID, 5, getResources().getString(R.string.pcview_menu_test_network));
menu.add(Menu.NONE, DELETE_ID, 6, getResources().getString(R.string.pcview_menu_delete_pc));
menu.add(Menu.NONE, VIEW_DETAILS_ID, 7, getResources().getString(R.string.pcview_menu_details));
}
@Override
public void onContextMenuClosed(Menu menu) {
// For some reason, this gets called again _after_ onPause() is called on this activity.
// startComputerUpdates() manages this and won't actual start polling until the activity
// returns to the foreground.
startComputerUpdates();
}
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;
}
@@ -299,19 +392,11 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
// Stop updates and wait while pairing
stopComputerUpdates(true);
InetAddress addr = null;
if (computer.reachability == ComputerDetails.Reachability.LOCAL) {
addr = computer.localIp;
}
else if (computer.reachability == ComputerDetails.Reachability.REMOTE) {
addr = computer.remoteIp;
}
httpConn = new NvHTTP(addr,
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;
@@ -323,17 +408,29 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
Dialog.displayDialog(PcView.this, getResources().getString(R.string.pair_pairing_title),
getResources().getString(R.string.pair_pairing_msg)+" "+pinStr, false);
PairingManager.PairState pairState = httpConn.pair(pinStr);
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.PAIRED) {
else if (pairState == PairState.ALREADY_IN_PROGRESS) {
message = getResources().getString(R.string.pair_already_in_progress);
}
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);
}
else {
// Should be no other values
@@ -344,7 +441,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();
}
@@ -361,20 +458,21 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
}
if (toastSuccess) {
// Open the app list after a successful pairing attemp
doAppList(computer);
// Open the app list after a successful pairing attempt
doAppList(computer, true, false);
}
else {
// Start polling again if we're still in the foreground
startComputerUpdates();
}
}
});
// Start polling again
startComputerUpdates();
}
}).start();
}
private void doWakeOnLan(final ComputerDetails computer) {
if (computer.reachability != ComputerDetails.Reachability.OFFLINE) {
if (computer.state == ComputerDetails.State.ONLINE) {
Toast.makeText(PcView.this, getResources().getString(R.string.wol_pc_online), Toast.LENGTH_SHORT).show();
return;
}
@@ -384,7 +482,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() {
@@ -408,7 +505,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;
}
@@ -424,17 +522,9 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
NvHTTP httpConn;
String message;
try {
InetAddress addr = null;
if (computer.reachability == ComputerDetails.Reachability.LOCAL) {
addr = computer.localIp;
}
else if (computer.reachability == ComputerDetails.Reachability.REMOTE) {
addr = computer.remoteIp;
}
httpConn = new NvHTTP(addr,
httpConn = new NvHTTP(ServerHelper.getCurrentAddressFromComputer(computer),
managerBinder.getUniqueId(),
PlatformBinding.getDeviceName(),
computer.serverCert,
PlatformBinding.getCryptoProvider(PcView.this));
if (httpConn.getPairState() == PairingManager.PairState.PAIRED) {
httpConn.unpair();
@@ -452,8 +542,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;
@@ -467,8 +558,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, boolean showHiddenGames) {
if (computer.state == ComputerDetails.State.OFFLINE) {
Toast.makeText(PcView.this, getResources().getString(R.string.error_pc_offline), Toast.LENGTH_SHORT).show();
return;
}
@@ -479,7 +570,9 @@ 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);
i.putExtra(AppView.SHOW_HIDDEN_APPS_EXTRA, showHiddenGames);
startActivity(i);
}
@@ -501,16 +594,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);
case FULL_APP_LIST_ID:
doAppList(computer.details, false, true);
return true;
case RESUME_ID:
@@ -519,7 +620,7 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
return true;
}
ServerHelper.doStart(this, new NvApp("app", computer.details.runningGameId), computer.details, managerBinder);
ServerHelper.doStart(this, new NvApp("app", computer.details.runningGameId, false), computer.details, managerBinder);
return true;
case QUIT_ID:
@@ -532,23 +633,44 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
UiHelper.displayQuitConfirmationDialog(this, new Runnable() {
@Override
public void run() {
ServerHelper.doQuit(PcView.this,
ServerHelper.getCurrentAddressFromComputer(computer.details),
new NvApp("app", 0), managerBinder, null);
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;
case TEST_NETWORK_ID:
ServerHelper.doNetworkTest(PcView.this);
return true;
default:
return super.onContextItemSelected(item);
}
}
private void removeComputer(ComputerDetails details) {
managerBinder.removeComputer(details);
new DiskAssetLoader(this).deleteAssetsForComputer(details.uuid);
// Delete hidden games preference value
getSharedPreferences(AppView.HIDDEN_APPS_PREF_FILENAME, MODE_PRIVATE)
.edit()
.remove(details.uuid)
.apply();
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.disableComputerShortcut(details,
getResources().getString(R.string.scut_deleted_pc));
pcGridAdapter.removeComputer(computer);
pcGridAdapter.notifyDataSetChanged();
@@ -575,6 +697,11 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
}
}
// Add a launcher shortcut for this PC
if (details.pairState == PairState.PAIRED) {
shortcutHelper.createAppViewShortcutForOnlineHost(details);
}
if (existingEntry != null) {
// Replace the information in the existing entry
existingEntry.details = details;
@@ -593,9 +720,7 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
@Override
public int getAdapterFragmentLayoutId() {
return PreferenceConfiguration.readPreferences(this).listMode ?
R.layout.list_view : (PreferenceConfiguration.readPreferences(this).smallIconMode ?
R.layout.pc_grid_view_small : R.layout.pc_grid_view);
return R.layout.pc_grid_view;
}
@Override
@@ -606,23 +731,23 @@ 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) {
// Do nothing
} else if (computer.details.reachability == ComputerDetails.Reachability.OFFLINE) {
// Open the context menu if a PC is 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, false);
}
}
});
UiHelper.applyStatusBarPadding(listView);
registerForContextMenu(listView);
}
public class ComputerObject {
public static class ComputerObject {
public ComputerDetails details;
public ComputerObject(ComputerDetails details) {

View File

@@ -1,6 +0,0 @@
package com.limelight;
/* This is a dummy class to allow for a separate icon
* and launcher for TV.
*/
public class PcViewTv extends PcView {}

View File

@@ -0,0 +1,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();
}
}

View File

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

View File

@@ -1,97 +1,219 @@
package com.limelight.binding.audio;
import android.media.AudioAttributes;
import android.media.AudioFormat;
import android.media.AudioManager;
import android.media.AudioTrack;
import android.os.Build;
import com.limelight.LimeLog;
import com.limelight.nvstream.av.audio.AudioRenderer;
import com.limelight.nvstream.jni.MoonBridge;
public class AndroidAudioRenderer implements AudioRenderer {
private static final int FRAME_SIZE = 960;
private AudioTrack track;
@Override
public boolean streamInitialized(int channelCount, int sampleRate) {
int channelConfig;
int bufferSize;
switch (channelCount)
{
case 1:
channelConfig = AudioFormat.CHANNEL_OUT_MONO;
break;
case 2:
channelConfig = AudioFormat.CHANNEL_OUT_STEREO;
break;
default:
LimeLog.severe("Decoder returned unhandled channel count");
return false;
private AudioTrack createAudioTrack(int channelConfig, int sampleRate, int bufferSize, boolean lowLatency) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
return new AudioTrack(AudioManager.STREAM_MUSIC,
sampleRate,
channelConfig,
AudioFormat.ENCODING_PCM_16BIT,
bufferSize,
AudioTrack.MODE_STREAM);
}
else {
AudioAttributes.Builder attributesBuilder = new AudioAttributes.Builder()
.setUsage(AudioAttributes.USAGE_GAME);
AudioFormat format = new AudioFormat.Builder()
.setEncoding(AudioFormat.ENCODING_PCM_16BIT)
.setSampleRate(sampleRate)
.setChannelMask(channelConfig)
.build();
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
// Use FLAG_LOW_LATENCY on L through N
if (lowLatency) {
attributesBuilder.setFlags(AudioAttributes.FLAG_LOW_LATENCY);
}
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
AudioTrack.Builder trackBuilder = new AudioTrack.Builder()
.setAudioFormat(format)
.setAudioAttributes(attributesBuilder.build())
.setTransferMode(AudioTrack.MODE_STREAM)
.setBufferSizeInBytes(bufferSize);
// Use PERFORMANCE_MODE_LOW_LATENCY on O and later
if (lowLatency) {
trackBuilder.setPerformanceMode(AudioTrack.PERFORMANCE_MODE_LOW_LATENCY);
}
return trackBuilder.build();
}
else {
return new AudioTrack(attributesBuilder.build(),
format,
bufferSize,
AudioTrack.MODE_STREAM,
AudioManager.AUDIO_SESSION_ID_GENERATE);
}
}
}
@Override
public int setup(MoonBridge.AudioConfiguration audioConfiguration, int sampleRate, int samplesPerFrame) {
int channelConfig;
int bytesPerFrame;
switch (audioConfiguration.channelCount)
{
case 2:
channelConfig = AudioFormat.CHANNEL_OUT_STEREO;
break;
case 4:
channelConfig = AudioFormat.CHANNEL_OUT_QUAD;
break;
case 6:
channelConfig = AudioFormat.CHANNEL_OUT_5POINT1;
break;
case 8:
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
// AudioFormat.CHANNEL_OUT_7POINT1_SURROUND isn't available until Android 6.0,
// yet the CHANNEL_OUT_SIDE_LEFT and CHANNEL_OUT_SIDE_RIGHT constants were added
// in 5.0, so just hardcode the constant so we can work on Lollipop.
channelConfig = 0x000018fc; // AudioFormat.CHANNEL_OUT_7POINT1_SURROUND
}
else {
// On KitKat and lower, creation of the AudioTrack will fail if we specify
// CHANNEL_OUT_SIDE_LEFT or CHANNEL_OUT_SIDE_RIGHT. That leaves us with
// the old CHANNEL_OUT_7POINT1 which uses left-of-center and right-of-center
// speakers instead of side-left and side-right. This non-standard layout
// is probably not what the user wants, but we don't really have a choice.
channelConfig = AudioFormat.CHANNEL_OUT_7POINT1;
}
break;
default:
LimeLog.severe("Decoder returned unhandled channel count");
return -1;
}
LimeLog.info("Audio channel config: "+String.format("0x%X", channelConfig));
bytesPerFrame = audioConfiguration.channelCount * samplesPerFrame * 2;
// We're not supposed to request less than the minimum
// buffer size for our buffer, but it appears that we can
// do this on many devices and it lowers audio latency.
// We'll try the small buffer size first and if it fails,
// use the recommended larger buffer size.
try {
// Buffer two frames of audio if possible
bufferSize = FRAME_SIZE * 2;
track = new AudioTrack(AudioManager.STREAM_MUSIC,
sampleRate,
channelConfig,
AudioFormat.ENCODING_PCM_16BIT,
bufferSize,
AudioTrack.MODE_STREAM);
track.play();
} catch (Exception e) {
// Try to release the AudioTrack if we got far enough
try {
if (track != null) {
track.release();
}
} catch (Exception ignored) {}
for (int i = 0; i < 4; i++) {
boolean lowLatency;
int bufferSize;
// Now try the larger buffer size
bufferSize = Math.max(AudioTrack.getMinBufferSize(sampleRate,
// We will try:
// 1) Small buffer, low latency mode
// 2) Large buffer, low latency mode
// 3) Small buffer, standard mode
// 4) Large buffer, standard mode
switch (i) {
case 0:
case 1:
lowLatency = true;
break;
case 2:
case 3:
lowLatency = false;
break;
default:
// Unreachable
throw new IllegalStateException();
}
switch (i) {
case 0:
case 2:
bufferSize = bytesPerFrame * 2;
break;
case 1:
case 3:
// Try the larger buffer size
bufferSize = Math.max(AudioTrack.getMinBufferSize(sampleRate,
channelConfig,
AudioFormat.ENCODING_PCM_16BIT),
FRAME_SIZE * 2);
bytesPerFrame * 2);
// Round to next frame
bufferSize = (((bufferSize + (FRAME_SIZE - 1)) / FRAME_SIZE) * FRAME_SIZE);
// Round to next frame
bufferSize = (((bufferSize + (bytesPerFrame - 1)) / bytesPerFrame) * bytesPerFrame);
break;
default:
// Unreachable
throw new IllegalStateException();
}
track = new AudioTrack(AudioManager.STREAM_MUSIC,
sampleRate,
channelConfig,
AudioFormat.ENCODING_PCM_16BIT,
bufferSize,
AudioTrack.MODE_STREAM);
track.play();
// Skip low latency options if hardware sample rate doesn't match the content
if (AudioTrack.getNativeOutputSampleRate(AudioManager.STREAM_MUSIC) != sampleRate && lowLatency) {
continue;
}
try {
track = createAudioTrack(channelConfig, sampleRate, bufferSize, lowLatency);
track.play();
// Successfully created working AudioTrack. We're done here.
LimeLog.info("Audio track configuration: "+bufferSize+" "+lowLatency);
break;
} catch (Exception e) {
// Try to release the AudioTrack if we got far enough
e.printStackTrace();
try {
if (track != null) {
track.release();
track = null;
}
} catch (Exception ignored) {}
}
}
LimeLog.info("Audio track buffer size: "+bufferSize);
return true;
}
@Override
public void playDecodedAudio(byte[] audioData, int offset, int length) {
track.write(audioData, offset, length);
}
@Override
public void streamClosing() {
if (track != null) {
track.release();
if (track == null) {
// Couldn't create any audio track for playback
return -2;
}
}
@Override
public int getCapabilities() {
return 0;
}
@Override
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.getPendingAudioDuration() < 40) {
// 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 much pending audio data: " + MoonBridge.getPendingAudioDuration() +" ms");
}
}
@Override
public void start() {}
@Override
public void stop() {}
@Override
public void cleanup() {
// Immediately drop all pending data
track.pause();
track.flush();
track.release();
}
}

View File

@@ -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,27 +92,21 @@ 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
LimeLog.warning("Corrupted certificate");
return false;
} catch (NoSuchAlgorithmException e) {
// Should never happen
e.printStackTrace();
return false;
throw new RuntimeException(e);
} catch (InvalidKeySpecException e) {
// May happen if the key is corrupt
LimeLog.warning("Corrupted key");
return false;
} catch (NoSuchProviderException e) {
// Should never happen
e.printStackTrace();
return false;
}
return true;
@@ -129,17 +119,11 @@ 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;
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException(e);
}
Date now = new Date();
@@ -160,13 +144,11 @@ 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");

View File

@@ -2,259 +2,296 @@ package com.limelight.binding.input;
import android.view.KeyEvent;
import com.limelight.nvstream.NvConnection;
import com.limelight.nvstream.input.KeycodeTranslator;
/**
* Class to translate a Android key code into the codes GFE is expecting
* @author Diego Waxemberg
* @author Cameron Gutman
*/
public class KeyboardTranslator extends KeycodeTranslator {
/**
* GFE's prefix for every key code
*/
private static final short KEY_PREFIX = (short) 0x80;
public static final int VK_0 = 48;
public static final int VK_9 = 57;
public static final int VK_A = 65;
public static final int VK_Z = 90;
public static final int VK_ALT = 18;
public static final int VK_NUMPAD0 = 96;
public class KeyboardTranslator {
/**
* GFE's prefix for every key code
*/
private static final short KEY_PREFIX = (short) 0x80;
public static final int VK_0 = 48;
public static final int VK_9 = 57;
public static final int VK_A = 65;
public static final int VK_Z = 90;
public static final int VK_NUMPAD0 = 96;
public static final int VK_BACK_SLASH = 92;
public static final int VK_CAPS_LOCK = 20;
public static final int VK_CLEAR = 12;
public static final int VK_COMMA = 44;
public static final int VK_CONTROL = 17;
public static final int VK_BACK_SPACE = 8;
public static final int VK_EQUALS = 61;
public static final int VK_ESCAPE = 27;
public static final int VK_F1 = 112;
public static final int VK_PERIOD = 46;
public static final int VK_INSERT = 155;
public static final int VK_OPEN_BRACKET = 91;
public static final int VK_WINDOWS = 524;
public static final int VK_MINUS = 45;
public static final int VK_END = 35;
public static final int VK_HOME = 36;
public static final int VK_NUM_LOCK = 144;
public static final int VK_PAGE_UP = 33;
public static final int VK_PAGE_DOWN = 34;
public static final int VK_PLUS = 521;
public static final int VK_CLOSE_BRACKET = 93;
public static final int VK_SCROLL_LOCK = 145;
public static final int VK_SEMICOLON = 59;
public static final int VK_SHIFT = 16;
public static final int VK_SLASH = 47;
public static final int VK_SPACE = 32;
public static final int VK_PRINTSCREEN = 154;
public static final int VK_TAB = 9;
public static final int VK_LEFT = 37;
public static final int VK_RIGHT = 39;
public static final int VK_UP = 38;
public static final int VK_DOWN = 40;
public static final int VK_BACK_QUOTE = 192;
public static final int VK_QUOTE = 222;
public static final int VK_PAUSE = 19;
/**
* Constructs a new translator for the specified connection
* @param conn the connection to which the translated codes are sent
*/
public KeyboardTranslator(NvConnection conn) {
super(conn);
}
/**
* Translates the given keycode and returns the GFE keycode
* @param keycode the code to be translated
* @return a GFE keycode for the given keycode
*/
@Override
public short translate(int keycode) {
int translated;
/* There seems to be no clean mapping between Android key codes
* and what Nvidia sends over the wire. If someone finds one,
* I'll happily delete this code :)
*/
if (keycode >= KeyEvent.KEYCODE_0 &&
keycode <= KeyEvent.KEYCODE_9) {
translated = (keycode - KeyEvent.KEYCODE_0) + VK_0;
}
else if (keycode >= KeyEvent.KEYCODE_A &&
keycode <= KeyEvent.KEYCODE_Z) {
translated = (keycode - KeyEvent.KEYCODE_A) + VK_A;
}
else if (keycode >= KeyEvent.KEYCODE_NUMPAD_0 &&
keycode <= KeyEvent.KEYCODE_NUMPAD_9) {
translated = (keycode - KeyEvent.KEYCODE_NUMPAD_0) + VK_NUMPAD0;
}
else if (keycode >= KeyEvent.KEYCODE_F1 &&
keycode <= KeyEvent.KEYCODE_F12) {
translated = (keycode - KeyEvent.KEYCODE_F1) + VK_F1;
}
else {
switch (keycode) {
case KeyEvent.KEYCODE_ALT_LEFT:
case KeyEvent.KEYCODE_ALT_RIGHT:
translated = VK_ALT;
break;
case KeyEvent.KEYCODE_BACKSLASH:
translated = 0xdc;
break;
case KeyEvent.KEYCODE_CAPS_LOCK:
translated = VK_CAPS_LOCK;
break;
case KeyEvent.KEYCODE_CLEAR:
translated = VK_CLEAR;
break;
case KeyEvent.KEYCODE_COMMA:
translated = 0xbc;
break;
case KeyEvent.KEYCODE_CTRL_LEFT:
case KeyEvent.KEYCODE_CTRL_RIGHT:
translated = VK_CONTROL;
break;
case KeyEvent.KEYCODE_DEL:
translated = VK_BACK_SPACE;
break;
case KeyEvent.KEYCODE_ENTER:
translated = 0x0d;
break;
case KeyEvent.KEYCODE_EQUALS:
translated = 0xbb;
break;
case KeyEvent.KEYCODE_ESCAPE:
translated = VK_ESCAPE;
break;
case KeyEvent.KEYCODE_FORWARD_DEL:
// Nvidia maps period to delete
translated = VK_PERIOD;
break;
case KeyEvent.KEYCODE_INSERT:
translated = -1;
break;
case KeyEvent.KEYCODE_LEFT_BRACKET:
translated = 0xdb;
break;
case KeyEvent.KEYCODE_META_LEFT:
case KeyEvent.KEYCODE_META_RIGHT:
translated = VK_WINDOWS;
break;
case KeyEvent.KEYCODE_MINUS:
translated = 0xbd;
break;
case KeyEvent.KEYCODE_MOVE_END:
translated = VK_END;
break;
case KeyEvent.KEYCODE_MOVE_HOME:
translated = VK_HOME;
break;
case KeyEvent.KEYCODE_NUM_LOCK:
translated = VK_NUM_LOCK;
break;
case KeyEvent.KEYCODE_PAGE_DOWN:
translated = VK_PAGE_DOWN;
break;
case KeyEvent.KEYCODE_PAGE_UP:
translated = VK_PAGE_UP;
break;
case KeyEvent.KEYCODE_PERIOD:
translated = 0xbe;
break;
case KeyEvent.KEYCODE_RIGHT_BRACKET:
translated = 0xdd;
break;
case KeyEvent.KEYCODE_SCROLL_LOCK:
translated = VK_SCROLL_LOCK;
break;
case KeyEvent.KEYCODE_SEMICOLON:
translated = 0xba;
break;
case KeyEvent.KEYCODE_SHIFT_LEFT:
case KeyEvent.KEYCODE_SHIFT_RIGHT:
translated = VK_SHIFT;
break;
case KeyEvent.KEYCODE_SLASH:
translated = 0xbf;
break;
case KeyEvent.KEYCODE_SPACE:
translated = VK_SPACE;
break;
case KeyEvent.KEYCODE_SYSRQ:
// Android defines this as SysRq/PrntScrn
translated = VK_PRINTSCREEN;
break;
case KeyEvent.KEYCODE_TAB:
translated = VK_TAB;
break;
case KeyEvent.KEYCODE_DPAD_LEFT:
translated = VK_LEFT;
break;
case KeyEvent.KEYCODE_DPAD_RIGHT:
translated = VK_RIGHT;
break;
case KeyEvent.KEYCODE_DPAD_UP:
translated = VK_UP;
break;
case KeyEvent.KEYCODE_DPAD_DOWN:
translated = VK_DOWN;
break;
case KeyEvent.KEYCODE_GRAVE:
translated = VK_BACK_QUOTE;
break;
case KeyEvent.KEYCODE_APOSTROPHE:
translated = 0xde;
break;
case KeyEvent.KEYCODE_BREAK:
translated = VK_PAUSE;
break;
default:
System.out.println("No key for "+keycode);
return 0;
}
}
return (short) ((KEY_PREFIX << 8) | translated);
}
public static final int VK_CLEAR = 12;
public static final int VK_COMMA = 44;
public static final int VK_BACK_SPACE = 8;
public static final int VK_EQUALS = 61;
public static final int VK_ESCAPE = 27;
public static final int VK_F1 = 112;
public static final int VK_END = 35;
public static final int VK_HOME = 36;
public static final int VK_NUM_LOCK = 144;
public static final int VK_PAGE_UP = 33;
public static final int VK_PAGE_DOWN = 34;
public static final int VK_PLUS = 521;
public static final int VK_CLOSE_BRACKET = 93;
public static final int VK_SCROLL_LOCK = 145;
public static final int VK_SEMICOLON = 59;
public static final int VK_SLASH = 47;
public static final int VK_SPACE = 32;
public static final int VK_PRINTSCREEN = 154;
public static final int VK_TAB = 9;
public static final int VK_LEFT = 37;
public static final int VK_RIGHT = 39;
public static final int VK_UP = 38;
public static final int VK_DOWN = 40;
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
* @return a GFE keycode for the given keycode
*/
public static short translate(int keycode) {
int translated;
// This is a poor man's mapping between Android key codes
// and Windows VK_* codes. For all defined VK_ codes, see:
// https://msdn.microsoft.com/en-us/library/windows/desktop/dd375731(v=vs.85).aspx
if (keycode >= KeyEvent.KEYCODE_0 &&
keycode <= KeyEvent.KEYCODE_9) {
translated = (keycode - KeyEvent.KEYCODE_0) + VK_0;
}
else if (keycode >= KeyEvent.KEYCODE_A &&
keycode <= KeyEvent.KEYCODE_Z) {
translated = (keycode - KeyEvent.KEYCODE_A) + VK_A;
}
else if (keycode >= KeyEvent.KEYCODE_NUMPAD_0 &&
keycode <= KeyEvent.KEYCODE_NUMPAD_9) {
translated = (keycode - KeyEvent.KEYCODE_NUMPAD_0) + VK_NUMPAD0;
}
else if (keycode >= KeyEvent.KEYCODE_F1 &&
keycode <= KeyEvent.KEYCODE_F12) {
translated = (keycode - KeyEvent.KEYCODE_F1) + VK_F1;
}
else {
switch (keycode) {
case KeyEvent.KEYCODE_ALT_LEFT:
translated = 0xA4;
break;
case KeyEvent.KEYCODE_ALT_RIGHT:
translated = 0xA5;
break;
case KeyEvent.KEYCODE_BACKSLASH:
translated = 0xdc;
break;
case KeyEvent.KEYCODE_CAPS_LOCK:
translated = VK_CAPS_LOCK;
break;
case KeyEvent.KEYCODE_CLEAR:
translated = VK_CLEAR;
break;
case KeyEvent.KEYCODE_COMMA:
translated = 0xbc;
break;
case KeyEvent.KEYCODE_CTRL_LEFT:
translated = 0xA2;
break;
case KeyEvent.KEYCODE_CTRL_RIGHT:
translated = 0xA3;
break;
case KeyEvent.KEYCODE_DEL:
translated = VK_BACK_SPACE;
break;
case KeyEvent.KEYCODE_ENTER:
translated = 0x0d;
break;
case KeyEvent.KEYCODE_PLUS:
case KeyEvent.KEYCODE_EQUALS:
translated = 0xbb;
break;
case KeyEvent.KEYCODE_ESCAPE:
translated = VK_ESCAPE;
break;
case KeyEvent.KEYCODE_FORWARD_DEL:
translated = 0x2e;
break;
case KeyEvent.KEYCODE_INSERT:
translated = 0x2d;
break;
case KeyEvent.KEYCODE_LEFT_BRACKET:
translated = 0xdb;
break;
case KeyEvent.KEYCODE_META_LEFT:
translated = 0x5b;
break;
case KeyEvent.KEYCODE_META_RIGHT:
translated = 0x5c;
break;
case KeyEvent.KEYCODE_MINUS:
translated = 0xbd;
break;
case KeyEvent.KEYCODE_MOVE_END:
translated = VK_END;
break;
case KeyEvent.KEYCODE_MOVE_HOME:
translated = VK_HOME;
break;
case KeyEvent.KEYCODE_NUM_LOCK:
translated = VK_NUM_LOCK;
break;
case KeyEvent.KEYCODE_PAGE_DOWN:
translated = VK_PAGE_DOWN;
break;
case KeyEvent.KEYCODE_PAGE_UP:
translated = VK_PAGE_UP;
break;
case KeyEvent.KEYCODE_PERIOD:
translated = 0xbe;
break;
case KeyEvent.KEYCODE_RIGHT_BRACKET:
translated = 0xdd;
break;
case KeyEvent.KEYCODE_SCROLL_LOCK:
translated = VK_SCROLL_LOCK;
break;
case KeyEvent.KEYCODE_SEMICOLON:
translated = 0xba;
break;
case KeyEvent.KEYCODE_SHIFT_LEFT:
translated = 0xA0;
break;
case KeyEvent.KEYCODE_SHIFT_RIGHT:
translated = 0xA1;
break;
case KeyEvent.KEYCODE_SLASH:
translated = 0xbf;
break;
case KeyEvent.KEYCODE_SPACE:
translated = VK_SPACE;
break;
case KeyEvent.KEYCODE_SYSRQ:
// Android defines this as SysRq/PrntScrn
translated = VK_PRINTSCREEN;
break;
case KeyEvent.KEYCODE_TAB:
translated = VK_TAB;
break;
case KeyEvent.KEYCODE_DPAD_LEFT:
translated = VK_LEFT;
break;
case KeyEvent.KEYCODE_DPAD_RIGHT:
translated = VK_RIGHT;
break;
case KeyEvent.KEYCODE_DPAD_UP:
translated = VK_UP;
break;
case KeyEvent.KEYCODE_DPAD_DOWN:
translated = VK_DOWN;
break;
case KeyEvent.KEYCODE_GRAVE:
translated = VK_BACK_QUOTE;
break;
case KeyEvent.KEYCODE_APOSTROPHE:
translated = 0xde;
break;
case KeyEvent.KEYCODE_BREAK:
translated = VK_PAUSE;
break;
case KeyEvent.KEYCODE_NUMPAD_DIVIDE:
translated = 0x6F;
break;
case KeyEvent.KEYCODE_NUMPAD_MULTIPLY:
translated = 0x6A;
break;
case KeyEvent.KEYCODE_NUMPAD_SUBTRACT:
translated = 0x6D;
break;
case KeyEvent.KEYCODE_NUMPAD_ADD:
translated = 0x6B;
break;
case KeyEvent.KEYCODE_NUMPAD_DOT:
translated = 0x6E;
break;
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;
}
}
return (short) ((KEY_PREFIX << 8) | translated);
}
}

View File

@@ -1,120 +0,0 @@
package com.limelight.binding.input;
import com.limelight.nvstream.NvConnection;
import com.limelight.nvstream.input.MouseButtonPacket;
public class TouchContext {
private int lastTouchX = 0;
private int lastTouchY = 0;
private int originalTouchX = 0;
private int originalTouchY = 0;
private long originalTouchTime = 0;
private boolean cancelled;
private final NvConnection conn;
private final int actionIndex;
private final double xFactor;
private final double yFactor;
private static final int TAP_MOVEMENT_THRESHOLD = 10;
private static final int TAP_TIME_THRESHOLD = 250;
public TouchContext(NvConnection conn, int actionIndex, double xFactor, double yFactor)
{
this.conn = conn;
this.actionIndex = actionIndex;
this.xFactor = xFactor;
this.yFactor = yFactor;
}
public int getActionIndex()
{
return actionIndex;
}
private boolean isTap()
{
int xDelta = Math.abs(lastTouchX - originalTouchX);
int yDelta = Math.abs(lastTouchY - originalTouchY);
long timeDelta = System.currentTimeMillis() - originalTouchTime;
return xDelta <= TAP_MOVEMENT_THRESHOLD &&
yDelta <= TAP_MOVEMENT_THRESHOLD &&
timeDelta <= TAP_TIME_THRESHOLD;
}
private byte getMouseButtonIndex()
{
if (actionIndex == 1) {
return MouseButtonPacket.BUTTON_RIGHT;
}
else {
return MouseButtonPacket.BUTTON_LEFT;
}
}
public boolean touchDownEvent(int eventX, int eventY)
{
originalTouchX = lastTouchX = eventX;
originalTouchY = lastTouchY = eventY;
originalTouchTime = System.currentTimeMillis();
cancelled = false;
return true;
}
public void touchUpEvent(int eventX, int eventY)
{
if (cancelled) {
return;
}
if (isTap())
{
byte buttonIndex = getMouseButtonIndex();
// Lower the mouse button
conn.sendMouseButtonDown(buttonIndex);
// We need to sleep a bit here because some games
// do input detection by polling
try {
Thread.sleep(100);
} catch (InterruptedException ignored) {}
// Raise the mouse button
conn.sendMouseButtonUp(buttonIndex);
}
}
public boolean touchMoveEvent(int eventX, int eventY)
{
if (eventX != lastTouchX || eventY != lastTouchY)
{
// We only send moves for the primary touch point
if (actionIndex == 0) {
int deltaX = eventX - lastTouchX;
int deltaY = eventY - lastTouchY;
// Scale the deltas based on the factors passed to our constructor
deltaX = (int)Math.round((double)deltaX * xFactor);
deltaY = (int)Math.round((double)deltaY * yFactor);
conn.sendMouseMove((short)deltaX, (short)deltaY);
}
lastTouchX = eventX;
lastTouchY = eventY;
}
return true;
}
public void cancelTouch() {
cancelled = true;
}
public boolean isCancelled() {
return cancelled;
}
}

View File

@@ -0,0 +1,69 @@
package com.limelight.binding.input.capture;
import android.annotation.TargetApi;
import android.app.Activity;
import android.os.Build;
import android.view.InputDevice;
import android.view.MotionEvent;
import android.view.View;
// We extend AndroidPointerIconCaptureProvider because we want to also get the
// pointer icon hiding behavior over our stream view just in case pointer capture
// is unavailable on this system (ex: DeX, ChromeOS)
@TargetApi(Build.VERSION_CODES.O)
public class AndroidNativePointerCaptureProvider extends AndroidPointerIconCaptureProvider {
private View targetView;
public AndroidNativePointerCaptureProvider(Activity activity, View targetView) {
super(activity, targetView);
this.targetView = targetView;
}
public static boolean isCaptureProviderSupported() {
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.O;
}
@Override
public void enableCapture() {
super.enableCapture();
targetView.requestPointerCapture();
}
@Override
public void disableCapture() {
super.disableCapture();
targetView.releasePointerCapture();
}
@Override
public boolean eventHasRelativeMouseAxes(MotionEvent event) {
// SOURCE_MOUSE_RELATIVE is how SOURCE_MOUSE appears when our view has pointer capture.
// SOURCE_TOUCHPAD will have relative axes populated iff our view has pointer capture.
// See https://developer.android.com/reference/android/view/View#requestPointerCapture()
return event.getSource() == InputDevice.SOURCE_MOUSE_RELATIVE ||
(event.getSource() == InputDevice.SOURCE_TOUCHPAD && targetView.hasPointerCapture());
}
@Override
public float getRelativeAxisX(MotionEvent event) {
int axis = (event.getSource() == InputDevice.SOURCE_MOUSE_RELATIVE) ?
MotionEvent.AXIS_X : MotionEvent.AXIS_RELATIVE_X;
float x = event.getAxisValue(axis);
for (int i = 0; i < event.getHistorySize(); i++) {
x += event.getHistoricalAxisValue(axis, i);
}
return x;
}
@Override
public float getRelativeAxisY(MotionEvent event) {
int axis = (event.getSource() == InputDevice.SOURCE_MOUSE_RELATIVE) ?
MotionEvent.AXIS_Y : MotionEvent.AXIS_RELATIVE_Y;
float y = event.getAxisValue(axis);
for (int i = 0; i < event.getHistorySize(); i++) {
y += event.getHistoricalAxisValue(axis, i);
}
return y;
}
}

View File

@@ -0,0 +1,36 @@
package com.limelight.binding.input.capture;
import android.annotation.TargetApi;
import android.app.Activity;
import android.content.Context;
import android.os.Build;
import android.view.MotionEvent;
import android.view.PointerIcon;
import android.view.View;
@TargetApi(Build.VERSION_CODES.N)
public class AndroidPointerIconCaptureProvider extends InputCaptureProvider {
private View targetView;
private Context context;
public AndroidPointerIconCaptureProvider(Activity activity, View targetView) {
this.context = activity;
this.targetView = targetView;
}
public static boolean isCaptureProviderSupported() {
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.N;
}
@Override
public void enableCapture() {
super.enableCapture();
targetView.setPointerIcon(PointerIcon.getSystemIcon(context, PointerIcon.TYPE_NULL));
}
@Override
public void disableCapture() {
super.disableCapture();
targetView.setPointerIcon(null);
}
}

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,93 @@
package com.limelight.binding.input.capture;
import android.content.Context;
import android.hardware.input.InputManager;
import android.view.MotionEvent;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
// NVIDIA extended the Android input APIs with support for using an attached mouse in relative
// mode without having to grab the input device (which requires root). The data comes in the form
// of new AXIS_RELATIVE_X and AXIS_RELATIVE_Y constants in the mouse's MotionEvent objects and
// a new function, InputManager.setCursorVisibility(), that allows the cursor to be hidden.
//
// http://docs.nvidia.com/gameworks/index.html#technologies/mobile/game_controller_handling_mouse.htm
public class ShieldCaptureProvider extends InputCaptureProvider {
private static boolean nvExtensionSupported;
private static Method methodSetCursorVisibility;
private static int AXIS_RELATIVE_X;
private static int AXIS_RELATIVE_Y;
private Context context;
static {
try {
methodSetCursorVisibility = InputManager.class.getMethod("setCursorVisibility", boolean.class);
Field fieldRelX = MotionEvent.class.getField("AXIS_RELATIVE_X");
Field fieldRelY = MotionEvent.class.getField("AXIS_RELATIVE_Y");
AXIS_RELATIVE_X = (Integer) fieldRelX.get(null);
AXIS_RELATIVE_Y = (Integer) fieldRelY.get(null);
nvExtensionSupported = true;
} catch (Exception e) {
nvExtensionSupported = false;
}
}
public ShieldCaptureProvider(Context context) {
this.context = context;
}
public static boolean isCaptureProviderSupported() {
return nvExtensionSupported;
}
private boolean setCursorVisibility(boolean visible) {
try {
methodSetCursorVisibility.invoke(context.getSystemService(Context.INPUT_SERVICE), visible);
return true;
} catch (InvocationTargetException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
return false;
}
@Override
public void enableCapture() {
super.enableCapture();
setCursorVisibility(false);
}
@Override
public void disableCapture() {
super.disableCapture();
setCursorVisibility(true);
}
@Override
public boolean eventHasRelativeMouseAxes(MotionEvent event) {
// All mouse events should use relative axes, even if they are zero. This avoids triggering
// cursor jumps if we get an event with no associated motion, like ACTION_DOWN or ACTION_UP.
return event.getPointerCount() == 1 && event.getActionIndex() == 0 &&
event.getToolType(0) == MotionEvent.TOOL_TYPE_MOUSE;
}
@Override
public float getRelativeAxisX(MotionEvent event) {
return event.getAxisValue(AXIS_RELATIVE_X);
}
@Override
public float getRelativeAxisY(MotionEvent event) {
return event.getAxisValue(AXIS_RELATIVE_Y);
}
}

View File

@@ -0,0 +1,61 @@
package com.limelight.binding.input.driver;
public abstract class AbstractController {
private final int deviceId;
private final int vendorId;
private final int productId;
private UsbDriverListener listener;
protected short buttonFlags;
protected float leftTrigger, rightTrigger;
protected float rightStickX, rightStickY;
protected float leftStickX, leftStickY;
public int getControllerId() {
return deviceId;
}
public int getVendorId() {
return vendorId;
}
public int getProductId() {
return productId;
}
protected void setButtonFlag(int buttonFlag, int data) {
if (data != 0) {
buttonFlags |= buttonFlag;
}
else {
buttonFlags &= ~buttonFlag;
}
}
protected void reportInput() {
listener.reportControllerState(deviceId, buttonFlags, leftStickX, leftStickY,
rightStickX, rightStickY, leftTrigger, rightTrigger);
}
public abstract boolean start();
public abstract void stop();
public AbstractController(int deviceId, UsbDriverListener listener, int vendorId, int productId) {
this.deviceId = deviceId;
this.listener = listener;
this.vendorId = vendorId;
this.productId = productId;
}
public abstract void rumble(short lowFreqMotor, short highFreqMotor);
protected void notifyDeviceRemoved() {
listener.deviceRemoved(this);
}
protected void notifyDeviceAdded() {
listener.deviceAdded(this);
}
}

View File

@@ -0,0 +1,161 @@
package com.limelight.binding.input.driver;
import android.hardware.usb.UsbConstants;
import android.hardware.usb.UsbDevice;
import android.hardware.usb.UsbDeviceConnection;
import android.hardware.usb.UsbEndpoint;
import android.hardware.usb.UsbInterface;
import com.limelight.LimeLog;
import com.limelight.binding.video.MediaCodecHelper;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
public abstract class AbstractXboxController extends AbstractController {
protected final UsbDevice device;
protected final UsbDeviceConnection connection;
private Thread inputThread;
private boolean stopped;
protected UsbEndpoint inEndpt, outEndpt;
public AbstractXboxController(UsbDevice device, UsbDeviceConnection connection, int deviceId, UsbDriverListener listener) {
super(deviceId, listener, device.getVendorId(), device.getProductId());
this.device = device;
this.connection = connection;
}
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];
int res;
//
// There's no way that I can tell to determine if a device has failed
// or if the timeout has simply expired. We'll check how long the transfer
// took to fail and assume the device failed if it happened before the timeout
// expired.
//
do {
// Read the next input state packet
long lastMillis = MediaCodecHelper.getMonotonicMillis();
res = connection.bulkTransfer(inEndpt, buffer, buffer.length, 3000);
// If we get a zero length response, treat it as an error
if (res == 0) {
res = -1;
}
if (res == -1 && MediaCodecHelper.getMonotonicMillis() - lastMillis < 1000) {
LimeLog.warning("Detected device I/O error");
AbstractXboxController.this.stop();
break;
}
} while (res == -1 && !isInterrupted() && !stopped);
if (res == -1 || stopped) {
break;
}
if (handleRead(ByteBuffer.wrap(buffer, 0, res).order(ByteOrder.LITTLE_ENDIAN))) {
// Report input if handleRead() returns true
reportInput();
}
}
}
};
}
public boolean start() {
// Force claim all interfaces
for (int i = 0; i < device.getInterfaceCount(); i++) {
UsbInterface iface = device.getInterface(i);
if (!connection.claimInterface(iface, true)) {
LimeLog.warning("Failed to claim interfaces");
return false;
}
}
// Find the endpoints
UsbInterface iface = device.getInterface(0);
for (int i = 0; i < iface.getEndpointCount(); i++) {
UsbEndpoint endpt = iface.getEndpoint(i);
if (endpt.getDirection() == UsbConstants.USB_DIR_IN) {
if (inEndpt != null) {
LimeLog.warning("Found duplicate IN endpoint");
return false;
}
inEndpt = endpt;
}
else if (endpt.getDirection() == UsbConstants.USB_DIR_OUT) {
if (outEndpt != null) {
LimeLog.warning("Found duplicate OUT endpoint");
return false;
}
outEndpt = endpt;
}
}
// Make sure the required endpoints were present
if (inEndpt == null || outEndpt == null) {
LimeLog.warning("Missing required endpoint");
return false;
}
// Run the init function
if (!doInit()) {
return false;
}
// Start listening for controller input
inputThread = createInputThread();
inputThread.start();
return true;
}
public void stop() {
if (stopped) {
return;
}
stopped = true;
// Cancel any rumble effects
rumble((short)0, (short)0);
// Stop the input thread
if (inputThread != null) {
inputThread.interrupt();
inputThread = null;
}
// Close the USB connection
connection.close();
// Report the device removed
notifyDeviceRemoved();
}
protected abstract boolean handleRead(ByteBuffer buffer);
protected abstract boolean doInit();
}

View File

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

View File

@@ -0,0 +1,262 @@
package com.limelight.binding.input.driver;
import android.app.PendingIntent;
import android.app.Service;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.hardware.usb.UsbDevice;
import android.hardware.usb.UsbDeviceConnection;
import android.hardware.usb.UsbManager;
import android.os.Binder;
import android.os.Build;
import android.os.Handler;
import android.os.IBinder;
import android.view.InputDevice;
import android.widget.Toast;
import com.limelight.LimeLog;
import com.limelight.R;
import com.limelight.preferences.PreferenceConfiguration;
import java.util.ArrayList;
public class UsbDriverService extends Service implements UsbDriverListener {
private static final String ACTION_USB_PERMISSION =
"com.limelight.USB_PERMISSION";
private UsbManager usbManager;
private PreferenceConfiguration prefConfig;
private final UsbEventReceiver receiver = new UsbEventReceiver();
private final UsbDriverBinder binder = new UsbDriverBinder();
private final ArrayList<AbstractController> controllers = new ArrayList<>();
private UsbDriverListener listener;
private int nextDeviceId;
@Override
public void reportControllerState(int controllerId, short buttonFlags, float leftStickX, float leftStickY, float rightStickX, float rightStickY, float leftTrigger, float rightTrigger) {
// Call through to the client's listener
if (listener != null) {
listener.reportControllerState(controllerId, buttonFlags, leftStickX, leftStickY, rightStickX, rightStickY, leftTrigger, rightTrigger);
}
}
@Override
public void deviceRemoved(AbstractController controller) {
// Remove the the controller from our list (if not removed already)
controllers.remove(controller);
// Call through to the client's listener
if (listener != null) {
listener.deviceRemoved(controller);
}
}
@Override
public void deviceAdded(AbstractController controller) {
// Call through to the client's listener
if (listener != null) {
listener.deviceAdded(controller);
}
}
public class UsbEventReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
// Initial attachment broadcast
if (action.equals(UsbManager.ACTION_USB_DEVICE_ATTACHED)) {
final UsbDevice device = intent.getParcelableExtra(UsbManager.EXTRA_DEVICE);
// shouldClaimDevice() looks at the kernel's enumerated input
// devices to make its decision about whether to prompt to take
// control of the device. The kernel bringing up the input stack
// may race with this callback and cause us to prompt when the
// kernel is capable of running the device. Let's post a delayed
// message to process this state change to allow the kernel
// some time to bring up the stack.
new Handler().postDelayed(new Runnable() {
@Override
public void run() {
// Continue the state machine
handleUsbDeviceState(device);
}
}, 1000);
}
// Subsequent permission dialog completion intent
else if (action.equals(ACTION_USB_PERMISSION)) {
UsbDevice device = intent.getParcelableExtra(UsbManager.EXTRA_DEVICE);
// If we got this far, we've already found we're able to handle this device
if (intent.getBooleanExtra(UsbManager.EXTRA_PERMISSION_GRANTED, false)) {
handleUsbDeviceState(device);
}
}
}
}
public class UsbDriverBinder extends Binder {
public void setListener(UsbDriverListener listener) {
UsbDriverService.this.listener = listener;
// Report all controllerMap that already exist
if (listener != null) {
for (AbstractController controller : controllers) {
listener.deviceAdded(controller);
}
}
}
}
private void handleUsbDeviceState(UsbDevice device) {
// Are we able to operate it?
if (shouldClaimDevice(device, prefConfig.bindAllUsb)) {
// Do we have permission yet?
if (!usbManager.hasPermission(device)) {
// Let's ask for permission
try {
// This function is not documented as throwing any exceptions (denying access
// is indicated by calling the PendingIntent with a false result). However,
// Samsung Knox has some policies which block this request, but rather than
// just returning a false result or returning 0 enumerated devices,
// they throw an undocumented SecurityException from this call, crashing
// the whole app. :(
usbManager.requestPermission(device, PendingIntent.getBroadcast(UsbDriverService.this, 0, new Intent(ACTION_USB_PERMISSION), 0));
} catch (SecurityException e) {
Toast.makeText(this, this.getText(R.string.error_usb_prohibited), Toast.LENGTH_LONG).show();
}
return;
}
// Open the device
UsbDeviceConnection connection = usbManager.openDevice(device);
if (connection == null) {
LimeLog.warning("Unable to open USB device: "+device.getDeviceName());
return;
}
AbstractController controller;
if (XboxOneController.canClaimDevice(device)) {
controller = new XboxOneController(device, connection, nextDeviceId++, this);
}
else if (Xbox360Controller.canClaimDevice(device)) {
controller = new Xbox360Controller(device, connection, nextDeviceId++, this);
}
else {
// Unreachable
return;
}
// Start the controller
if (!controller.start()) {
connection.close();
return;
}
// Add this controller to the list
controllers.add(controller);
}
}
public static boolean isRecognizedInputDevice(UsbDevice device) {
// On KitKat and later, we can determine if this VID and PID combo
// matches an existing input device and defer to the built-in controller
// support in that case. Prior to KitKat, we'll always return true to be safe.
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
for (int id : InputDevice.getDeviceIds()) {
InputDevice inputDev = InputDevice.getDevice(id);
if (inputDev == null) {
// Device was removed while looping
continue;
}
if (inputDev.getVendorId() == device.getVendorId() &&
inputDev.getProductId() == device.getProductId()) {
return true;
}
}
return false;
}
else {
return true;
}
}
public static boolean kernelSupportsXboxOne() {
String kernelVersion = System.getProperty("os.version");
LimeLog.info("Kernel Version: "+kernelVersion);
if (kernelVersion == null) {
// We'll assume this is some newer version of Android
// that doesn't let you read the kernel version this way.
return true;
}
else if (kernelVersion.startsWith("2.") || kernelVersion.startsWith("3.")) {
// These are old kernels that definitely don't support Xbox One controllers properly
return false;
}
else if (kernelVersion.startsWith("4.4.") || kernelVersion.startsWith("4.9.")) {
// These aren't guaranteed to have backported kernel patches for proper Xbox One
// support (though some devices will).
return false;
}
else {
// The next AOSP common kernel is 4.14 which has working Xbox One controller support
return true;
}
}
public static boolean shouldClaimDevice(UsbDevice device, boolean claimAllAvailable) {
return ((!kernelSupportsXboxOne() || !isRecognizedInputDevice(device) || claimAllAvailable) && XboxOneController.canClaimDevice(device)) ||
((!isRecognizedInputDevice(device) || claimAllAvailable) && Xbox360Controller.canClaimDevice(device));
}
@Override
public void onCreate() {
this.usbManager = (UsbManager) getSystemService(Context.USB_SERVICE);
this.prefConfig = PreferenceConfiguration.readPreferences(this);
// Register for USB attach broadcasts and permission completions
IntentFilter filter = new IntentFilter();
filter.addAction(UsbManager.ACTION_USB_DEVICE_ATTACHED);
filter.addAction(ACTION_USB_PERMISSION);
registerReceiver(receiver, filter);
// Enumerate existing devices
for (UsbDevice dev : usbManager.getDeviceList().values()) {
if (shouldClaimDevice(dev, prefConfig.bindAllUsb)) {
// Start the process of claiming this device
handleUsbDeviceState(dev);
}
}
}
@Override
public void onDestroy() {
// Stop the attachment receiver
unregisterReceiver(receiver);
// Remove listeners
listener = null;
// Stop all controllers
while (controllers.size() > 0) {
// Stop and remove the controller
controllers.remove(0).stop();
}
}
@Override
public IBinder onBind(Intent intent) {
return binder;
}
}

View File

@@ -0,0 +1,159 @@
package com.limelight.binding.input.driver;
import android.hardware.usb.UsbConstants;
import android.hardware.usb.UsbDevice;
import android.hardware.usb.UsbDeviceConnection;
import com.limelight.LimeLog;
import com.limelight.nvstream.input.ControllerPacket;
import java.nio.ByteBuffer;
public class Xbox360Controller extends AbstractXboxController {
private static final int XB360_IFACE_SUBCLASS = 93;
private static final int XB360_IFACE_PROTOCOL = 1; // Wired only
private static final int[] SUPPORTED_VENDORS = {
0x0079, // GPD Win 2
0x044f, // Thrustmaster
0x045e, // Microsoft
0x046d, // Logitech
0x056e, // Elecom
0x06a3, // Saitek
0x0738, // Mad Catz
0x07ff, // Mad Catz
0x0e6f, // Unknown
0x0f0d, // Hori
0x1038, // SteelSeries
0x11c9, // Nacon
0x1209, // Ardwiino
0x12ab, // Unknown
0x1430, // RedOctane
0x146b, // BigBen
0x1532, // Razer Sabertooth
0x15e4, // Numark
0x162e, // Joytech
0x1689, // Razer Onza
0x1949, // Lab126 (Amazon Luna)
0x1bad, // Harmonix
0x20d6, // PowerA
0x24c6, // PowerA
0x2f24, // GameSir
};
public static boolean canClaimDevice(UsbDevice device) {
for (int supportedVid : SUPPORTED_VENDORS) {
if (device.getVendorId() == supportedVid &&
device.getInterfaceCount() >= 1 &&
device.getInterface(0).getInterfaceClass() == UsbConstants.USB_CLASS_VENDOR_SPEC &&
device.getInterface(0).getInterfaceSubclass() == XB360_IFACE_SUBCLASS &&
device.getInterface(0).getInterfaceProtocol() == XB360_IFACE_PROTOCOL) {
return true;
}
}
return false;
}
public Xbox360Controller(UsbDevice device, UsbDeviceConnection connection, int deviceId, UsbDriverListener listener) {
super(device, connection, deviceId, listener);
}
private int unsignByte(byte b) {
if (b < 0) {
return b + 256;
}
else {
return b;
}
}
@Override
protected boolean handleRead(ByteBuffer buffer) {
if (buffer.remaining() < 14) {
LimeLog.severe("Read too small: "+buffer.remaining());
return false;
}
// Skip first short
buffer.position(buffer.position() + 2);
// DPAD
byte b = buffer.get();
setButtonFlag(ControllerPacket.LEFT_FLAG, b & 0x04);
setButtonFlag(ControllerPacket.RIGHT_FLAG, b & 0x08);
setButtonFlag(ControllerPacket.UP_FLAG, b & 0x01);
setButtonFlag(ControllerPacket.DOWN_FLAG, b & 0x02);
// Start/Select
setButtonFlag(ControllerPacket.PLAY_FLAG, b & 0x10);
setButtonFlag(ControllerPacket.BACK_FLAG, b & 0x20);
// LS/RS
setButtonFlag(ControllerPacket.LS_CLK_FLAG, b & 0x40);
setButtonFlag(ControllerPacket.RS_CLK_FLAG, b & 0x80);
// ABXY buttons
b = buffer.get();
setButtonFlag(ControllerPacket.A_FLAG, b & 0x10);
setButtonFlag(ControllerPacket.B_FLAG, b & 0x20);
setButtonFlag(ControllerPacket.X_FLAG, b & 0x40);
setButtonFlag(ControllerPacket.Y_FLAG, b & 0x80);
// LB/RB
setButtonFlag(ControllerPacket.LB_FLAG, b & 0x01);
setButtonFlag(ControllerPacket.RB_FLAG, b & 0x02);
// Xbox button
setButtonFlag(ControllerPacket.SPECIAL_BUTTON_FLAG, b & 0x04);
// Triggers
leftTrigger = unsignByte(buffer.get()) / 255.0f;
rightTrigger = unsignByte(buffer.get()) / 255.0f;
// Left stick
leftStickX = buffer.getShort() / 32767.0f;
leftStickY = ~buffer.getShort() / 32767.0f;
// Right stick
rightStickX = buffer.getShort() / 32767.0f;
rightStickY = ~buffer.getShort() / 32767.0f;
// Return true to send input
return true;
}
private boolean sendLedCommand(byte command) {
byte[] commandBuffer = {0x01, 0x03, command};
int res = connection.bulkTransfer(outEndpt, commandBuffer, commandBuffer.length, 3000);
if (res != commandBuffer.length) {
LimeLog.warning("LED set transfer failed: "+res);
return false;
}
return true;
}
@Override
protected boolean doInit() {
// Turn the LED on corresponding to our device ID
sendLedCommand((byte)(2 + (getControllerId() % 4)));
// No need to fail init if the LED command fails
return true;
}
@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);
}
}
}

View File

@@ -0,0 +1,204 @@
package com.limelight.binding.input.driver;
import android.hardware.usb.UsbConstants;
import android.hardware.usb.UsbDevice;
import android.hardware.usb.UsbDeviceConnection;
import com.limelight.LimeLog;
import com.limelight.nvstream.input.ControllerPacket;
import java.nio.ByteBuffer;
import java.util.Arrays;
public class XboxOneController extends AbstractXboxController {
private static final int XB1_IFACE_SUBCLASS = 71;
private static final int XB1_IFACE_PROTOCOL = 208;
private static final int[] SUPPORTED_VENDORS = {
0x045e, // Microsoft
0x0738, // Mad Catz
0x0e6f, // Unknown
0x0f0d, // Hori
0x1532, // Razer Wildcat
0x20d6, // PowerA
0x24c6, // PowerA
0x2e24, // Hyperkin
};
private static final byte[] FW2015_INIT = {0x05, 0x20, 0x00, 0x01, 0x00};
private static final byte[] ONE_S_INIT = {0x05, 0x20, 0x00, 0x0f, 0x06};
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(0x045e, 0x02ea, ONE_S_INIT),
new InitPacket(0x045e, 0x0b00, ONE_S_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);
}
private void processButtons(ByteBuffer buffer) {
byte b = buffer.get();
setButtonFlag(ControllerPacket.PLAY_FLAG, b & 0x04);
setButtonFlag(ControllerPacket.BACK_FLAG, b & 0x08);
setButtonFlag(ControllerPacket.A_FLAG, b & 0x10);
setButtonFlag(ControllerPacket.B_FLAG, b & 0x20);
setButtonFlag(ControllerPacket.X_FLAG, b & 0x40);
setButtonFlag(ControllerPacket.Y_FLAG, b & 0x80);
b = buffer.get();
setButtonFlag(ControllerPacket.LEFT_FLAG, b & 0x04);
setButtonFlag(ControllerPacket.RIGHT_FLAG, b & 0x08);
setButtonFlag(ControllerPacket.UP_FLAG, b & 0x01);
setButtonFlag(ControllerPacket.DOWN_FLAG, b & 0x02);
setButtonFlag(ControllerPacket.LB_FLAG, b & 0x10);
setButtonFlag(ControllerPacket.RB_FLAG, b & 0x20);
setButtonFlag(ControllerPacket.LS_CLK_FLAG, b & 0x40);
setButtonFlag(ControllerPacket.RS_CLK_FLAG, b & 0x80);
leftTrigger = buffer.getShort() / 1023.0f;
rightTrigger = buffer.getShort() / 1023.0f;
leftStickX = buffer.getShort() / 32767.0f;
leftStickY = ~buffer.getShort() / 32767.0f;
rightStickX = buffer.getShort() / 32767.0f;
rightStickY = ~buffer.getShort() / 32767.0f;
}
private void ackModeReport(byte seqNum) {
byte[] payload = {0x01, 0x20, seqNum, 0x09, 0x00, 0x07, 0x20, 0x02,
0x00, 0x00, 0x00, 0x00, 0x00};
connection.bulkTransfer(outEndpt, payload, payload.length, 3000);
}
@Override
protected boolean handleRead(ByteBuffer buffer) {
switch (buffer.get())
{
case 0x20:
if (buffer.remaining() < 17) {
LimeLog.severe("XBone button/axis read too small: "+buffer.remaining());
return false;
}
buffer.position(buffer.position()+3);
processButtons(buffer);
return true;
case 0x07:
if (buffer.remaining() < 4) {
LimeLog.severe("XBone mode read too small: "+buffer.remaining());
return false;
}
// The Xbox One S controller needs acks for mode reports otherwise
// it retransmits them forever.
if (buffer.get() == 0x30) {
ackModeReport(buffer.get());
buffer.position(buffer.position() + 1);
}
else {
buffer.position(buffer.position() + 2);
}
setButtonFlag(ControllerPacket.SPECIAL_BUTTON_FLAG, buffer.get() & 0x01);
return true;
}
return false;
}
public static boolean canClaimDevice(UsbDevice device) {
for (int supportedVid : SUPPORTED_VENDORS) {
if (device.getVendorId() == supportedVid &&
device.getInterfaceCount() >= 1 &&
device.getInterface(0).getInterfaceClass() == UsbConstants.USB_CLASS_VENDOR_SPEC &&
device.getInterface(0).getInterfaceSubclass() == XB1_IFACE_SUBCLASS &&
device.getInterface(0).getInterfaceProtocol() == XB1_IFACE_PROTOCOL) {
return true;
}
}
return false;
}
@Override
protected boolean doInit() {
// Send 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;
}
}
}

View File

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

View File

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

View File

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

View File

@@ -1,105 +0,0 @@
package com.limelight.binding.input.evdev;
import android.os.Build;
import java.nio.ByteBuffer;
import java.util.Locale;
import com.limelight.LimeLog;
public class EvdevReader {
static {
System.loadLibrary("evdev_reader");
}
public static void patchSeLinuxPolicies() {
//
// FIXME: We REALLY shouldn't being changing permissions on the input devices like this.
// We should probably do something clever with a separate daemon and talk via a localhost
// socket. We don't return the SELinux policies back to default after we're done which I feel
// bad about, but we do chmod the input devices back so I don't think any additional attack surface
// remains opened after streaming other than listing the /dev/input directory which you wouldn't
// normally be able to do with SELinux enforcing on Lollipop.
//
// We need to modify SELinux policies to allow us to capture input devices on Lollipop and possibly other
// more restrictive ROMs. Per Chainfire's SuperSU documentation, the supolicy binary is provided on
// 4.4 and later to do live SELinux policy changes.
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
EvdevShell shell = EvdevShell.getInstance();
shell.runCommand("supolicy --live \"allow untrusted_app input_device dir { open getattr read search }\" " +
"\"allow untrusted_app input_device chr_file { open read write ioctl }\"");
}
}
// Requires root to chmod /dev/input/eventX
public static void setPermissions(String[] files, int octalPermissions) {
EvdevShell shell = EvdevShell.getInstance();
for (String file : files) {
shell.runCommand(String.format((Locale)null, "chmod %o %s", octalPermissions, file));
}
}
// Returns the fd to be passed to other function or -1 on error
public static native int open(String fileName);
// Prevent other apps (including Android itself) from using the device while "grabbed"
public static native boolean grab(int fd);
public static native boolean ungrab(int fd);
// Used for checking device capabilities
public static native boolean hasRelAxis(int fd, short axis);
public static native boolean hasAbsAxis(int fd, short axis);
public static native boolean hasKey(int fd, short key);
public static boolean isMouse(int fd) {
// This is the same check that Android does in EventHub.cpp
return hasRelAxis(fd, EvdevEvent.REL_X) &&
hasRelAxis(fd, EvdevEvent.REL_Y) &&
hasKey(fd, EvdevEvent.BTN_LEFT);
}
public static boolean isAlphaKeyboard(int fd) {
// This is the same check that Android does in EventHub.cpp
return hasKey(fd, EvdevEvent.KEY_Q);
}
public static boolean isGamepad(int fd) {
return hasKey(fd, EvdevEvent.BTN_GAMEPAD);
}
// Returns the bytes read or -1 on error
private static native int read(int fd, byte[] buffer);
// Takes a byte buffer to use to read the output into.
// This buffer MUST be in native byte order and at least
// EVDEV_MAX_EVENT_SIZE bytes long.
public static EvdevEvent read(int fd, ByteBuffer buffer) {
int bytesRead = read(fd, buffer.array());
if (bytesRead < 0) {
LimeLog.warning("Failed to read: "+bytesRead);
return null;
}
else if (bytesRead < EvdevEvent.EVDEV_MIN_EVENT_SIZE) {
LimeLog.warning("Short read: "+bytesRead);
return null;
}
buffer.limit(bytesRead);
buffer.rewind();
// Throw away the time stamp
if (bytesRead == EvdevEvent.EVDEV_MAX_EVENT_SIZE) {
buffer.getLong();
buffer.getLong();
} else {
buffer.getInt();
buffer.getInt();
}
return new EvdevEvent(buffer.getShort(), buffer.getShort(), buffer.getInt());
}
// Closes the fd from open()
public static native int close(int fd);
}

View File

@@ -1,116 +0,0 @@
package com.limelight.binding.input.evdev;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Scanner;
import java.util.UUID;
public class EvdevShell {
private OutputStream stdin;
private InputStream stdout;
private Process shell;
private final String uuidString = UUID.randomUUID().toString();
private static final EvdevShell globalShell = new EvdevShell();
public static EvdevShell getInstance() {
return globalShell;
}
public void startShell() {
ProcessBuilder builder = new ProcessBuilder("su");
try {
// Redirect stderr to stdout
builder.redirectErrorStream(true);
shell = builder.start();
stdin = shell.getOutputStream();
stdout = shell.getInputStream();
} catch (IOException e) {
// This is unexpected
e.printStackTrace();
// Kill the shell if it spawned
if (stdin != null) {
try {
stdin.close();
} catch (IOException e1) {
e1.printStackTrace();
} finally {
stdin = null;
}
}
if (stdout != null) {
try {
stdout.close();
} catch (IOException e1) {
e1.printStackTrace();
} finally {
stdout = null;
}
}
if (shell != null) {
shell.destroy();
shell = null;
}
}
}
public void runCommand(String command) {
if (shell == null) {
// Shell never started
return;
}
try {
// Write the command followed by an echo with our UUID
stdin.write((command+'\n').getBytes("UTF-8"));
stdin.write(("echo "+uuidString+'\n').getBytes("UTF-8"));
stdin.flush();
// This is the only command in flight so we can use a scanner
// without worrying about it eating too many characters
Scanner scanner = new Scanner(stdout);
while (scanner.hasNext()) {
if (scanner.next().contains(uuidString)) {
// Our command ran
return;
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
public void stopShell() throws InterruptedException {
boolean exitWritten = false;
if (shell == null) {
// Shell never started
return;
}
try {
stdin.write("exit\n".getBytes("UTF-8"));
exitWritten = true;
} catch (IOException e) {
// We'll destroy the process without
// waiting for it to terminate since
// we don't know whether our exit command made it
e.printStackTrace();
}
if (exitWritten) {
try {
shell.waitFor();
} finally {
shell.destroy();
}
}
else {
shell.destroy();
}
}
}

View File

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

View File

@@ -0,0 +1,272 @@
package com.limelight.binding.input.touch;
import android.os.SystemClock;
import android.view.View;
import com.limelight.nvstream.NvConnection;
import com.limelight.nvstream.input.MouseButtonPacket;
import java.util.Timer;
import java.util.TimerTask;
public class AbsoluteTouchContext implements TouchContext {
private int lastTouchDownX = 0;
private int lastTouchDownY = 0;
private long lastTouchDownTime = 0;
private int lastTouchUpX = 0;
private int lastTouchUpY = 0;
private long lastTouchUpTime = 0;
private int lastTouchLocationX = 0;
private int lastTouchLocationY = 0;
private boolean cancelled;
private boolean confirmedLongPress;
private boolean confirmedTap;
private Timer longPressTimer;
private Timer tapDownTimer;
private float accumulatedScrollDelta;
private final NvConnection conn;
private final int actionIndex;
private final View targetView;
private static final int SCROLL_SPEED_DIVISOR = 20;
private static final int LONG_PRESS_TIME_THRESHOLD = 650;
private static final int LONG_PRESS_DISTANCE_THRESHOLD = 30;
private static final int DOUBLE_TAP_TIME_THRESHOLD = 250;
private static final int DOUBLE_TAP_DISTANCE_THRESHOLD = 60;
private static final int TOUCH_DOWN_DEAD_ZONE_TIME_THRESHOLD = 100;
private static final int TOUCH_DOWN_DEAD_ZONE_DISTANCE_THRESHOLD = 20;
public AbsoluteTouchContext(NvConnection conn, int actionIndex, View view)
{
this.conn = conn;
this.actionIndex = actionIndex;
this.targetView = view;
}
@Override
public int getActionIndex()
{
return actionIndex;
}
@Override
public boolean touchDownEvent(int eventX, int eventY, boolean isNewFinger)
{
if (!isNewFinger) {
// We don't handle finger transitions for absolute mode
return true;
}
lastTouchLocationX = lastTouchDownX = eventX;
lastTouchLocationY = lastTouchDownY = eventY;
lastTouchDownTime = SystemClock.uptimeMillis();
cancelled = confirmedTap = confirmedLongPress = false;
accumulatedScrollDelta = 0;
if (actionIndex == 0) {
// Start the timers
startTapDownTimer();
startLongPressTimer();
}
return true;
}
private boolean distanceExceeds(int deltaX, int deltaY, double limit) {
return Math.sqrt(Math.pow(deltaX, 2) + Math.pow(deltaY, 2)) > limit;
}
private void updatePosition(int eventX, int eventY) {
// We may get values slightly outside our view region on ACTION_HOVER_ENTER and ACTION_HOVER_EXIT.
// Normalize these to the view size. We can't just drop them because we won't always get an event
// right at the boundary of the view, so dropping them would result in our cursor never really
// reaching the sides of the screen.
eventX = Math.min(Math.max(eventX, 0), targetView.getWidth());
eventY = Math.min(Math.max(eventY, 0), targetView.getHeight());
conn.sendMousePosition((short)eventX, (short)eventY, (short)targetView.getWidth(), (short)targetView.getHeight());
}
@Override
public void touchUpEvent(int eventX, int eventY)
{
if (cancelled) {
return;
}
if (actionIndex == 0) {
// Cancel the timers
cancelLongPressTimer();
cancelTapDownTimer();
// Raise the mouse buttons that we currently have down
if (confirmedLongPress) {
conn.sendMouseButtonUp(MouseButtonPacket.BUTTON_RIGHT);
}
else if (confirmedTap) {
conn.sendMouseButtonUp(MouseButtonPacket.BUTTON_LEFT);
}
else {
// If we get here, this means that the tap completed within the touch down
// deadzone time. We'll need to send the touch down and up events now at the
// original touch down position.
tapConfirmed();
try {
// FIXME: Sleeping on the main thread sucks
Thread.sleep(50);
} catch (InterruptedException ignored) {}
conn.sendMouseButtonUp(MouseButtonPacket.BUTTON_LEFT);
}
}
lastTouchLocationX = lastTouchUpX = eventX;
lastTouchLocationY = lastTouchUpY = eventY;
lastTouchUpTime = SystemClock.uptimeMillis();
}
private synchronized void startLongPressTimer() {
longPressTimer = new Timer(true);
longPressTimer.schedule(new TimerTask() {
@Override
public void run() {
synchronized (AbsoluteTouchContext.this) {
// Check if someone cancelled us
if (longPressTimer == null) {
return;
}
// Uncancellable now
longPressTimer = null;
// This timer should have already expired, but cancel it just in case
cancelTapDownTimer();
// Switch from a left click to a right click after a long press
confirmedLongPress = true;
if (confirmedTap) {
conn.sendMouseButtonUp(MouseButtonPacket.BUTTON_LEFT);
}
conn.sendMouseButtonDown(MouseButtonPacket.BUTTON_RIGHT);
}
}
}, LONG_PRESS_TIME_THRESHOLD);
}
private synchronized void cancelLongPressTimer() {
if (longPressTimer != null) {
longPressTimer.cancel();
longPressTimer = null;
}
}
private synchronized void startTapDownTimer() {
tapDownTimer = new Timer(true);
tapDownTimer.schedule(new TimerTask() {
@Override
public void run() {
synchronized (AbsoluteTouchContext.this) {
// Check if someone cancelled us
if (tapDownTimer == null) {
return;
}
// Uncancellable now
tapDownTimer = null;
// Start our tap
tapConfirmed();
}
}
}, TOUCH_DOWN_DEAD_ZONE_TIME_THRESHOLD);
}
private synchronized void cancelTapDownTimer() {
if (tapDownTimer != null) {
tapDownTimer.cancel();
tapDownTimer = null;
}
}
private void tapConfirmed() {
if (confirmedTap || confirmedLongPress) {
return;
}
confirmedTap = true;
cancelTapDownTimer();
// Left button down at original position
if (lastTouchDownTime - lastTouchUpTime > DOUBLE_TAP_TIME_THRESHOLD ||
distanceExceeds(lastTouchDownX - lastTouchUpX, lastTouchDownY - lastTouchUpY, DOUBLE_TAP_DISTANCE_THRESHOLD)) {
// Don't reposition for finger down events within the deadzone. This makes double-clicking easier.
updatePosition(lastTouchDownX, lastTouchDownY);
}
conn.sendMouseButtonDown(MouseButtonPacket.BUTTON_LEFT);
}
@Override
public boolean touchMoveEvent(int eventX, int eventY)
{
if (cancelled) {
return true;
}
if (actionIndex == 0) {
if (distanceExceeds(eventX - lastTouchDownX, eventY - lastTouchDownY, LONG_PRESS_DISTANCE_THRESHOLD)) {
// Moved too far since touch down. Cancel the long press timer.
cancelLongPressTimer();
}
// Ignore motion within the deadzone period after touch down
if (confirmedTap || distanceExceeds(eventX - lastTouchDownX, eventY - lastTouchDownY, TOUCH_DOWN_DEAD_ZONE_DISTANCE_THRESHOLD)) {
tapConfirmed();
updatePosition(eventX, eventY);
}
}
else if (actionIndex == 1) {
accumulatedScrollDelta += (eventY - lastTouchLocationY) / (float)SCROLL_SPEED_DIVISOR;
if ((short)accumulatedScrollDelta != 0) {
conn.sendMouseHighResScroll((short)accumulatedScrollDelta);
accumulatedScrollDelta -= (short)accumulatedScrollDelta;
}
}
lastTouchLocationX = eventX;
lastTouchLocationY = eventY;
return true;
}
@Override
public void cancelTouch() {
cancelled = true;
// Cancel the timers
cancelLongPressTimer();
cancelTapDownTimer();
// Raise the mouse buttons
if (confirmedLongPress) {
conn.sendMouseButtonUp(MouseButtonPacket.BUTTON_RIGHT);
}
else if (confirmedTap) {
conn.sendMouseButtonUp(MouseButtonPacket.BUTTON_LEFT);
}
}
@Override
public boolean isCancelled() {
return cancelled;
}
@Override
public void setPointerCount(int pointerCount) {
if (actionIndex == 0 && pointerCount > 1) {
cancelTouch();
}
}
}

View File

@@ -0,0 +1,299 @@
package com.limelight.binding.input.touch;
import android.os.SystemClock;
import android.view.View;
import com.limelight.nvstream.NvConnection;
import com.limelight.nvstream.input.MouseButtonPacket;
import java.util.Timer;
import java.util.TimerTask;
public class RelativeTouchContext implements TouchContext {
private int lastTouchX = 0;
private int lastTouchY = 0;
private int originalTouchX = 0;
private int originalTouchY = 0;
private long originalTouchTime = 0;
private boolean cancelled;
private boolean confirmedMove;
private boolean confirmedDrag;
private boolean confirmedScroll;
private Timer dragTimer;
private double distanceMoved;
private double xFactor, yFactor;
private int pointerCount;
private int maxPointerCountInGesture;
private final NvConnection conn;
private final int actionIndex;
private final int referenceWidth;
private final int referenceHeight;
private final View targetView;
private static final int TAP_MOVEMENT_THRESHOLD = 20;
private static final int TAP_DISTANCE_THRESHOLD = 25;
private static final int TAP_TIME_THRESHOLD = 250;
private static final int DRAG_TIME_THRESHOLD = 650;
private static final int SCROLL_SPEED_DIVISOR = 20;
public RelativeTouchContext(NvConnection conn, int actionIndex,
int referenceWidth, int referenceHeight, View view)
{
this.conn = conn;
this.actionIndex = actionIndex;
this.referenceWidth = referenceWidth;
this.referenceHeight = referenceHeight;
this.targetView = view;
}
@Override
public int getActionIndex()
{
return actionIndex;
}
private boolean isWithinTapBounds(int touchX, int touchY)
{
int xDelta = Math.abs(touchX - originalTouchX);
int yDelta = Math.abs(touchY - originalTouchY);
return xDelta <= TAP_MOVEMENT_THRESHOLD &&
yDelta <= TAP_MOVEMENT_THRESHOLD;
}
private boolean isTap()
{
if (confirmedDrag || confirmedMove || confirmedScroll) {
return false;
}
// If this input wasn't the last finger down, do not report
// a tap. This ensures we don't report duplicate taps for each
// finger on a multi-finger tap gesture
if (actionIndex + 1 != maxPointerCountInGesture) {
return false;
}
long timeDelta = SystemClock.uptimeMillis() - originalTouchTime;
return isWithinTapBounds(lastTouchX, lastTouchY) && timeDelta <= TAP_TIME_THRESHOLD;
}
private byte getMouseButtonIndex()
{
if (actionIndex == 1) {
return MouseButtonPacket.BUTTON_RIGHT;
}
else {
return MouseButtonPacket.BUTTON_LEFT;
}
}
@Override
public boolean touchDownEvent(int eventX, int eventY, boolean isNewFinger)
{
// Get the view dimensions to scale inputs on this touch
xFactor = referenceWidth / (double)targetView.getWidth();
yFactor = referenceHeight / (double)targetView.getHeight();
originalTouchX = lastTouchX = eventX;
originalTouchY = lastTouchY = eventY;
if (isNewFinger) {
maxPointerCountInGesture = pointerCount;
originalTouchTime = SystemClock.uptimeMillis();
cancelled = confirmedDrag = confirmedMove = confirmedScroll = false;
distanceMoved = 0;
if (actionIndex == 0) {
// Start the timer for engaging a drag
startDragTimer();
}
}
return true;
}
@Override
public void touchUpEvent(int eventX, int eventY)
{
if (cancelled) {
return;
}
// Cancel the drag timer
cancelDragTimer();
byte buttonIndex = getMouseButtonIndex();
if (confirmedDrag) {
// Raise the button after a drag
conn.sendMouseButtonUp(buttonIndex);
}
else if (isTap())
{
// Lower the mouse button
conn.sendMouseButtonDown(buttonIndex);
// We need to sleep a bit here because some games
// do input detection by polling
try {
Thread.sleep(100);
} catch (InterruptedException ignored) {}
// Raise the mouse button
conn.sendMouseButtonUp(buttonIndex);
}
}
private synchronized void startDragTimer() {
// Cancel any existing drag timers
cancelDragTimer();
dragTimer = new Timer(true);
dragTimer.schedule(new TimerTask() {
@Override
public void run() {
synchronized (RelativeTouchContext.this) {
// Check if someone already set move
if (confirmedMove) {
return;
}
// Check if someone cancelled us
if (dragTimer == null) {
return;
}
// Uncancellable now
dragTimer = null;
// We haven't been cancelled before the timer expired so begin dragging
confirmedDrag = true;
conn.sendMouseButtonDown(getMouseButtonIndex());
}
}
}, DRAG_TIME_THRESHOLD);
}
private synchronized void cancelDragTimer() {
if (dragTimer != null) {
dragTimer.cancel();
dragTimer = null;
}
}
private synchronized void checkForConfirmedMove(int eventX, int eventY) {
// If we've already confirmed something, get out now
if (confirmedMove || confirmedDrag) {
return;
}
// If it leaves the tap bounds before the drag time expires, it's a move.
if (!isWithinTapBounds(eventX, eventY)) {
confirmedMove = true;
cancelDragTimer();
return;
}
// Check if we've exceeded the maximum distance moved
distanceMoved += Math.sqrt(Math.pow(eventX - lastTouchX, 2) + Math.pow(eventY - lastTouchY, 2));
if (distanceMoved >= TAP_DISTANCE_THRESHOLD) {
confirmedMove = true;
cancelDragTimer();
return;
}
}
private void checkForConfirmedScroll() {
// Enter scrolling mode if we've already left the tap zone
// and we have 2 fingers on screen. Leave scroll mode if
// we no longer have 2 fingers on screen
confirmedScroll = (actionIndex == 0 && pointerCount == 2 && confirmedMove);
}
@Override
public boolean touchMoveEvent(int eventX, int eventY)
{
if (cancelled) {
return true;
}
if (eventX != lastTouchX || eventY != lastTouchY)
{
checkForConfirmedMove(eventX, eventY);
checkForConfirmedScroll();
// We only send moves and drags for the primary touch point
if (actionIndex == 0) {
int deltaX = eventX - lastTouchX;
int deltaY = eventY - lastTouchY;
// Scale the deltas based on the factors passed to our constructor
deltaX = (int) Math.round((double) Math.abs(deltaX) * xFactor);
deltaY = (int) Math.round((double) Math.abs(deltaY) * yFactor);
// Fix up the signs
if (eventX < lastTouchX) {
deltaX = -deltaX;
}
if (eventY < lastTouchY) {
deltaY = -deltaY;
}
if (pointerCount == 2) {
if (confirmedScroll) {
deltaY /= SCROLL_SPEED_DIVISOR;
conn.sendMouseHighResScroll((short) deltaY);
}
} else {
conn.sendMouseMove((short) deltaX, (short) deltaY);
}
// If the scaling factor ended up rounding deltas to zero, wait until they are
// non-zero to update lastTouch that way devices that report small touch events often
// will work correctly
if (deltaX != 0) {
lastTouchX = eventX;
}
if (deltaY != 0) {
lastTouchY = eventY;
}
}
else {
lastTouchX = eventX;
lastTouchY = eventY;
}
}
return true;
}
@Override
public void cancelTouch() {
cancelled = true;
// Cancel the drag timer
cancelDragTimer();
// If it was a confirmed drag, we'll need to raise the button now
if (confirmedDrag) {
conn.sendMouseButtonUp(getMouseButtonIndex());
}
}
@Override
public boolean isCancelled() {
return cancelled;
}
@Override
public void setPointerCount(int pointerCount) {
this.pointerCount = pointerCount;
if (pointerCount > maxPointerCountInGesture) {
maxPointerCountInGesture = pointerCount;
}
}
}

View File

@@ -0,0 +1,11 @@
package com.limelight.binding.input.touch;
public interface TouchContext {
int getActionIndex();
void setPointerCount(int pointerCount);
boolean touchDownEvent(int eventX, int eventY, boolean isNewFinger);
boolean touchMoveEvent(int eventX, int eventY);
void touchUpEvent(int eventX, int eventY);
void cancelTouch();
boolean isCancelled();
}

View File

@@ -0,0 +1,351 @@
/**
* Created by Karim Mreisi.
*/
package com.limelight.binding.input.virtual_controller;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.os.SystemClock;
import android.view.MotionEvent;
import java.util.ArrayList;
import java.util.List;
/**
* This is a analog stick on screen element. It is used to get 2-Axis user input.
*/
public class AnalogStick extends VirtualControllerElement {
/**
* outer radius size in percent of the ui element
*/
public static final int SIZE_RADIUS_COMPLETE = 90;
/**
* analog stick size in percent of the ui element
*/
public static final int SIZE_RADIUS_ANALOG_STICK = 90;
/**
* dead zone size in percent of the ui element
*/
public static final int SIZE_RADIUS_DEADZONE = 90;
/**
* time frame for a double click
*/
public final static long timeoutDoubleClick = 350;
/**
* touch down time until the deadzone is lifted to allow precise movements with the analog sticks
*/
public final static long timeoutDeadzone = 150;
/**
* Listener interface to update registered observers.
*/
public interface AnalogStickListener {
/**
* onMovement event will be fired on real analog stick movement (outside of the deadzone).
*
* @param x horizontal position, value from -1.0 ... 0 .. 1.0
* @param y vertical position, value from -1.0 ... 0 .. 1.0
*/
void onMovement(float x, float y);
/**
* onClick event will be fired on click on the analog stick
*/
void onClick();
/**
* onDoubleClick event will be fired on a double click in a short time frame on the analog
* stick.
*/
void onDoubleClick();
/**
* onRevoke event will be fired on unpress of the analog stick.
*/
void onRevoke();
}
/**
* Movement states of the analog sick.
*/
private enum STICK_STATE {
NO_MOVEMENT,
MOVED_IN_DEAD_ZONE,
MOVED_ACTIVE
}
/**
* Click type states.
*/
private enum CLICK_STATE {
SINGLE,
DOUBLE
}
/**
* configuration if the analog stick should be displayed as circle or square
*/
private boolean circle_stick = true; // TODO: implement square sick for simulations
/**
* outer radius, this size will be automatically updated on resize
*/
private float radius_complete = 0;
/**
* analog stick radius, this size will be automatically updated on resize
*/
private float radius_analog_stick = 0;
/**
* dead zone radius, this size will be automatically updated on resize
*/
private float radius_dead_zone = 0;
/**
* horizontal position in relation to the center of the element
*/
private float relative_x = 0;
/**
* vertical position in relation to the center of the element
*/
private float relative_y = 0;
private double movement_radius = 0;
private double movement_angle = 0;
private float position_stick_x = 0;
private float position_stick_y = 0;
private final Paint paint = new Paint();
private STICK_STATE stick_state = STICK_STATE.NO_MOVEMENT;
private CLICK_STATE click_state = CLICK_STATE.SINGLE;
private List<AnalogStickListener> listeners = new ArrayList<>();
private long timeLastClick = 0;
private static double getMovementRadius(float x, float y) {
return Math.sqrt(x * x + y * y);
}
private static double getAngle(float way_x, float way_y) {
// prevent divisions by zero for corner cases
if (way_x == 0) {
return way_y < 0 ? Math.PI : 0;
} else if (way_y == 0) {
if (way_x > 0) {
return Math.PI * 3 / 2;
} else if (way_x < 0) {
return Math.PI * 1 / 2;
}
}
// return correct calculated angle for each quadrant
if (way_x > 0) {
if (way_y < 0) {
// first quadrant
return 3 * Math.PI / 2 + Math.atan((double) (-way_y / way_x));
} else {
// second quadrant
return Math.PI + Math.atan((double) (way_x / way_y));
}
} else {
if (way_y > 0) {
// third quadrant
return Math.PI / 2 + Math.atan((double) (way_y / -way_x));
} else {
// fourth quadrant
return 0 + Math.atan((double) (-way_x / -way_y));
}
}
}
public AnalogStick(VirtualController controller, Context context, int elementId) {
super(controller, context, elementId);
// reset stick position
position_stick_x = getWidth() / 2;
position_stick_y = getHeight() / 2;
}
public void addAnalogStickListener(AnalogStickListener listener) {
listeners.add(listener);
}
private void notifyOnMovement(float x, float y) {
_DBG("movement x: " + x + " movement y: " + y);
// notify listeners
for (AnalogStickListener listener : listeners) {
listener.onMovement(x, y);
}
}
private void notifyOnClick() {
_DBG("click");
// notify listeners
for (AnalogStickListener listener : listeners) {
listener.onClick();
}
}
private void notifyOnDoubleClick() {
_DBG("double click");
// notify listeners
for (AnalogStickListener listener : listeners) {
listener.onDoubleClick();
}
}
private void notifyOnRevoke() {
_DBG("revoke");
// notify listeners
for (AnalogStickListener listener : listeners) {
listener.onRevoke();
}
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
// calculate new radius sizes depending
radius_complete = getPercent(getCorrectWidth() / 2, 100) - 2 * getDefaultStrokeWidth();
radius_dead_zone = getPercent(getCorrectWidth() / 2, 30);
radius_analog_stick = getPercent(getCorrectWidth() / 2, 20);
super.onSizeChanged(w, h, oldw, oldh);
}
@Override
protected void onElementDraw(Canvas canvas) {
// set transparent background
canvas.drawColor(Color.TRANSPARENT);
paint.setStyle(Paint.Style.STROKE);
paint.setStrokeWidth(getDefaultStrokeWidth());
// draw outer circle
if (!isPressed() || click_state == CLICK_STATE.SINGLE) {
paint.setColor(getDefaultColor());
} else {
paint.setColor(pressedColor);
}
canvas.drawCircle(getWidth() / 2, getHeight() / 2, radius_complete, paint);
paint.setColor(getDefaultColor());
// draw dead zone
canvas.drawCircle(getWidth() / 2, getHeight() / 2, radius_dead_zone, paint);
// draw stick depending on state
switch (stick_state) {
case NO_MOVEMENT: {
paint.setColor(getDefaultColor());
canvas.drawCircle(getWidth() / 2, getHeight() / 2, radius_analog_stick, paint);
break;
}
case MOVED_IN_DEAD_ZONE:
case MOVED_ACTIVE: {
paint.setColor(pressedColor);
canvas.drawCircle(position_stick_x, position_stick_y, radius_analog_stick, paint);
break;
}
}
}
private void updatePosition() {
// get 100% way
float complete = radius_complete - radius_analog_stick;
// calculate relative way
float correlated_y = (float) (Math.sin(Math.PI / 2 - movement_angle) * (movement_radius));
float correlated_x = (float) (Math.cos(Math.PI / 2 - movement_angle) * (movement_radius));
// update positions
position_stick_x = getWidth() / 2 - correlated_x;
position_stick_y = getHeight() / 2 - correlated_y;
// Stay active even if we're back in the deadzone because we know the user is actively
// giving analog stick input and we don't want to snap back into the deadzone.
// We also release the deadzone if the user keeps the stick pressed for a bit to allow
// them to make precise movements.
stick_state = (stick_state == STICK_STATE.MOVED_ACTIVE ||
SystemClock.uptimeMillis() - timeLastClick > timeoutDeadzone ||
movement_radius > radius_dead_zone) ?
STICK_STATE.MOVED_ACTIVE : STICK_STATE.MOVED_IN_DEAD_ZONE;
// trigger move event if state active
if (stick_state == STICK_STATE.MOVED_ACTIVE) {
notifyOnMovement(-correlated_x / complete, correlated_y / complete);
}
}
@Override
public boolean onElementTouchEvent(MotionEvent event) {
// save last click state
CLICK_STATE lastClickState = click_state;
// get absolute way for each axis
relative_x = -(getWidth() / 2 - event.getX());
relative_y = -(getHeight() / 2 - event.getY());
// get radius and angel of movement from center
movement_radius = getMovementRadius(relative_x, relative_y);
movement_angle = getAngle(relative_x, relative_y);
// pass touch event to parent if out of outer circle
if (movement_radius > radius_complete && !isPressed())
return false;
// chop radius if out of outer circle or near the edge
if (movement_radius > (radius_complete - radius_analog_stick)) {
movement_radius = radius_complete - radius_analog_stick;
}
// handle event depending on action
switch (event.getActionMasked()) {
// down event (touch event)
case MotionEvent.ACTION_DOWN:
case MotionEvent.ACTION_POINTER_DOWN: {
// set to dead zoned, will be corrected in update position if necessary
stick_state = STICK_STATE.MOVED_IN_DEAD_ZONE;
// check for double click
if (lastClickState == CLICK_STATE.SINGLE &&
timeLastClick + timeoutDoubleClick > SystemClock.uptimeMillis()) {
click_state = CLICK_STATE.DOUBLE;
notifyOnDoubleClick();
} else {
click_state = CLICK_STATE.SINGLE;
notifyOnClick();
}
// reset last click timestamp
timeLastClick = SystemClock.uptimeMillis();
// set item pressed and update
setPressed(true);
break;
}
// up event (revoke touch)
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_POINTER_UP: {
setPressed(false);
break;
}
}
if (isPressed()) {
// when is pressed calculate new positions (will trigger movement if necessary)
updatePosition();
} else {
stick_state = STICK_STATE.NO_MOVEMENT;
notifyOnRevoke();
// not longer pressed reset analog stick
notifyOnMovement(0, 0);
}
// refresh view
invalidate();
// accept the touch event
return true;
}
}

View File

@@ -0,0 +1,260 @@
/**
* Created by Karim Mreisi.
*/
package com.limelight.binding.input.virtual_controller;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.RectF;
import android.graphics.drawable.Drawable;
import android.view.MotionEvent;
import java.util.ArrayList;
import java.util.List;
import java.util.Timer;
import java.util.TimerTask;
/**
* This is a digital button on screen element. It is used to get click and double click user input.
*/
public class DigitalButton extends VirtualControllerElement {
/**
* Listener interface to update registered observers.
*/
public interface DigitalButtonListener {
/**
* onClick event will be fired on button click.
*/
void onClick();
/**
* onLongClick event will be fired on button long click.
*/
void onLongClick();
/**
* onRelease event will be fired on button unpress.
*/
void onRelease();
}
/**
*
*/
private class TimerLongClickTimerTask extends TimerTask {
@Override
public void run() {
onLongClickCallback();
}
}
private List<DigitalButtonListener> listeners = new ArrayList<>();
private String text = "";
private int icon = -1;
private long timerLongClickTimeout = 3000;
private Timer timerLongClick = null;
private TimerLongClickTimerTask longClickTimerTask = null;
private final Paint paint = new Paint();
private final RectF rect = new RectF();
private int layer;
private DigitalButton movingButton = null;
boolean inRange(float x, float y) {
return (this.getX() < x && this.getX() + this.getWidth() > x) &&
(this.getY() < y && this.getY() + this.getHeight() > y);
}
public boolean checkMovement(float x, float y, DigitalButton movingButton) {
// check if the movement happened in the same layer
if (movingButton.layer != this.layer) {
return false;
}
// save current pressed state
boolean wasPressed = isPressed();
// check if the movement directly happened on the button
if ((this.movingButton == null || movingButton == this.movingButton)
&& this.inRange(x, y)) {
// set button pressed state depending on moving button pressed state
if (this.isPressed() != movingButton.isPressed()) {
this.setPressed(movingButton.isPressed());
}
}
// check if the movement is outside of the range and the movement button
// is the saved moving button
else if (movingButton == this.movingButton) {
this.setPressed(false);
}
// check if a change occurred
if (wasPressed != isPressed()) {
if (isPressed()) {
// is pressed set moving button and emit click event
this.movingButton = movingButton;
onClickCallback();
} else {
// no longer pressed reset moving button and emit release event
this.movingButton = null;
onReleaseCallback();
}
invalidate();
return true;
}
return false;
}
private void checkMovementForAllButtons(float x, float y) {
for (VirtualControllerElement element : virtualController.getElements()) {
if (element != this && element instanceof DigitalButton) {
((DigitalButton) element).checkMovement(x, y, this);
}
}
}
public DigitalButton(VirtualController controller, int elementId, int layer, Context context) {
super(controller, context, elementId);
this.layer = layer;
}
public void addDigitalButtonListener(DigitalButtonListener listener) {
listeners.add(listener);
}
public void setText(String text) {
this.text = text;
invalidate();
}
public void setIcon(int id) {
this.icon = id;
invalidate();
}
@Override
protected void onElementDraw(Canvas canvas) {
// set transparent background
canvas.drawColor(Color.TRANSPARENT);
paint.setTextSize(getPercent(getWidth(), 25));
paint.setTextAlign(Paint.Align.CENTER);
paint.setStrokeWidth(getDefaultStrokeWidth());
paint.setColor(isPressed() ? pressedColor : getDefaultColor());
paint.setStyle(Paint.Style.STROKE);
rect.left = rect.top = paint.getStrokeWidth();
rect.right = getWidth() - rect.left;
rect.bottom = getHeight() - rect.top;
canvas.drawOval(rect, paint);
if (icon != -1) {
Drawable d = getResources().getDrawable(icon);
d.setBounds(5, 5, getWidth() - 5, getHeight() - 5);
d.draw(canvas);
} else {
paint.setStyle(Paint.Style.FILL_AND_STROKE);
paint.setStrokeWidth(getDefaultStrokeWidth()/2);
canvas.drawText(text, getPercent(getWidth(), 50), getPercent(getHeight(), 63), paint);
}
}
private void onClickCallback() {
_DBG("clicked");
// notify listeners
for (DigitalButtonListener listener : listeners) {
listener.onClick();
}
if (timerLongClick != null) {
timerLongClick.cancel();
timerLongClick = null;
}
if (longClickTimerTask != null) {
longClickTimerTask.cancel();
longClickTimerTask = null;
}
timerLongClick = new Timer();
longClickTimerTask = new TimerLongClickTimerTask();
timerLongClick.schedule(longClickTimerTask, timerLongClickTimeout);
}
private void onLongClickCallback() {
_DBG("long click");
// notify listeners
for (DigitalButtonListener listener : listeners) {
listener.onLongClick();
}
}
private void onReleaseCallback() {
_DBG("released");
// notify listeners
for (DigitalButtonListener listener : listeners) {
listener.onRelease();
}
// We may be called for a release without a prior click
if (timerLongClick != null) {
timerLongClick.cancel();
timerLongClick = null;
}
if (longClickTimerTask != null) {
longClickTimerTask.cancel();
longClickTimerTask = null;
}
}
@Override
public boolean onElementTouchEvent(MotionEvent event) {
// get masked (not specific to a pointer) action
float x = getX() + event.getX();
float y = getY() + event.getY();
int action = event.getActionMasked();
switch (action) {
case MotionEvent.ACTION_DOWN:
case MotionEvent.ACTION_POINTER_DOWN: {
movingButton = null;
setPressed(true);
onClickCallback();
invalidate();
return true;
}
case MotionEvent.ACTION_MOVE: {
checkMovementForAllButtons(x, y);
return true;
}
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_POINTER_UP: {
setPressed(false);
onReleaseCallback();
checkMovementForAllButtons(x, y);
invalidate();
return true;
}
default: {
}
}
return true;
}
}

View File

@@ -0,0 +1,205 @@
/**
* Created by Karim Mreisi.
*/
package com.limelight.binding.input.virtual_controller;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.view.MotionEvent;
import java.util.ArrayList;
import java.util.List;
public class DigitalPad extends VirtualControllerElement {
public final static int DIGITAL_PAD_DIRECTION_NO_DIRECTION = 0;
int direction = DIGITAL_PAD_DIRECTION_NO_DIRECTION;
public final static int DIGITAL_PAD_DIRECTION_LEFT = 1;
public final static int DIGITAL_PAD_DIRECTION_UP = 2;
public final static int DIGITAL_PAD_DIRECTION_RIGHT = 4;
public final static int DIGITAL_PAD_DIRECTION_DOWN = 8;
List<DigitalPadListener> listeners = new ArrayList<>();
private static final int DPAD_MARGIN = 5;
private final Paint paint = new Paint();
public DigitalPad(VirtualController controller, Context context) {
super(controller, context, EID_DPAD);
}
public void addDigitalPadListener(DigitalPadListener listener) {
listeners.add(listener);
}
@Override
protected void onElementDraw(Canvas canvas) {
// set transparent background
canvas.drawColor(Color.TRANSPARENT);
paint.setTextSize(getPercent(getCorrectWidth(), 20));
paint.setTextAlign(Paint.Align.CENTER);
paint.setStrokeWidth(getDefaultStrokeWidth());
if (direction == DIGITAL_PAD_DIRECTION_NO_DIRECTION) {
// draw no direction rect
paint.setStyle(Paint.Style.STROKE);
paint.setColor(getDefaultColor());
canvas.drawRect(
getPercent(getWidth(), 36), getPercent(getHeight(), 36),
getPercent(getWidth(), 63), getPercent(getHeight(), 63),
paint
);
}
// draw left rect
paint.setColor(
(direction & DIGITAL_PAD_DIRECTION_LEFT) > 0 ? pressedColor : getDefaultColor());
paint.setStyle(Paint.Style.STROKE);
canvas.drawRect(
paint.getStrokeWidth()+DPAD_MARGIN, getPercent(getHeight(), 33),
getPercent(getWidth(), 33), getPercent(getHeight(), 66),
paint
);
// draw up rect
paint.setColor(
(direction & DIGITAL_PAD_DIRECTION_UP) > 0 ? pressedColor : getDefaultColor());
paint.setStyle(Paint.Style.STROKE);
canvas.drawRect(
getPercent(getWidth(), 33), paint.getStrokeWidth()+DPAD_MARGIN,
getPercent(getWidth(), 66), getPercent(getHeight(), 33),
paint
);
// draw right rect
paint.setColor(
(direction & DIGITAL_PAD_DIRECTION_RIGHT) > 0 ? pressedColor : getDefaultColor());
paint.setStyle(Paint.Style.STROKE);
canvas.drawRect(
getPercent(getWidth(), 66), getPercent(getHeight(), 33),
getWidth() - (paint.getStrokeWidth()+DPAD_MARGIN), getPercent(getHeight(), 66),
paint
);
// draw down rect
paint.setColor(
(direction & DIGITAL_PAD_DIRECTION_DOWN) > 0 ? pressedColor : getDefaultColor());
paint.setStyle(Paint.Style.STROKE);
canvas.drawRect(
getPercent(getWidth(), 33), getPercent(getHeight(), 66),
getPercent(getWidth(), 66), getHeight() - (paint.getStrokeWidth()+DPAD_MARGIN),
paint
);
// draw left up line
paint.setColor((
(direction & DIGITAL_PAD_DIRECTION_LEFT) > 0 &&
(direction & DIGITAL_PAD_DIRECTION_UP) > 0
) ? pressedColor : getDefaultColor()
);
paint.setStyle(Paint.Style.STROKE);
canvas.drawLine(
paint.getStrokeWidth()+DPAD_MARGIN, getPercent(getHeight(), 33),
getPercent(getWidth(), 33), paint.getStrokeWidth()+DPAD_MARGIN,
paint
);
// draw up right line
paint.setColor((
(direction & DIGITAL_PAD_DIRECTION_UP) > 0 &&
(direction & DIGITAL_PAD_DIRECTION_RIGHT) > 0
) ? pressedColor : getDefaultColor()
);
paint.setStyle(Paint.Style.STROKE);
canvas.drawLine(
getPercent(getWidth(), 66), paint.getStrokeWidth()+DPAD_MARGIN,
getWidth() - (paint.getStrokeWidth()+DPAD_MARGIN), getPercent(getHeight(), 33),
paint
);
// draw right down line
paint.setColor((
(direction & DIGITAL_PAD_DIRECTION_RIGHT) > 0 &&
(direction & DIGITAL_PAD_DIRECTION_DOWN) > 0
) ? pressedColor : getDefaultColor()
);
paint.setStyle(Paint.Style.STROKE);
canvas.drawLine(
getWidth()-paint.getStrokeWidth(), getPercent(getHeight(), 66),
getPercent(getWidth(), 66), getHeight()-(paint.getStrokeWidth()+DPAD_MARGIN),
paint
);
// draw down left line
paint.setColor((
(direction & DIGITAL_PAD_DIRECTION_DOWN) > 0 &&
(direction & DIGITAL_PAD_DIRECTION_LEFT) > 0
) ? pressedColor : getDefaultColor()
);
paint.setStyle(Paint.Style.STROKE);
canvas.drawLine(
getPercent(getWidth(), 33), getHeight()-(paint.getStrokeWidth()+DPAD_MARGIN),
paint.getStrokeWidth()+DPAD_MARGIN, getPercent(getHeight(), 66),
paint
);
}
private void newDirectionCallback(int direction) {
_DBG("direction: " + direction);
// notify listeners
for (DigitalPadListener listener : listeners) {
listener.onDirectionChange(direction);
}
}
@Override
public boolean onElementTouchEvent(MotionEvent event) {
// get masked (not specific to a pointer) action
switch (event.getActionMasked()) {
case MotionEvent.ACTION_DOWN:
case MotionEvent.ACTION_POINTER_DOWN:
case MotionEvent.ACTION_MOVE: {
direction = 0;
if (event.getX() < getPercent(getWidth(), 33)) {
direction |= DIGITAL_PAD_DIRECTION_LEFT;
}
if (event.getX() > getPercent(getWidth(), 66)) {
direction |= DIGITAL_PAD_DIRECTION_RIGHT;
}
if (event.getY() > getPercent(getHeight(), 66)) {
direction |= DIGITAL_PAD_DIRECTION_DOWN;
}
if (event.getY() < getPercent(getHeight(), 33)) {
direction |= DIGITAL_PAD_DIRECTION_UP;
}
newDirectionCallback(direction);
invalidate();
return true;
}
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_POINTER_UP: {
direction = 0;
newDirectionCallback(direction);
invalidate();
return true;
}
default: {
}
}
return true;
}
public interface DigitalPadListener {
void onDirectionChange(int direction);
}
}

View File

@@ -0,0 +1,49 @@
/**
* Created by Karim Mreisi.
*/
package com.limelight.binding.input.virtual_controller;
import android.content.Context;
import com.limelight.nvstream.input.ControllerPacket;
public class LeftAnalogStick extends AnalogStick {
public LeftAnalogStick(final VirtualController controller, final Context context) {
super(controller, context, EID_LS);
addAnalogStickListener(new AnalogStick.AnalogStickListener() {
@Override
public void onMovement(float x, float y) {
VirtualController.ControllerInputContext inputContext =
controller.getControllerInputContext();
inputContext.leftStickX = (short) (x * 0x7FFE);
inputContext.leftStickY = (short) (y * 0x7FFE);
controller.sendControllerInputContext();
}
@Override
public void onClick() {
}
@Override
public void onDoubleClick() {
VirtualController.ControllerInputContext inputContext =
controller.getControllerInputContext();
inputContext.inputMap |= ControllerPacket.LS_CLK_FLAG;
controller.sendControllerInputContext();
}
@Override
public void onRevoke() {
VirtualController.ControllerInputContext inputContext =
controller.getControllerInputContext();
inputContext.inputMap &= ~ControllerPacket.LS_CLK_FLAG;
controller.sendControllerInputContext();
}
});
}
}

View File

@@ -0,0 +1,36 @@
/**
* Created by Karim Mreisi.
*/
package com.limelight.binding.input.virtual_controller;
import android.content.Context;
public class LeftTrigger extends DigitalButton {
public LeftTrigger(final VirtualController controller, final int layer, final Context context) {
super(controller, EID_LT, layer, context);
addDigitalButtonListener(new DigitalButton.DigitalButtonListener() {
@Override
public void onClick() {
VirtualController.ControllerInputContext inputContext =
controller.getControllerInputContext();
inputContext.leftTrigger = (byte) 0xFF;
controller.sendControllerInputContext();
}
@Override
public void onLongClick() {
}
@Override
public void onRelease() {
VirtualController.ControllerInputContext inputContext =
controller.getControllerInputContext();
inputContext.leftTrigger = (byte) 0x00;
controller.sendControllerInputContext();
}
});
}
}

View File

@@ -0,0 +1,49 @@
/**
* Created by Karim Mreisi.
*/
package com.limelight.binding.input.virtual_controller;
import android.content.Context;
import com.limelight.nvstream.input.ControllerPacket;
public class RightAnalogStick extends AnalogStick {
public RightAnalogStick(final VirtualController controller, final Context context) {
super(controller, context, EID_RS);
addAnalogStickListener(new AnalogStick.AnalogStickListener() {
@Override
public void onMovement(float x, float y) {
VirtualController.ControllerInputContext inputContext =
controller.getControllerInputContext();
inputContext.rightStickX = (short) (x * 0x7FFE);
inputContext.rightStickY = (short) (y * 0x7FFE);
controller.sendControllerInputContext();
}
@Override
public void onClick() {
}
@Override
public void onDoubleClick() {
VirtualController.ControllerInputContext inputContext =
controller.getControllerInputContext();
inputContext.inputMap |= ControllerPacket.RS_CLK_FLAG;
controller.sendControllerInputContext();
}
@Override
public void onRevoke() {
VirtualController.ControllerInputContext inputContext =
controller.getControllerInputContext();
inputContext.inputMap &= ~ControllerPacket.RS_CLK_FLAG;
controller.sendControllerInputContext();
}
});
}
}

View File

@@ -0,0 +1,36 @@
/**
* Created by Karim Mreisi.
*/
package com.limelight.binding.input.virtual_controller;
import android.content.Context;
public class RightTrigger extends DigitalButton {
public RightTrigger(final VirtualController controller, final int layer, final Context context) {
super(controller, EID_RT, layer, context);
addDigitalButtonListener(new DigitalButton.DigitalButtonListener() {
@Override
public void onClick() {
VirtualController.ControllerInputContext inputContext =
controller.getControllerInputContext();
inputContext.rightTrigger = (byte) 0xFF;
controller.sendControllerInputContext();
}
@Override
public void onLongClick() {
}
@Override
public void onRelease() {
VirtualController.ControllerInputContext inputContext =
controller.getControllerInputContext();
inputContext.rightTrigger = (byte) 0x00;
controller.sendControllerInputContext();
}
});
}
}

View File

@@ -0,0 +1,201 @@
/**
* Created by Karim Mreisi.
*/
package com.limelight.binding.input.virtual_controller;
import android.content.Context;
import android.util.DisplayMetrics;
import android.view.View;
import android.widget.Button;
import android.widget.FrameLayout;
import android.widget.RelativeLayout;
import android.widget.Toast;
import com.limelight.R;
import com.limelight.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 static class ControllerInputContext {
public short inputMap = 0x0000;
public byte leftTrigger = 0x00;
public byte rightTrigger = 0x00;
public short rightStickX = 0x0000;
public short rightStickY = 0x0000;
public short leftStickX = 0x0000;
public short leftStickY = 0x0000;
}
public enum ControllerMode {
Active,
MoveButtons,
ResizeButtons
}
private static final boolean _PRINT_DEBUG_INFORMATION = false;
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();
private Button buttonConfigure = null;
private List<VirtualControllerElement> elements = new ArrayList<>();
public VirtualController(final ControllerHandler controllerHandler, FrameLayout layout, final Context context) {
this.controllerHandler = controllerHandler;
this.frame_layout = layout;
this.context = context;
relative_layout = new RelativeLayout(context);
frame_layout.addView(relative_layout);
buttonConfigure = new Button(context);
buttonConfigure.setAlpha(0.25f);
buttonConfigure.setFocusable(false);
buttonConfigure.setBackgroundResource(R.drawable.ic_settings);
buttonConfigure.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
String message;
if (currentMode == ControllerMode.Active){
currentMode = ControllerMode.MoveButtons;
message = "Entering configuration mode (Move buttons)";
} else if (currentMode == ControllerMode.MoveButtons) {
currentMode = ControllerMode.ResizeButtons;
message = "Entering configuration mode (Resize buttons)";
} else {
currentMode = ControllerMode.Active;
VirtualControllerConfigurationLoader.saveProfile(VirtualController.this, context);
message = "Exiting configuration mode";
}
Toast.makeText(context, message, Toast.LENGTH_SHORT).show();
relative_layout.invalidate();
for (VirtualControllerElement element : elements) {
element.invalidate();
}
}
});
}
public void hide() {
retransmitTimer.cancel();
relative_layout.setVisibility(View.INVISIBLE);
}
public void show() {
relative_layout.setVisibility(View.VISIBLE);
// HACK: GFE sometimes discards gamepad packets when they are received
// very shortly after another. This can be critical if an axis zeroing packet
// is lost and causes an analog stick to get stuck. To avoid this, we send
// a gamepad input packet every 100 ms to ensure any loss can be recovered.
retransmitTimer = new Timer("OSC timer", true);
retransmitTimer.schedule(new TimerTask() {
@Override
public void run() {
sendControllerInputContext();
}
}, 100, 100);
}
public void removeElements() {
for (VirtualControllerElement element : elements) {
relative_layout.removeView(element);
}
elements.clear();
}
public void setOpacity(int opacity) {
for (VirtualControllerElement element : elements) {
element.setOpacity(opacity);
}
}
public void addElement(VirtualControllerElement element, int x, int y, int width, int height) {
elements.add(element);
RelativeLayout.LayoutParams layoutParams = new RelativeLayout.LayoutParams(width, height);
layoutParams.setMargins(x, y, 0, 0);
relative_layout.addView(element, layoutParams);
}
public List<VirtualControllerElement> getElements() {
return elements;
}
private static final void _DBG(String text) {
if (_PRINT_DEBUG_INFORMATION) {
System.out.println("VirtualController: " + text);
}
}
public void refreshLayout() {
relative_layout.removeAllViews();
removeElements();
DisplayMetrics screen = context.getResources().getDisplayMetrics();
int buttonSize = (int)(screen.heightPixels*0.06f);
RelativeLayout.LayoutParams params = new RelativeLayout.LayoutParams(buttonSize, buttonSize);
params.addRule(RelativeLayout.ALIGN_PARENT_LEFT, RelativeLayout.TRUE);
params.addRule(RelativeLayout.ALIGN_PARENT_TOP, RelativeLayout.TRUE);
params.leftMargin = 15;
params.topMargin = 15;
relative_layout.addView(buttonConfigure, params);
// Start with the default layout
VirtualControllerConfigurationLoader.createDefaultLayout(this, context);
// Apply user preferences onto the default layout
VirtualControllerConfigurationLoader.loadFromPreferences(this, context);
}
public ControllerMode getControllerMode() {
return currentMode;
}
public ControllerInputContext getControllerInputContext() {
return inputContext;
}
void sendControllerInputContext() {
_DBG("INPUT_MAP + " + inputContext.inputMap);
_DBG("LEFT_TRIGGER " + inputContext.leftTrigger);
_DBG("RIGHT_TRIGGER " + inputContext.rightTrigger);
_DBG("LEFT STICK X: " + inputContext.leftStickX + " Y: " + inputContext.leftStickY);
_DBG("RIGHT STICK X: " + inputContext.rightStickX + " Y: " + inputContext.rightStickY);
if (controllerHandler != null) {
controllerHandler.reportOscState(
inputContext.inputMap,
inputContext.leftStickX,
inputContext.leftStickY,
inputContext.rightStickX,
inputContext.rightStickY,
inputContext.leftTrigger,
inputContext.rightTrigger
);
}
}
}

View File

@@ -0,0 +1,370 @@
/**
* Created by Karim Mreisi.
*/
package com.limelight.binding.input.virtual_controller;
import android.app.Activity;
import android.content.Context;
import android.content.SharedPreferences;
import android.util.DisplayMetrics;
import com.limelight.nvstream.input.ControllerPacket;
import com.limelight.preferences.PreferenceConfiguration;
import org.json.JSONException;
import org.json.JSONObject;
public class VirtualControllerConfigurationLoader {
public static final String OSC_PREFERENCE = "OSC";
private static int getPercent(
int percent,
int total) {
return (int) (((float) total / (float) 100) * (float) percent);
}
// The default controls are specified using a grid of 128*72 cells at 16:9
private static int screenScale(int units, int height) {
return (int) (((float) height / (float) 72) * (float) units);
}
private static DigitalPad createDigitalPad(
final VirtualController controller,
final Context context) {
DigitalPad digitalPad = new DigitalPad(controller, context);
digitalPad.addDigitalPadListener(new DigitalPad.DigitalPadListener() {
@Override
public void onDirectionChange(int direction) {
VirtualController.ControllerInputContext inputContext =
controller.getControllerInputContext();
if (direction == DigitalPad.DIGITAL_PAD_DIRECTION_NO_DIRECTION) {
inputContext.inputMap &= ~ControllerPacket.LEFT_FLAG;
inputContext.inputMap &= ~ControllerPacket.RIGHT_FLAG;
inputContext.inputMap &= ~ControllerPacket.UP_FLAG;
inputContext.inputMap &= ~ControllerPacket.DOWN_FLAG;
controller.sendControllerInputContext();
return;
}
if ((direction & DigitalPad.DIGITAL_PAD_DIRECTION_LEFT) > 0) {
inputContext.inputMap |= ControllerPacket.LEFT_FLAG;
}
if ((direction & DigitalPad.DIGITAL_PAD_DIRECTION_RIGHT) > 0) {
inputContext.inputMap |= ControllerPacket.RIGHT_FLAG;
}
if ((direction & DigitalPad.DIGITAL_PAD_DIRECTION_UP) > 0) {
inputContext.inputMap |= ControllerPacket.UP_FLAG;
}
if ((direction & DigitalPad.DIGITAL_PAD_DIRECTION_DOWN) > 0) {
inputContext.inputMap |= ControllerPacket.DOWN_FLAG;
}
controller.sendControllerInputContext();
}
});
return digitalPad;
}
private static DigitalButton createDigitalButton(
final int elementId,
final int keyShort,
final int keyLong,
final int layer,
final String text,
final int icon,
final VirtualController controller,
final Context context) {
DigitalButton button = new DigitalButton(controller, elementId, layer, context);
button.setText(text);
button.setIcon(icon);
button.addDigitalButtonListener(new DigitalButton.DigitalButtonListener() {
@Override
public void onClick() {
VirtualController.ControllerInputContext inputContext =
controller.getControllerInputContext();
inputContext.inputMap |= keyShort;
controller.sendControllerInputContext();
}
@Override
public void onLongClick() {
VirtualController.ControllerInputContext inputContext =
controller.getControllerInputContext();
inputContext.inputMap |= keyLong;
controller.sendControllerInputContext();
}
@Override
public void onRelease() {
VirtualController.ControllerInputContext inputContext =
controller.getControllerInputContext();
inputContext.inputMap &= ~keyShort;
inputContext.inputMap &= ~keyLong;
controller.sendControllerInputContext();
}
});
return button;
}
private static DigitalButton createLeftTrigger(
final int layer,
final String text,
final int icon,
final VirtualController controller,
final Context context) {
LeftTrigger button = new LeftTrigger(controller, layer, context);
button.setText(text);
button.setIcon(icon);
return button;
}
private static DigitalButton createRightTrigger(
final int layer,
final String text,
final int icon,
final VirtualController controller,
final Context context) {
RightTrigger button = new RightTrigger(controller, layer, context);
button.setText(text);
button.setIcon(icon);
return button;
}
private static AnalogStick createLeftStick(
final VirtualController controller,
final Context context) {
return new LeftAnalogStick(controller, context);
}
private static AnalogStick createRightStick(
final VirtualController controller,
final Context context) {
return new RightAnalogStick(controller, context);
}
private static final int TRIGGER_L_BASE_X = 1;
private static final int TRIGGER_R_BASE_X = 92;
private static final int TRIGGER_DISTANCE = 23;
private static final int TRIGGER_BASE_Y = 31;
private static final int TRIGGER_WIDTH = 12;
private static final int TRIGGER_HEIGHT = 9;
// Face buttons are defined based on the Y button (button number 9)
private static final int BUTTON_BASE_X = 106;
private static final int BUTTON_BASE_Y = 1;
private static final int BUTTON_SIZE = 10;
private static final int DPAD_BASE_X = 4;
private static final int DPAD_BASE_Y = 41;
private static final int DPAD_SIZE = 30;
private static final int ANALOG_L_BASE_X = 6;
private static final int ANALOG_L_BASE_Y = 4;
private static final int ANALOG_R_BASE_X = 98;
private static final int ANALOG_R_BASE_Y = 42;
private static final int ANALOG_SIZE = 26;
private static final int L3_R3_BASE_Y = 60;
private static final int START_X = 83;
private static final int BACK_X = 34;
private static final int START_BACK_Y = 64;
private static final int START_BACK_WIDTH = 12;
private static final int START_BACK_HEIGHT = 7;
public static void createDefaultLayout(final VirtualController controller, final Context context) {
DisplayMetrics screen = context.getResources().getDisplayMetrics();
PreferenceConfiguration config = PreferenceConfiguration.readPreferences(context);
// Displace controls on the right by this amount of pixels to account for different aspect ratios
int rightDisplacement = screen.widthPixels - screen.heightPixels * 16 / 9;
int height = screen.heightPixels;
// NOTE: Some of these getPercent() expressions seem like they can be combined
// into a single call. Due to floating point rounding, this isn't actually possible.
if (!config.onlyL3R3)
{
controller.addElement(createDigitalPad(controller, context),
screenScale(DPAD_BASE_X, height),
screenScale(DPAD_BASE_Y, height),
screenScale(DPAD_SIZE, height),
screenScale(DPAD_SIZE, height)
);
controller.addElement(createDigitalButton(
VirtualControllerElement.EID_A,
!config.flipFaceButtons ? ControllerPacket.A_FLAG : ControllerPacket.B_FLAG, 0, 1,
!config.flipFaceButtons ? "A" : "B", -1, controller, context),
screenScale(BUTTON_BASE_X, height) + rightDisplacement,
screenScale(BUTTON_BASE_Y + 2 * BUTTON_SIZE, height),
screenScale(BUTTON_SIZE, height),
screenScale(BUTTON_SIZE, height)
);
controller.addElement(createDigitalButton(
VirtualControllerElement.EID_B,
config.flipFaceButtons ? ControllerPacket.A_FLAG : ControllerPacket.B_FLAG, 0, 1,
config.flipFaceButtons ? "A" : "B", -1, controller, context),
screenScale(BUTTON_BASE_X + BUTTON_SIZE, height) + rightDisplacement,
screenScale(BUTTON_BASE_Y + BUTTON_SIZE, height),
screenScale(BUTTON_SIZE, height),
screenScale(BUTTON_SIZE, height)
);
controller.addElement(createDigitalButton(
VirtualControllerElement.EID_X,
!config.flipFaceButtons ? ControllerPacket.X_FLAG : ControllerPacket.Y_FLAG, 0, 1,
!config.flipFaceButtons ? "X" : "Y", -1, controller, context),
screenScale(BUTTON_BASE_X - BUTTON_SIZE, height) + rightDisplacement,
screenScale(BUTTON_BASE_Y + BUTTON_SIZE, height),
screenScale(BUTTON_SIZE, height),
screenScale(BUTTON_SIZE, height)
);
controller.addElement(createDigitalButton(
VirtualControllerElement.EID_Y,
config.flipFaceButtons ? ControllerPacket.X_FLAG : ControllerPacket.Y_FLAG, 0, 1,
config.flipFaceButtons ? "X" : "Y", -1, controller, context),
screenScale(BUTTON_BASE_X, height) + rightDisplacement,
screenScale(BUTTON_BASE_Y, height),
screenScale(BUTTON_SIZE, height),
screenScale(BUTTON_SIZE, height)
);
controller.addElement(createLeftTrigger(
1, "LT", -1, controller, context),
screenScale(TRIGGER_L_BASE_X, height),
screenScale(TRIGGER_BASE_Y, height),
screenScale(TRIGGER_WIDTH, height),
screenScale(TRIGGER_HEIGHT, height)
);
controller.addElement(createRightTrigger(
1, "RT", -1, controller, context),
screenScale(TRIGGER_R_BASE_X + TRIGGER_DISTANCE, height) + rightDisplacement,
screenScale(TRIGGER_BASE_Y, height),
screenScale(TRIGGER_WIDTH, height),
screenScale(TRIGGER_HEIGHT, height)
);
controller.addElement(createDigitalButton(
VirtualControllerElement.EID_LB,
ControllerPacket.LB_FLAG, 0, 1, "LB", -1, controller, context),
screenScale(TRIGGER_L_BASE_X + TRIGGER_DISTANCE, height),
screenScale(TRIGGER_BASE_Y, height),
screenScale(TRIGGER_WIDTH, height),
screenScale(TRIGGER_HEIGHT, height)
);
controller.addElement(createDigitalButton(
VirtualControllerElement.EID_RB,
ControllerPacket.RB_FLAG, 0, 1, "RB", -1, controller, context),
screenScale(TRIGGER_R_BASE_X, height) + rightDisplacement,
screenScale(TRIGGER_BASE_Y, height),
screenScale(TRIGGER_WIDTH, height),
screenScale(TRIGGER_HEIGHT, height)
);
controller.addElement(createLeftStick(controller, context),
screenScale(ANALOG_L_BASE_X, height),
screenScale(ANALOG_L_BASE_Y, height),
screenScale(ANALOG_SIZE, height),
screenScale(ANALOG_SIZE, height)
);
controller.addElement(createRightStick(controller, context),
screenScale(ANALOG_R_BASE_X, height) + rightDisplacement,
screenScale(ANALOG_R_BASE_Y, height),
screenScale(ANALOG_SIZE, height),
screenScale(ANALOG_SIZE, height)
);
controller.addElement(createDigitalButton(
VirtualControllerElement.EID_BACK,
ControllerPacket.BACK_FLAG, 0, 2, "BACK", -1, controller, context),
screenScale(BACK_X, height),
screenScale(START_BACK_Y, height),
screenScale(START_BACK_WIDTH, height),
screenScale(START_BACK_HEIGHT, height)
);
controller.addElement(createDigitalButton(
VirtualControllerElement.EID_START,
ControllerPacket.PLAY_FLAG, 0, 3, "START", -1, controller, context),
screenScale(START_X, height) + rightDisplacement,
screenScale(START_BACK_Y, height),
screenScale(START_BACK_WIDTH, height),
screenScale(START_BACK_HEIGHT, height)
);
}
else {
controller.addElement(createDigitalButton(
VirtualControllerElement.EID_LSB,
ControllerPacket.LS_CLK_FLAG, 0, 1, "L3", -1, controller, context),
screenScale(TRIGGER_L_BASE_X, height),
screenScale(L3_R3_BASE_Y, height),
screenScale(TRIGGER_WIDTH, height),
screenScale(TRIGGER_HEIGHT, height)
);
controller.addElement(createDigitalButton(
VirtualControllerElement.EID_RSB,
ControllerPacket.RS_CLK_FLAG, 0, 1, "R3", -1, controller, context),
screenScale(TRIGGER_R_BASE_X + TRIGGER_DISTANCE, height) + rightDisplacement,
screenScale(L3_R3_BASE_Y, height),
screenScale(TRIGGER_WIDTH, height),
screenScale(TRIGGER_HEIGHT, height)
);
}
controller.setOpacity(config.oscOpacity);
}
public static void saveProfile(final VirtualController controller,
final Context context) {
SharedPreferences.Editor prefEditor = context.getSharedPreferences(OSC_PREFERENCE, Activity.MODE_PRIVATE).edit();
for (VirtualControllerElement element : controller.getElements()) {
String prefKey = ""+element.elementId;
try {
prefEditor.putString(prefKey, element.getConfiguration().toString());
} catch (JSONException e) {
e.printStackTrace();
}
}
prefEditor.apply();
}
public static void loadFromPreferences(final VirtualController controller, final Context context) {
SharedPreferences pref = context.getSharedPreferences(OSC_PREFERENCE, Activity.MODE_PRIVATE);
for (VirtualControllerElement element : controller.getElements()) {
String prefKey = ""+element.elementId;
String jsonConfig = pref.getString(prefKey, null);
if (jsonConfig != null) {
try {
element.loadConfiguration(new JSONObject(jsonConfig));
} catch (JSONException e) {
e.printStackTrace();
// Remove the corrupt element from the preferences
pref.edit().remove(prefKey).apply();
}
}
}
}
}

View File

@@ -0,0 +1,339 @@
/**
* Created by Karim Mreisi.
*/
package com.limelight.binding.input.virtual_controller;
import android.app.AlertDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.util.DisplayMetrics;
import android.view.MotionEvent;
import android.view.View;
import android.widget.RelativeLayout;
import org.json.JSONException;
import org.json.JSONObject;
public abstract class VirtualControllerElement extends View {
protected static boolean _PRINT_DEBUG_INFORMATION = false;
public static final int EID_DPAD = 1;
public static final int EID_LT = 2;
public static final int EID_RT = 3;
public static final int EID_LB = 4;
public static final int EID_RB = 5;
public static final int EID_A = 6;
public static final int EID_B = 7;
public static final int EID_X = 8;
public static final int EID_Y = 9;
public static final int EID_BACK = 10;
public static final int EID_START = 11;
public static final int EID_LS = 12;
public static final int EID_RS = 13;
public static final int EID_LSB = 14;
public static final int EID_RSB = 15;
protected VirtualController virtualController;
protected final int elementId;
private final Paint paint = new Paint();
private int normalColor = 0xF0888888;
protected int pressedColor = 0xF00000FF;
private int configMoveColor = 0xF0FF0000;
private int configResizeColor = 0xF0FF00FF;
private int configSelectedColor = 0xF000FF00;
protected int startSize_x;
protected int startSize_y;
float position_pressed_x = 0;
float position_pressed_y = 0;
private enum Mode {
Normal,
Resize,
Move
}
private Mode currentMode = Mode.Normal;
protected VirtualControllerElement(VirtualController controller, Context context, int elementId) {
super(context);
this.virtualController = controller;
this.elementId = elementId;
}
protected void moveElement(int pressed_x, int pressed_y, int x, int y) {
int newPos_x = (int) getX() + x - pressed_x;
int newPos_y = (int) getY() + y - pressed_y;
RelativeLayout.LayoutParams layoutParams = (RelativeLayout.LayoutParams) getLayoutParams();
layoutParams.leftMargin = newPos_x > 0 ? newPos_x : 0;
layoutParams.topMargin = newPos_y > 0 ? newPos_y : 0;
layoutParams.rightMargin = 0;
layoutParams.bottomMargin = 0;
requestLayout();
}
protected void resizeElement(int pressed_x, int pressed_y, int width, int height) {
RelativeLayout.LayoutParams layoutParams = (RelativeLayout.LayoutParams) getLayoutParams();
int newHeight = height + (startSize_y - pressed_y);
int newWidth = width + (startSize_x - pressed_x);
layoutParams.height = newHeight > 20 ? newHeight : 20;
layoutParams.width = newWidth > 20 ? newWidth : 20;
requestLayout();
}
@Override
protected void onDraw(Canvas canvas) {
onElementDraw(canvas);
if (currentMode != Mode.Normal) {
paint.setColor(configSelectedColor);
paint.setStrokeWidth(getDefaultStrokeWidth());
paint.setStyle(Paint.Style.STROKE);
canvas.drawRect(paint.getStrokeWidth(), paint.getStrokeWidth(),
getWidth()-paint.getStrokeWidth(), getHeight()-paint.getStrokeWidth(),
paint);
}
super.onDraw(canvas);
}
/*
protected void actionShowNormalColorChooser() {
AmbilWarnaDialog colorDialog = new AmbilWarnaDialog(getContext(), normalColor, true, new AmbilWarnaDialog.OnAmbilWarnaListener() {
@Override
public void onCancel(AmbilWarnaDialog dialog)
{}
@Override
public void onOk(AmbilWarnaDialog dialog, int color) {
normalColor = color;
invalidate();
}
});
colorDialog.show();
}
protected void actionShowPressedColorChooser() {
AmbilWarnaDialog colorDialog = new AmbilWarnaDialog(getContext(), normalColor, true, new AmbilWarnaDialog.OnAmbilWarnaListener() {
@Override
public void onCancel(AmbilWarnaDialog dialog) {
}
@Override
public void onOk(AmbilWarnaDialog dialog, int color) {
pressedColor = color;
invalidate();
}
});
colorDialog.show();
}
*/
protected void actionEnableMove() {
currentMode = Mode.Move;
}
protected void actionEnableResize() {
currentMode = Mode.Resize;
}
protected void actionCancel() {
currentMode = Mode.Normal;
invalidate();
}
protected int getDefaultColor() {
if (virtualController.getControllerMode() == VirtualController.ControllerMode.MoveButtons)
return configMoveColor;
else if (virtualController.getControllerMode() == VirtualController.ControllerMode.ResizeButtons)
return configResizeColor;
else
return normalColor;
}
protected int getDefaultStrokeWidth() {
DisplayMetrics screen = getResources().getDisplayMetrics();
return (int)(screen.heightPixels*0.004f);
}
protected void showConfigurationDialog() {
AlertDialog.Builder alertBuilder = new AlertDialog.Builder(getContext());
alertBuilder.setTitle("Configuration");
CharSequence functions[] = new CharSequence[]{
"Move",
"Resize",
/*election
"Set n
Disable color sormal color",
"Set pressed color",
*/
"Cancel"
};
alertBuilder.setItems(functions, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
switch (which) {
case 0: { // move
actionEnableMove();
break;
}
case 1: { // resize
actionEnableResize();
break;
}
/*
case 2: { // set default color
actionShowNormalColorChooser();
break;
}
case 3: { // set pressed color
actionShowPressedColorChooser();
break;
}
*/
default: { // cancel
actionCancel();
break;
}
}
}
});
AlertDialog alert = alertBuilder.create();
// show menu
alert.show();
}
@Override
public boolean onTouchEvent(MotionEvent event) {
if (virtualController.getControllerMode() == VirtualController.ControllerMode.Active) {
return onElementTouchEvent(event);
}
switch (event.getActionMasked()) {
case MotionEvent.ACTION_DOWN:
case MotionEvent.ACTION_POINTER_DOWN: {
position_pressed_x = event.getX();
position_pressed_y = event.getY();
startSize_x = getWidth();
startSize_y = getHeight();
if (virtualController.getControllerMode() == VirtualController.ControllerMode.MoveButtons)
actionEnableMove();
else if (virtualController.getControllerMode() == VirtualController.ControllerMode.ResizeButtons)
actionEnableResize();
return true;
}
case MotionEvent.ACTION_MOVE: {
switch (currentMode) {
case Move: {
moveElement(
(int) position_pressed_x,
(int) position_pressed_y,
(int) event.getX(),
(int) event.getY());
break;
}
case Resize: {
resizeElement(
(int) position_pressed_x,
(int) position_pressed_y,
(int) event.getX(),
(int) event.getY());
break;
}
case Normal: {
break;
}
}
return true;
}
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_POINTER_UP: {
actionCancel();
return true;
}
default: {
}
}
return true;
}
abstract protected void onElementDraw(Canvas canvas);
abstract public boolean onElementTouchEvent(MotionEvent event);
protected static final void _DBG(String text) {
if (_PRINT_DEBUG_INFORMATION) {
System.out.println(text);
}
}
public void setColors(int normalColor, int pressedColor) {
this.normalColor = normalColor;
this.pressedColor = pressedColor;
invalidate();
}
public void setOpacity(int opacity) {
int hexOpacity = opacity * 255 / 100;
this.normalColor = (hexOpacity << 24) | (normalColor & 0x00FFFFFF);
this.pressedColor = (hexOpacity << 24) | (pressedColor & 0x00FFFFFF);
invalidate();
}
protected final float getPercent(float value, float percent) {
return value / 100 * percent;
}
protected final int getCorrectWidth() {
return getWidth() > getHeight() ? getHeight() : getWidth();
}
public JSONObject getConfiguration() throws JSONException {
JSONObject configuration = new JSONObject();
RelativeLayout.LayoutParams layoutParams = (RelativeLayout.LayoutParams) getLayoutParams();
configuration.put("LEFT", layoutParams.leftMargin);
configuration.put("TOP", layoutParams.topMargin);
configuration.put("WIDTH", layoutParams.width);
configuration.put("HEIGHT", layoutParams.height);
return configuration;
}
public void loadConfiguration(JSONObject configuration) throws JSONException {
RelativeLayout.LayoutParams layoutParams = (RelativeLayout.LayoutParams) getLayoutParams();
layoutParams.leftMargin = configuration.getInt("LEFT");
layoutParams.topMargin = configuration.getInt("TOP");
layoutParams.width = configuration.getInt("WIDTH");
layoutParams.height = configuration.getInt("HEIGHT");
requestLayout();
}
}

View File

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

View File

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

View File

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

View File

@@ -1,7 +0,0 @@
package com.limelight.binding.video;
import com.limelight.nvstream.av.video.VideoDecoderRenderer;
public abstract class EnhancedDecoderRenderer extends VideoDecoderRenderer {
public abstract String getDecoderName();
}

View File

@@ -0,0 +1,5 @@
package com.limelight.binding.video;
public interface PerfOverlayListener {
void onPerfUpdate(final String text);
}

View File

@@ -0,0 +1,72 @@
package com.limelight.binding.video;
import android.os.SystemClock;
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 = (SystemClock.uptimeMillis() - 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;
}

View File

@@ -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,13 +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 char ADDRESS_DELIMITER = ';';
private SQLiteDatabase computerDb;
@@ -36,80 +39,107 @@ 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+"='"+name+"'", null);
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, details.localIp.getAddress());
values.put(REMOTE_IP_COLUMN_NAME, details.remoteIp.getAddress());
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.uuid = c.getString(0);
details.name = c.getString(1);
String[] addresses = c.getString(2).split(""+ADDRESS_DELIMITER, -1);
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 {
byte[] derCertData = c.getBlob(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;
return details;
}
public List<ComputerDetails> getAllComputers() {
Cursor c = computerDb.rawQuery("SELECT * FROM "+COMPUTER_TABLE_NAME, null);
LinkedList<ComputerDetails> computerList = new LinkedList<ComputerDetails>();
LinkedList<ComputerDetails> computerList = new LinkedList<>();
while (c.moveToNext()) {
ComputerDetails details = new ComputerDetails();
details.name = c.getString(0);
String uuidStr = c.getString(1);
try {
details.uuid = UUID.fromString(uuidStr);
} catch (IllegalArgumentException e) {
// We'll delete this entry
LimeLog.severe("DB: Corrupted UUID for "+details.name);
}
try {
details.localIp = InetAddress.getByAddress(c.getBlob(2));
} catch (UnknownHostException e) {
// We'll delete this entry
LimeLog.severe("DB: Corrupted local IP for "+details.name);
}
try {
details.remoteIp = InetAddress.getByAddress(c.getBlob(3));
} catch (UnknownHostException e) {
// We'll delete this entry
LimeLog.severe("DB: Corrupted remote IP for "+details.name);
}
details.macAddress = c.getString(4);
// This signifies we don't have dynamic state (like pair state)
details.state = ComputerDetails.State.UNKNOWN;
details.reachability = ComputerDetails.Reachability.UNKNOWN;
// If a field is corrupt or missing, skip the database entry
if (details.uuid == null || details.localIp == null || details.remoteIp == null ||
details.macAddress == null) {
continue;
}
computerList.add(details);
computerList.add(getComputerFromCursor(c));
}
c.close();
@@ -117,53 +147,17 @@ public class ComputerDatabaseManager {
return computerList;
}
public ComputerDetails getComputerByName(String name) {
Cursor c = computerDb.rawQuery("SELECT * FROM "+COMPUTER_TABLE_NAME+" WHERE "+COMPUTER_NAME_COLUMN_NAME+"='"+name+"'", null);
ComputerDetails details = new ComputerDetails();
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();
return null;
}
details.name = c.getString(0);
String uuidStr = c.getString(1);
try {
details.uuid = UUID.fromString(uuidStr);
} catch (IllegalArgumentException e) {
// We'll delete this entry
LimeLog.severe("DB: Corrupted UUID for "+details.name);
}
try {
details.localIp = InetAddress.getByAddress(c.getBlob(2));
} catch (UnknownHostException e) {
// We'll delete this entry
LimeLog.severe("DB: Corrupted local IP for "+details.name);
}
try {
details.remoteIp = InetAddress.getByAddress(c.getBlob(3));
} catch (UnknownHostException e) {
// We'll delete this entry
LimeLog.severe("DB: Corrupted remote IP for "+details.name);
}
details.macAddress = c.getString(4);
ComputerDetails details = getComputerFromCursor(c);
c.close();
details.state = ComputerDetails.State.UNKNOWN;
details.reachability = ComputerDetails.Reachability.UNKNOWN;
// If a field is corrupt or missing, delete the database entry
if (details.uuid == null || details.localIp == null || details.remoteIp == null ||
details.macAddress == null) {
deleteComputer(details.name);
return null;
}
return details;
}
}

View File

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

View File

@@ -3,39 +3,57 @@ package com.limelight.computers;
import java.io.IOException;
import java.io.OutputStream;
import java.io.StringReader;
import java.net.Inet4Address;
import java.net.InetAddress;
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 java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
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;
import com.limelight.utils.NetHelper;
import com.limelight.utils.ServerHelper;
import android.app.Service;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.net.ConnectivityManager;
import android.net.Network;
import android.net.NetworkCapabilities;
import android.os.Binder;
import android.os.Build;
import android.os.IBinder;
import android.os.SystemClock;
import org.xmlpull.v1.XmlPullParserException;
public class ComputerManagerService extends Service {
private static final int SERVERINFO_POLLING_PERIOD_MS = 3000;
private static final int SERVERINFO_POLLING_PERIOD_MS = 1500;
private static final int APPLIST_POLLING_PERIOD_MS = 30000;
private static final int APPLIST_FAILED_POLLING_RETRY_MS = 2000;
private static final int MDNS_QUERY_PERIOD_MS = 1000;
private static final int FAST_POLL_TIMEOUT = 500;
private static final int OFFLINE_POLL_TRIES = 3;
private static final int FAST_POLL_TIMEOUT = 1000;
private static final int OFFLINE_POLL_TRIES = 5;
private static final int INITIAL_POLL_TRIES = 2;
private static final int EMPTY_LIST_THRESHOLD = 3;
private static final int POLL_DATA_TTL_MS = 30000;
private final ComputerManagerBinder binder = new ComputerManagerBinder();
@@ -43,10 +61,11 @@ public class ComputerManagerService extends Service {
private final AtomicInteger dbRefCount = new AtomicInteger(0);
private IdentityManager idManager;
private final LinkedList<PollingTuple> pollingTuples = new LinkedList<PollingTuple>();
private final LinkedList<PollingTuple> pollingTuples = new LinkedList<>();
private ComputerManagerListener listener = null;
private final AtomicInteger activePolls = new AtomicInteger(0);
private boolean pollingActive = false;
private final Lock defaultNetworkLock = new ReentrantLock();
private DiscoveryService.DiscoveryBinder discoveryBinder;
private final ServiceConnection discoveryServiceConnection = new ServiceConnection() {
@@ -74,18 +93,21 @@ public class ComputerManagerService extends Service {
return false;
}
final int pollTriesBeforeOffline = details.state == ComputerDetails.State.UNKNOWN ?
INITIAL_POLL_TRIES : OFFLINE_POLL_TRIES;
activePolls.incrementAndGet();
// Poll the machine
try {
if (!pollComputer(details)) {
if (!newPc && offlineCount < OFFLINE_POLL_TRIES) {
if (!newPc && offlineCount < pollTriesBeforeOffline) {
// Return without calling the listener
releaseLocalDatabaseReference();
return false;
}
details.state = ComputerDetails.State.OFFLINE;
details.reachability = ComputerDetails.Reachability.OFFLINE;
}
} catch (InterruptedException e) {
releaseLocalDatabaseReference();
@@ -96,17 +118,39 @@ 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 {
try {
// If the active address is a site-local address (RFC 1918),
// then use STUN to populate the external address field if
// it's not set already.
if (details.remoteAddress == null) {
InetAddress addr = InetAddress.getByName(details.activeAddress);
if (addr.isSiteLocalAddress()) {
populateExternalAddress(details);
}
}
} catch (UnknownHostException ignored) {}
dbManager.updateComputer(details);
}
}
// Don't call the listener if this is a failed lookup of a new PC
@@ -118,32 +162,35 @@ public class ComputerManagerService extends Service {
return true;
}
private Thread createPollingThread(final ComputerDetails details) {
private Thread createPollingThread(final PollingTuple tuple) {
Thread t = new Thread() {
@Override
public void run() {
int offlineCount = 0;
while (!isInterrupted() && pollingActive) {
while (!isInterrupted() && pollingActive && tuple.thread == this) {
try {
// Check if this poll has modified the details
if (!runPoll(details, false, offlineCount)) {
LimeLog.warning(details.name + " is offline (try " + offlineCount + ")");
offlineCount++;
}
else {
offlineCount = 0;
// Only allow one request to the machine at a time
synchronized (tuple.networkLock) {
// Check if this poll has modified the details
if (!runPoll(tuple.computer, false, offlineCount)) {
LimeLog.warning(tuple.computer.name + " is offline (try " + offlineCount + ")");
offlineCount++;
} else {
tuple.lastSuccessfulPollMs = SystemClock.elapsedRealtime();
offlineCount = 0;
}
}
// Wait until the next polling interval
Thread.sleep(SERVERINFO_POLLING_PERIOD_MS / ((offlineCount > 0) ? 2 : 1));
Thread.sleep(SERVERINFO_POLLING_PERIOD_MS);
} catch (InterruptedException e) {
break;
}
}
}
};
t.setName("Polling thread for "+details.localIp.getHostAddress());
t.setName("Polling thread for " + tuple.computer.name);
return t;
}
@@ -160,12 +207,18 @@ public class ComputerManagerService extends Service {
synchronized (pollingTuples) {
for (PollingTuple tuple : pollingTuples) {
// Enforce the poll data TTL
if (SystemClock.elapsedRealtime() - tuple.lastSuccessfulPollMs > POLL_DATA_TTL_MS) {
LimeLog.info("Timing out polled state for "+tuple.computer.name);
tuple.computer.state = ComputerDetails.State.UNKNOWN;
}
// Report this computer initially
listener.notifyComputerUpdated(tuple.computer);
// This polling thread might already be there
if (tuple.thread == null) {
// Report this computer initially
listener.notifyComputerUpdated(tuple.computer);
tuple.thread = createPollingThread(tuple.computer);
tuple.thread = createPollingThread(tuple);
tuple.thread.start();
}
}
@@ -192,12 +245,12 @@ public class ComputerManagerService extends Service {
}
}
public boolean addComputerBlocking(InetAddress addr) {
return ComputerManagerService.this.addComputerBlocking(addr);
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() {
@@ -213,7 +266,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)) {
@@ -224,12 +277,28 @@ public class ComputerManagerService extends Service {
return null;
}
public void invalidateStateForComputer(String uuid) {
synchronized (pollingTuples) {
for (PollingTuple tuple : pollingTuples) {
if (uuid.equals(tuple.computer.uuid)) {
// We need the network lock to prevent a concurrent poll
// from wiping this change out
synchronized (tuple.networkLock) {
tuple.computer.state = ComputerDetails.State.UNKNOWN;
}
}
}
}
}
}
@Override
public boolean onUnbind(Intent intent) {
// Stop mDNS autodiscovery
discoveryBinder.stopDiscovery();
if (discoveryBinder != null) {
// Stop mDNS autodiscovery
discoveryBinder.stopDiscovery();
}
// Stop polling
pollingActive = false;
@@ -249,12 +318,91 @@ public class ComputerManagerService extends Service {
return false;
}
private void populateExternalAddress(ComputerDetails details) {
boolean boundToNetwork = false;
boolean activeNetworkIsVpn = NetHelper.isActiveNetworkVpn(this);
ConnectivityManager connMgr = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE);
// Check if we're currently connected to a VPN which may send our
// STUN request from an unexpected interface
if (activeNetworkIsVpn) {
// Acquire the default network lock since we could be changing global process state
defaultNetworkLock.lock();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
// On Lollipop or later, we can bind our process to the underlying interface
// to ensure our STUN request goes out on that interface or not at all (which is
// preferable to getting a VPN endpoint address back).
Network[] networks = connMgr.getAllNetworks();
for (Network net : networks) {
NetworkCapabilities netCaps = connMgr.getNetworkCapabilities(net);
if (netCaps != null) {
if (!netCaps.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) &&
!netCaps.hasTransport(NetworkCapabilities.TRANSPORT_VPN)) {
// This network looks like an underlying multicast-capable transport,
// so let's guess that it's probably where our mDNS response came from.
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
if (connMgr.bindProcessToNetwork(net)) {
boundToNetwork = true;
break;
}
}
else if (ConnectivityManager.setProcessDefaultNetwork(net)) {
boundToNetwork = true;
break;
}
}
}
}
}
}
// Perform the STUN request if we're not on a VPN or if we bound to a network
if (!activeNetworkIsVpn || boundToNetwork) {
details.remoteAddress = NvConnection.findExternalAddressForMdns("stun.moonlight-stream.org", 3478);
}
// Unbind from the network
if (boundToNetwork) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
connMgr.bindProcessToNetwork(null);
}
else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
ConnectivityManager.setProcessDefaultNetwork(null);
}
}
// Unlock the network state
if (activeNetworkIsVpn) {
defaultNetworkLock.unlock();
}
}
private MdnsDiscoveryListener createDiscoveryListener() {
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) {
populateExternalAddress(details);
}
}
if (computer.getIpv6Address() != null) {
details.ipv6Address = computer.getIpv6Address().getHostAddress();
}
// Kick off a serverinfo poll on this machine
addComputerBlocking(computer.getAddress());
if (!addComputerBlocking(details)) {
LimeLog.warning("Auto-discovered PC failed to respond: "+details);
}
}
@Override
@@ -275,14 +423,12 @@ public class ComputerManagerService extends Service {
for (PollingTuple tuple : pollingTuples) {
// Check if this is the same computer
if (tuple.computer.uuid.equals(details.uuid)) {
// Update details anyway in case this machine has been re-added by IP
// after not being reachable by our existing information
tuple.computer.localIp = details.localIp;
tuple.computer.remoteIp = details.remoteIp;
// 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) {
tuple.thread = createPollingThread(details);
tuple.thread = createPollingThread(tuple);
tuple.thread.start();
}
@@ -292,7 +438,10 @@ public class ComputerManagerService extends Service {
}
// If we got here, we didn't find an entry
PollingTuple tuple = new PollingTuple(details, pollingActive ? createPollingThread(details) : null);
PollingTuple tuple = new PollingTuple(details, null);
if (pollingActive) {
tuple.thread = createPollingThread(tuple);
}
pollingTuples.add(tuple);
if (tuple.thread != null) {
tuple.thread.start();
@@ -300,15 +449,26 @@ public class ComputerManagerService extends Service {
}
}
public boolean addComputerBlocking(InetAddress addr) {
// Setup a placeholder
ComputerDetails fakeDetails = new ComputerDetails();
fakeDetails.localIp = addr;
fakeDetails.remoteIp = 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;
}
@@ -326,21 +486,22 @@ public class ComputerManagerService extends Service {
}
}
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();
tuple.thread = null;
}
pollingTuples.remove(tuple);
break;
@@ -366,33 +527,51 @@ public class ComputerManagerService extends Service {
}
}
private ComputerDetails tryPollIp(ComputerDetails details, InetAddress ipAddr) {
private ComputerDetails tryPollIp(ComputerDetails details, String address) {
// Fast poll this address first to determine if we can connect at the TCP layer
if (!fastPollIp(address)) {
return null;
}
try {
NvHTTP http = new NvHTTP(ipAddr, 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;
}
}
// Just try to establish a TCP connection to speculatively detect a running
// GFE server
private boolean fastPollIp(InetAddress addr) {
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(addr, NvHTTP.PORT), FAST_POLL_TIMEOUT);
s.connect(new InetSocketAddress(address, NvHTTP.HTTPS_PORT), FAST_POLL_TIMEOUT);
s.close();
return true;
} catch (IOException e) {
@@ -400,11 +579,11 @@ public class ComputerManagerService extends Service {
}
}
private void startFastPollThread(final InetAddress addr, final boolean[] info) {
private void startFastPollThread(final String address, final boolean[] info) {
Thread t = new Thread() {
@Override
public void run() {
boolean pollRes = fastPollIp(addr);
boolean pollRes = fastPollIp(address);
synchronized (info) {
info[0] = true; // Done
@@ -414,16 +593,20 @@ public class ComputerManagerService extends Service {
}
}
};
t.setName("Fast Poll - "+addr.getHostAddress());
t.setName("Fast Poll - "+address);
t.start();
}
private ComputerDetails.Reachability fastPollPc(final InetAddress local, final InetAddress remote) 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(local, localInfo);
startFastPollThread(remote, remoteInfo);
startFastPollThread(localAddress, localInfo);
startFastPollThread(remoteAddress, remoteInfo);
startFastPollThread(manualAddress, manualInfo);
startFastPollThread(ipv6Address, ipv6Info);
// Check local first
synchronized (localInfo) {
@@ -432,92 +615,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);
}
if (ipv6Info[1]) {
return ipv6Address;
}
}
return null;
}
private boolean pollComputer(ComputerDetails details) throws InterruptedException {
ComputerDetails polledDetails;
ComputerDetails.Reachability reachability;
// If the local address is routable across the Internet,
// always consider this PC remote to be conservative
if (details.localIp.equals(details.remoteIp)) {
reachability = 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.localIp+", "+details.remoteIp+")");
reachability = fastPollPc(details.localIp, details.remoteIp);
LimeLog.info("Fast poll for "+details.name+" returned "+reachability.toString());
}
// 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 (reachability == ComputerDetails.Reachability.OFFLINE) {
if (candidateAddress == null) {
return false;
}
boolean localFirst = (reachability == ComputerDetails.Reachability.LOCAL);
// 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;
}
}
}
if (localFirst) {
polledDetails = tryPollIp(details, details.localIp);
if (polledDetails != null) {
details.update(polledDetails);
return true;
}
else {
polledDetails = tryPollIp(details, details.remoteIp);
}
if (polledDetails == null && !details.localIp.equals(details.remoteIp)) {
// Failed, so let's try the fallback
if (!localFirst) {
polledDetails = tryPollIp(details, details.localIp);
}
else {
polledDetails = tryPollIp(details, details.remoteIp);
}
// The fallback poll worked
if (polledDetails != null) {
polledDetails.reachability = !localFirst ? ComputerDetails.Reachability.LOCAL :
ComputerDetails.Reachability.REMOTE;
}
}
else if (polledDetails != null) {
polledDetails.reachability = localFirst ? ComputerDetails.Reachability.LOCAL :
ComputerDetails.Reachability.REMOTE;
}
// Machine was unreachable both tries
if (polledDetails == null) {
return false;
}
// Save the old MAC address
String savedMacAddress = details.macAddress;
// If we got here, it's reachable
details.update(polledDetails);
// 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;
}
return true;
}
@Override
@@ -568,6 +749,7 @@ public class ComputerManagerService extends Service {
private Thread thread;
private final ComputerDetails computer;
private final Object pollEvent = new Object();
private boolean receivedAppList = false;
public ApplistPoller(ComputerDetails computer) {
this.computer = computer;
@@ -582,7 +764,15 @@ public class ComputerManagerService extends Service {
private boolean waitPollingDelay() {
try {
synchronized (pollEvent) {
pollEvent.wait(APPLIST_POLLING_PERIOD_MS);
if (receivedAppList) {
// If we've already reported an app list successfully,
// wait the full polling period
pollEvent.wait(APPLIST_POLLING_PERIOD_MS);
}
else {
// If we've failed to get an app list so far, retry much earlier
pollEvent.wait(APPLIST_FAILED_POLLING_RETRY_MS);
}
}
} catch (InterruptedException e) {
return false;
@@ -591,15 +781,27 @@ public class ComputerManagerService extends Service {
return thread != null && !thread.isInterrupted();
}
private PollingTuple getPollingTuple(ComputerDetails details) {
synchronized (pollingTuples) {
for (PollingTuple tuple : pollingTuples) {
if (details.uuid.equals(tuple.computer.uuid)) {
return tuple;
}
}
}
return null;
}
public void start() {
thread = new Thread() {
@Override
public void run() {
int emptyAppListResponses = 0;
do {
InetAddress selectedAddr;
// 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);
}
@@ -611,25 +813,40 @@ public class ComputerManagerService extends Service {
continue;
}
if (computer.reachability == ComputerDetails.Reachability.LOCAL) {
selectedAddr = computer.localIp;
}
else {
selectedAddr = computer.remoteIp;
}
NvHTTP http = new NvHTTP(selectedAddr, idManager.getUniqueId(),
null, PlatformBinding.getCryptoProvider(ComputerManagerService.this));
PollingTuple tuple = getPollingTuple(computer);
try {
// Query the app list from the server
String appList = http.getAppListRaw();
NvHTTP http = new NvHTTP(ServerHelper.getCurrentAddressFromComputer(computer), idManager.getUniqueId(),
computer.serverCert, PlatformBinding.getCryptoProvider(ComputerManagerService.this));
String appList;
if (tuple != null) {
// If we're polling this machine too, grab the network lock
// while doing the app list request to prevent other requests
// from being issued in the meantime.
synchronized (tuple.networkLock) {
appList = http.getAppListRaw();
}
}
else {
// No polling is happening now, so we just call it directly
appList = http.getAppListRaw();
}
List<NvApp> list = NvHTTP.getAppListByReader(new StringReader(appList));
if (appList != null && !appList.isEmpty() && !list.isEmpty()) {
if (list.isEmpty()) {
LimeLog.warning("Empty app list received from "+computer.uuid);
// The app list might actually be empty, so if we get an empty response a few times
// in a row, we'll go ahead and believe it.
emptyAppListResponses++;
}
if (!appList.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();
@@ -638,11 +855,17 @@ public class ComputerManagerService extends Service {
if (cacheOut != null) {
cacheOut.close();
}
} catch (IOException e) {}
} catch (IOException ignored) {}
}
// Reset empty count if it wasn't empty this time
if (!list.isEmpty()) {
emptyAppListResponses = 0;
}
// Update the computer
computer.rawAppList = appList;
receivedAppList = true;
// Notify that the app list has been updated
// and ensure that the thread is still active
@@ -650,8 +873,8 @@ public class ComputerManagerService extends Service {
listener.notifyComputerUpdated(computer);
}
}
else {
LimeLog.warning("Empty app list received from "+computer.uuid);
else if (appList.isEmpty()) {
LimeLog.warning("Null app list received from "+computer.uuid);
}
} catch (IOException e) {
e.printStackTrace();
@@ -661,6 +884,7 @@ public class ComputerManagerService extends Service {
} while (waitPollingDelay());
}
};
thread.setName("App list polling thread for " + computer.name);
thread.start();
}
@@ -679,9 +903,22 @@ public class ComputerManagerService extends Service {
class PollingTuple {
public Thread thread;
public final ComputerDetails computer;
public final Object networkLock;
public long lastSuccessfulPollMs;
public PollingTuple(ComputerDetails computer, Thread thread) {
this.computer = computer;
this.thread = thread;
this.networkLock = new Object();
}
}
class ReachabilityTuple {
public final String reachableAddress;
public final ComputerDetails computer;
public ReachabilityTuple(ComputerDetails computer, String reachableAddress) {
this.computer = computer;
this.reachableAddress = reachableAddress;
}
}

View File

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

View File

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

View File

@@ -42,7 +42,7 @@ public class DiscoveryService extends Service {
@Override
public void onCreate() {
WifiManager wifiMgr = (WifiManager) getSystemService(Context.WIFI_SERVICE);
WifiManager wifiMgr = (WifiManager) getApplicationContext().getSystemService(Context.WIFI_SERVICE);
multicastLock = wifiMgr.createMulticastLock("Limelight mDNS");
multicastLock.setReferenceCounted(false);

View File

@@ -1,8 +1,10 @@
package com.limelight.grid;
import android.app.Activity;
import android.content.Context;
import android.graphics.BitmapFactory;
import android.view.View;
import android.widget.ImageView;
import android.widget.ProgressBar;
import android.widget.TextView;
import com.limelight.AppView;
@@ -13,11 +15,14 @@ import com.limelight.grid.assets.DiskAssetLoader;
import com.limelight.grid.assets.MemoryAssetLoader;
import com.limelight.grid.assets.NetworkAssetLoader;
import com.limelight.nvstream.http.ComputerDetails;
import com.limelight.preferences.PreferenceConfiguration;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
@SuppressWarnings("unchecked")
public class AppGridAdapter extends GenericGridAdapter<AppView.AppObject> {
@@ -25,37 +30,89 @@ public class AppGridAdapter extends GenericGridAdapter<AppView.AppObject> {
private static final int SMALL_WIDTH_DP = 100;
private static final int LARGE_WIDTH_DP = 150;
private final CachedAppAssetLoader loader;
private final ComputerDetails computer;
private final String uniqueId;
private final boolean showHiddenApps;
public AppGridAdapter(Activity activity, boolean listMode, boolean small, ComputerDetails computer, String uniqueId) throws KeyManagementException, NoSuchAlgorithmException {
super(activity, listMode ? R.layout.simple_row : (small ? R.layout.app_grid_item_small : R.layout.app_grid_item), R.drawable.image_loading);
private CachedAppAssetLoader loader;
private Set<Integer> hiddenAppIds = new HashSet<>();
private ArrayList<AppView.AppObject> allApps = new ArrayList<>();
int dpi = activity.getResources().getDisplayMetrics().densityDpi;
public AppGridAdapter(Context context, PreferenceConfiguration prefs, ComputerDetails computer, String uniqueId, boolean showHiddenApps) {
super(context, getLayoutIdForPreferences(prefs));
this.computer = computer;
this.uniqueId = uniqueId;
this.showHiddenApps = showHiddenApps;
updateLayoutWithPreferences(context, prefs);
}
public void updateHiddenApps(Set<Integer> newHiddenAppIds, boolean hideImmediately) {
this.hiddenAppIds.clear();
this.hiddenAppIds.addAll(newHiddenAppIds);
if (hideImmediately) {
// Reconstruct the itemList with the new hidden app set
itemList.clear();
for (AppView.AppObject app : allApps) {
app.isHidden = hiddenAppIds.contains(app.app.getAppId());
if (!app.isHidden || showHiddenApps) {
itemList.add(app);
}
}
}
else {
// Just update the isHidden state to show the correct UI indication
for (AppView.AppObject app : allApps) {
app.isHidden = hiddenAppIds.contains(app.app.getAppId());
}
}
notifyDataSetChanged();
}
private static int getLayoutIdForPreferences(PreferenceConfiguration prefs) {
if (prefs.smallIconMode) {
return R.layout.app_grid_item_small;
}
else {
return R.layout.app_grid_item;
}
}
public void updateLayoutWithPreferences(Context context, PreferenceConfiguration prefs) {
int dpi = context.getResources().getDisplayMetrics().densityDpi;
int dp;
if (small) {
if (prefs.smallIconMode) {
dp = SMALL_WIDTH_DP;
}
else {
dp = LARGE_WIDTH_DP;
}
double scalingDivisor = ART_WIDTH_PX / (dp * (dpi / 160));
double scalingDivisor = ART_WIDTH_PX / (dp * (dpi / 160.0));
if (scalingDivisor < 1.0) {
// We don't want to make them bigger before draw-time
scalingDivisor = 1.0;
}
LimeLog.info("Art scaling divisor: " + scalingDivisor);
BitmapFactory.Options options = new BitmapFactory.Options();
options.inSampleSize = (int) scalingDivisor;
if (loader != null) {
// Cancel operations on the old loader
cancelQueuedOperations();
}
this.loader = new CachedAppAssetLoader(computer, scalingDivisor,
new NetworkAssetLoader(context, uniqueId),
new MemoryAssetLoader(),
new DiskAssetLoader(context.getCacheDir()),
BitmapFactory.decodeResource(activity.getResources(),
R.drawable.image_loading, options));
new DiskAssetLoader(context),
BitmapFactory.decodeResource(context.getResources(), R.drawable.no_app_image));
// This will trigger the view to reload with the new layout
setLayoutId(getLayoutIdForPreferences(prefs));
}
public void cancelQueuedOperations() {
@@ -64,52 +121,64 @@ public class AppGridAdapter extends GenericGridAdapter<AppView.AppObject> {
loader.freeCacheMemory();
}
private void sortList() {
Collections.sort(itemList, new Comparator<AppView.AppObject>() {
private static void sortList(List<AppView.AppObject> list) {
Collections.sort(list, new Comparator<AppView.AppObject>() {
@Override
public int compare(AppView.AppObject lhs, AppView.AppObject rhs) {
return lhs.app.getAppName().compareTo(rhs.app.getAppName());
return lhs.app.getAppName().toLowerCase().compareTo(rhs.app.getAppName().toLowerCase());
}
});
}
public void addApp(AppView.AppObject app) {
// Queue a request to fetch this bitmap into cache
loader.queueCacheLoad(app.app);
// Update hidden state
app.isHidden = hiddenAppIds.contains(app.app.getAppId());
// Add the app to our sorted list
itemList.add(app);
sortList();
// Always add the app to the all apps list
allApps.add(app);
sortList(allApps);
// Add the app to the adapter data if it's not hidden
if (showHiddenApps || !app.isHidden) {
// Queue a request to fetch this bitmap into cache
loader.queueCacheLoad(app.app);
// Add the app to our sorted list
itemList.add(app);
sortList(itemList);
}
}
public void removeApp(AppView.AppObject app) {
itemList.remove(app);
allApps.remove(app);
}
public boolean populateImageView(ImageView imgView, AppView.AppObject obj) {
@Override
public void clear() {
super.clear();
allApps.clear();
}
@Override
public void populateView(View parentView, ImageView imgView, ProgressBar prgView, TextView txtView, ImageView overlayView, AppView.AppObject obj) {
// Let the cached asset loader handle it
loader.populateImageView(obj.app, imgView);
return true;
}
loader.populateImageView(obj.app, imgView, txtView);
@Override
public boolean populateTextView(TextView txtView, AppView.AppObject obj) {
// Select the text view so it starts marquee mode
txtView.setSelected(true);
// Return false to use the app's toString method
return false;
}
@Override
public boolean populateOverlayView(ImageView overlayView, AppView.AppObject obj) {
if (obj.app.getIsRunning()) {
if (obj.isRunning) {
// Show the play button overlay
overlayView.setImageResource(R.drawable.play);
return true;
overlayView.setImageResource(R.drawable.ic_play);
overlayView.setVisibility(View.VISIBLE);
}
else {
overlayView.setVisibility(View.GONE);
}
// No overlay
return false;
if (obj.isHidden) {
parentView.setAlpha(0.40f);
}
else {
parentView.setAlpha(1.0f);
}
}
}

View File

@@ -6,6 +6,7 @@ import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.ImageView;
import android.widget.ProgressBar;
import android.widget.TextView;
import com.limelight.R;
@@ -14,19 +15,26 @@ import java.util.ArrayList;
public abstract class GenericGridAdapter<T> extends BaseAdapter {
protected final Context context;
protected final int defaultImageRes;
protected final int layoutId;
protected final ArrayList<T> itemList = new ArrayList<T>();
protected final LayoutInflater inflater;
private int layoutId;
final ArrayList<T> itemList = new ArrayList<>();
private final LayoutInflater inflater;
public GenericGridAdapter(Context context, int layoutId, int defaultImageRes) {
GenericGridAdapter(Context context, int layoutId) {
this.context = context;
this.layoutId = layoutId;
this.defaultImageRes = defaultImageRes;
this.inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
}
void setLayoutId(int layoutId) {
if (layoutId != this.layoutId) {
this.layoutId = layoutId;
// Force the view to be redrawn with the new layout
notifyDataSetInvalidated();
}
}
public void clear() {
itemList.clear();
}
@@ -46,9 +54,7 @@ public abstract class GenericGridAdapter<T> extends BaseAdapter {
return i;
}
public abstract boolean populateImageView(ImageView imgView, T obj);
public abstract boolean populateTextView(TextView txtView, T obj);
public abstract boolean populateOverlayView(ImageView overlayView, T obj);
public abstract void populateView(View parentView, ImageView imgView, ProgressBar prgView, TextView txtView, ImageView overlayView, T obj);
@Override
public View getView(int i, View convertView, ViewGroup viewGroup) {
@@ -56,26 +62,12 @@ public abstract class GenericGridAdapter<T> extends BaseAdapter {
convertView = inflater.inflate(layoutId, viewGroup, false);
}
ImageView imgView = (ImageView) convertView.findViewById(R.id.grid_image);
ImageView overlayView = (ImageView) convertView.findViewById(R.id.grid_overlay);
TextView txtView = (TextView) convertView.findViewById(R.id.grid_text);
ImageView imgView = convertView.findViewById(R.id.grid_image);
ImageView overlayView = convertView.findViewById(R.id.grid_overlay);
TextView txtView = convertView.findViewById(R.id.grid_text);
ProgressBar prgView = convertView.findViewById(R.id.grid_spinner);
if (imgView != null) {
if (!populateImageView(imgView, itemList.get(i))) {
imgView.setImageResource(defaultImageRes);
}
}
if (!populateTextView(txtView, itemList.get(i))) {
txtView.setText(itemList.get(i).toString());
}
if (overlayView != null) {
if (!populateOverlayView(overlayView, itemList.get(i))) {
overlayView.setVisibility(View.INVISIBLE);
}
else {
overlayView.setVisibility(View.VISIBLE);
}
}
populateView(convertView, imgView, prgView, txtView, overlayView, itemList.get(i));
return convertView;
}

View File

@@ -1,20 +1,33 @@
package com.limelight.grid;
import android.content.Context;
import android.view.View;
import android.widget.ImageView;
import android.widget.ProgressBar;
import android.widget.TextView;
import com.limelight.PcView;
import com.limelight.R;
import com.limelight.nvstream.http.ComputerDetails;
import com.limelight.nvstream.http.PairingManager;
import com.limelight.preferences.PreferenceConfiguration;
import java.util.Collections;
import java.util.Comparator;
public class PcGridAdapter extends GenericGridAdapter<PcView.ComputerObject> {
public PcGridAdapter(Context context, boolean listMode, boolean small) {
super(context, listMode ? R.layout.simple_row : (small ? R.layout.pc_grid_item_small : R.layout.pc_grid_item), R.drawable.computer);
public PcGridAdapter(Context context, PreferenceConfiguration prefs) {
super(context, getLayoutIdForPreferences(prefs));
}
private static int getLayoutIdForPreferences(PreferenceConfiguration prefs) {
return R.layout.pc_grid_item;
}
public void updateLayoutWithPreferences(Context context, PreferenceConfiguration prefs) {
// This will trigger the view to reload with the new layout
setLayoutId(getLayoutIdForPreferences(prefs));
}
public void addComputer(PcView.ComputerObject computer) {
@@ -26,7 +39,7 @@ public class PcGridAdapter extends GenericGridAdapter<PcView.ComputerObject> {
Collections.sort(itemList, new Comparator<PcView.ComputerObject>() {
@Override
public int compare(PcView.ComputerObject lhs, PcView.ComputerObject rhs) {
return lhs.details.name.compareTo(rhs.details.name);
return lhs.details.name.toLowerCase().compareTo(rhs.details.name.toLowerCase());
}
});
}
@@ -36,40 +49,45 @@ public class PcGridAdapter extends GenericGridAdapter<PcView.ComputerObject> {
}
@Override
public boolean populateImageView(ImageView imgView, PcView.ComputerObject obj) {
if (obj.details.reachability != ComputerDetails.Reachability.OFFLINE) {
public void populateView(View parentView, ImageView imgView, ProgressBar prgView, TextView txtView, ImageView overlayView, PcView.ComputerObject obj) {
imgView.setImageResource(R.drawable.ic_computer);
if (obj.details.state == ComputerDetails.State.ONLINE) {
imgView.setAlpha(1.0f);
}
else {
imgView.setAlpha(0.4f);
}
// Return false to use the default drawable
return false;
}
if (obj.details.state == ComputerDetails.State.UNKNOWN) {
prgView.setVisibility(View.VISIBLE);
}
else {
prgView.setVisibility(View.INVISIBLE);
}
@Override
public boolean populateTextView(TextView txtView, PcView.ComputerObject obj) {
if (obj.details.reachability != ComputerDetails.Reachability.OFFLINE) {
txtView.setText(obj.details.name);
if (obj.details.state == ComputerDetails.State.ONLINE) {
txtView.setAlpha(1.0f);
}
else {
txtView.setAlpha(0.4f);
}
// Return false to use the computer's toString method
return false;
}
@Override
public boolean populateOverlayView(ImageView overlayView, PcView.ComputerObject obj) {
if (obj.details.reachability == ComputerDetails.Reachability.UNKNOWN) {
// Still refreshing this PC so display the overlay
overlayView.setImageResource(R.drawable.image_loading);
return true;
if (obj.details.state == ComputerDetails.State.OFFLINE) {
overlayView.setImageResource(R.drawable.ic_pc_offline);
overlayView.setAlpha(0.4f);
overlayView.setVisibility(View.VISIBLE);
}
// 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);
overlayView.setVisibility(View.VISIBLE);
}
else {
overlayView.setVisibility(View.GONE);
}
// No overlay
return false;
}
}

View File

@@ -5,11 +5,17 @@ import android.graphics.Bitmap;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.os.AsyncTask;
import android.view.View;
import android.view.animation.Animation;
import android.view.animation.AnimationUtils;
import android.widget.ImageView;
import android.widget.TextView;
import com.limelight.R;
import com.limelight.nvstream.http.ComputerDetails;
import com.limelight.nvstream.http.NvApp;
import java.io.IOException;
import java.io.InputStream;
import java.lang.ref.WeakReference;
import java.util.concurrent.LinkedBlockingQueue;
@@ -49,16 +55,18 @@ public class CachedAppAssetLoader {
private final MemoryAssetLoader memoryLoader;
private final DiskAssetLoader diskLoader;
private final Bitmap placeholderBitmap;
private final Bitmap noAppImageBitmap;
public CachedAppAssetLoader(ComputerDetails computer, double scalingDivider,
NetworkAssetLoader networkLoader, MemoryAssetLoader memoryLoader,
DiskAssetLoader diskLoader, Bitmap placeholderBitmap) {
DiskAssetLoader diskLoader, Bitmap noAppImageBitmap) {
this.computer = computer;
this.scalingDivider = scalingDivider;
this.networkLoader = networkLoader;
this.memoryLoader = memoryLoader;
this.diskLoader = diskLoader;
this.placeholderBitmap = placeholderBitmap;
this.noAppImageBitmap = noAppImageBitmap;
this.placeholderBitmap = Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888);
}
public void cancelBackgroundLoads() {
@@ -84,7 +92,7 @@ public class CachedAppAssetLoader {
memoryLoader.clearCache();
}
private Bitmap doNetworkAssetLoad(LoaderTuple tuple, LoaderTask task) {
private ScaledBitmap doNetworkAssetLoad(LoaderTuple tuple, LoaderTask task) {
// Try 3 times
for (int i = 0; i < 3; i++) {
// Check again whether we've been cancelled or the image view is gone
@@ -97,9 +105,18 @@ public class CachedAppAssetLoader {
// Write the stream straight to disk
diskLoader.populateCacheWithStream(tuple, in);
// Close the network input stream
try {
in.close();
} catch (IOException ignored) {}
// If there's a task associated with this load, we should return the bitmap
if (task != null) {
return diskLoader.loadBitmapFromCache(tuple, (int) scalingDivider);
// If the cached bitmap is valid, return it. Otherwise, we'll try the load again
ScaledBitmap bmp = diskLoader.loadBitmapFromCache(tuple, (int) scalingDivider);
if (bmp != null) {
return bmp;
}
}
else {
// Otherwise it's a background load and we return nothing
@@ -118,27 +135,29 @@ public class CachedAppAssetLoader {
return null;
}
private class LoaderTask extends AsyncTask<LoaderTuple, Void, Bitmap> {
private class LoaderTask extends AsyncTask<LoaderTuple, Void, ScaledBitmap> {
private final WeakReference<ImageView> imageViewRef;
private final WeakReference<TextView> textViewRef;
private final boolean diskOnly;
private LoaderTuple tuple;
public LoaderTask(ImageView imageView, boolean diskOnly) {
this.imageViewRef = new WeakReference<ImageView>(imageView);
public LoaderTask(ImageView imageView, TextView textView, boolean diskOnly) {
this.imageViewRef = new WeakReference<>(imageView);
this.textViewRef = new WeakReference<>(textView);
this.diskOnly = diskOnly;
}
@Override
protected Bitmap doInBackground(LoaderTuple... params) {
protected ScaledBitmap doInBackground(LoaderTuple... params) {
tuple = params[0];
// Check whether it has been cancelled or the image view is gone
if (isCancelled() || imageViewRef.get() == null) {
// Check whether it has been cancelled or the views are gone
if (isCancelled() || imageViewRef.get() == null || textViewRef.get() == null) {
return null;
}
Bitmap bmp = diskLoader.loadBitmapFromCache(tuple, (int) scalingDivider);
ScaledBitmap bmp = diskLoader.loadBitmapFromCache(tuple, (int) scalingDivider);
if (bmp == null) {
if (!diskOnly) {
// Try to load the asset from the network
@@ -167,32 +186,61 @@ public class CachedAppAssetLoader {
// If the current loader task for this view isn't us, do nothing
final ImageView imageView = imageViewRef.get();
final TextView textView = textViewRef.get();
if (getLoaderTask(imageView) == this) {
// Set off another loader task on the network executor
LoaderTask task = new LoaderTask(imageView, false);
AsyncDrawable asyncDrawable = new AsyncDrawable(imageView.getResources(), placeholderBitmap, task);
imageView.setAlpha(1.0f);
// Set off another loader task on the network executor. This time our AsyncDrawable
// will use the app image placeholder bitmap, rather than an empty bitmap.
LoaderTask task = new LoaderTask(imageView, textView, false);
AsyncDrawable asyncDrawable = new AsyncDrawable(imageView.getResources(), noAppImageBitmap, task);
imageView.setImageDrawable(asyncDrawable);
imageView.startAnimation(AnimationUtils.loadAnimation(imageView.getContext(), R.anim.boxart_fadein));
imageView.setVisibility(View.VISIBLE);
textView.setVisibility(View.VISIBLE);
task.executeOnExecutor(networkExecutor, tuple);
}
}
@Override
protected void onPostExecute(Bitmap bitmap) {
protected void onPostExecute(final ScaledBitmap bitmap) {
// Do nothing if cancelled
if (isCancelled()) {
return;
}
final ImageView imageView = imageViewRef.get();
final TextView textView = textViewRef.get();
if (getLoaderTask(imageView) == this) {
// Set the bitmap
// Fade in the box art
if (bitmap != null) {
imageView.setImageBitmap(bitmap);
}
// Show the text if it's a placeholder
textView.setVisibility(isBitmapPlaceholder(bitmap) ? View.VISIBLE : View.GONE);
// Show the view
imageView.setAlpha(1.0f);
if (imageView.getVisibility() == View.VISIBLE) {
// Fade out the placeholder first
Animation fadeOutAnimation = AnimationUtils.loadAnimation(imageView.getContext(), R.anim.boxart_fadeout);
fadeOutAnimation.setAnimationListener(new Animation.AnimationListener() {
@Override
public void onAnimationStart(Animation animation) {}
@Override
public void onAnimationEnd(Animation animation) {
// Fade in the new box art
imageView.setImageBitmap(bitmap.bitmap);
imageView.startAnimation(AnimationUtils.loadAnimation(imageView.getContext(), R.anim.boxart_fadein));
}
@Override
public void onAnimationRepeat(Animation animation) {}
});
imageView.startAnimation(fadeOutAnimation);
}
else {
// View is invisible already, so just fade in the new art
imageView.setImageBitmap(bitmap.bitmap);
imageView.startAnimation(AnimationUtils.loadAnimation(imageView.getContext(), R.anim.boxart_fadein));
imageView.setVisibility(View.VISIBLE);
}
}
}
}
}
@@ -203,7 +251,7 @@ public class CachedAppAssetLoader {
public AsyncDrawable(Resources res, Bitmap bitmap,
LoaderTask loaderTask) {
super(res, bitmap);
loaderTaskReference = new WeakReference<LoaderTask>(loaderTask);
loaderTaskReference = new WeakReference<>(loaderTask);
}
public LoaderTask getLoaderTask() {
@@ -270,37 +318,51 @@ public class CachedAppAssetLoader {
});
}
public void populateImageView(NvApp app, ImageView view) {
private boolean isBitmapPlaceholder(ScaledBitmap bitmap) {
return (bitmap == null) ||
(bitmap.originalWidth == 130 && bitmap.originalHeight == 180) || // GFE 2.0
(bitmap.originalWidth == 628 && bitmap.originalHeight == 888); // GFE 3.0
}
public boolean populateImageView(NvApp app, ImageView imgView, TextView textView) {
LoaderTuple tuple = new LoaderTuple(computer, app);
// If there's already a task in progress for this view,
// cancel it. If the task is already loading the same image,
// we return and let that load finish.
if (!cancelPendingLoad(tuple, view)) {
return;
if (!cancelPendingLoad(tuple, imgView)) {
return true;
}
// Always set the name text so we have it if needed later
textView.setText(app.getAppName());
// First, try the memory cache in the current context
Bitmap bmp = memoryLoader.loadBitmapFromCache(tuple);
ScaledBitmap bmp = memoryLoader.loadBitmapFromCache(tuple);
if (bmp != null) {
// Show the bitmap immediately
view.setAlpha(1.0f);
view.setImageBitmap(bmp);
return;
imgView.setVisibility(View.VISIBLE);
imgView.setImageBitmap(bmp.bitmap);
// Show the text if it's a placeholder bitmap
textView.setVisibility(isBitmapPlaceholder(bmp) ? View.VISIBLE : View.GONE);
return true;
}
// If it's not in memory, create an async task to load it. This task will be attached
// via AsyncDrawable to this view.
final LoaderTask task = new LoaderTask(view, true);
final AsyncDrawable asyncDrawable = new AsyncDrawable(view.getResources(), placeholderBitmap, task);
view.setAlpha(0.0f);
view.setImageDrawable(asyncDrawable);
final LoaderTask task = new LoaderTask(imgView, textView, true);
final AsyncDrawable asyncDrawable = new AsyncDrawable(imgView.getResources(), placeholderBitmap, task);
textView.setVisibility(View.INVISIBLE);
imgView.setVisibility(View.INVISIBLE);
imgView.setImageDrawable(asyncDrawable);
// Run the task on our foreground executor
task.executeOnExecutor(foregroundExecutor, tuple);
return false;
}
public class LoaderTuple {
public static class LoaderTuple {
public final ComputerDetails computer;
public final NvApp app;

View File

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

View File

@@ -1,37 +1,74 @@
package com.limelight.grid.assets;
import android.graphics.Bitmap;
import android.util.LruCache;
import com.limelight.LimeLog;
import java.lang.ref.SoftReference;
import java.util.HashMap;
public class MemoryAssetLoader {
private static final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
private static final LruCache<String, Bitmap> memoryCache = new LruCache<String, Bitmap>(maxMemory / 16) {
private static final LruCache<String, ScaledBitmap> memoryCache = new LruCache<String, ScaledBitmap>(maxMemory / 16) {
@Override
protected int sizeOf(String key, Bitmap bitmap) {
protected int sizeOf(String key, ScaledBitmap bitmap) {
// Sizeof returns kilobytes
return bitmap.getByteCount() / 1024;
return bitmap.bitmap.getByteCount() / 1024;
}
@Override
protected void entryRemoved(boolean evicted, String key, ScaledBitmap oldValue, ScaledBitmap newValue) {
super.entryRemoved(evicted, key, oldValue, newValue);
if (evicted) {
// Keep a soft reference around to the bitmap as long as we can
evictionCache.put(key, new SoftReference<>(oldValue));
}
}
};
private static final HashMap<String, SoftReference<ScaledBitmap>> evictionCache = new HashMap<>();
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) {
Bitmap bmp = memoryCache.get(constructKey(tuple));
public ScaledBitmap loadBitmapFromCache(CachedAppAssetLoader.LoaderTuple tuple) {
final String key = constructKey(tuple);
ScaledBitmap bmp = memoryCache.get(key);
if (bmp != null) {
LimeLog.info("Memory cache hit for tuple: "+tuple);
LimeLog.info("LRU cache hit for tuple: "+tuple);
return bmp;
}
return bmp;
SoftReference<ScaledBitmap> bmpRef = evictionCache.get(key);
if (bmpRef != null) {
bmp = bmpRef.get();
if (bmp != null) {
LimeLog.info("Eviction cache hit for tuple: "+tuple);
// Put this entry back into the LRU cache
evictionCache.remove(key);
memoryCache.put(key, bmp);
return bmp;
}
else {
// The data is gone, so remove the dangling SoftReference now
evictionCache.remove(key);
}
}
return null;
}
public void populateCache(CachedAppAssetLoader.LoaderTuple tuple, Bitmap bitmap) {
public void populateCache(CachedAppAssetLoader.LoaderTuple tuple, ScaledBitmap bitmap) {
memoryCache.put(constructKey(tuple), bitmap);
}
public void clearCache() {
// We must evict first because that will push all items into the eviction cache
memoryCache.evictAll();
evictionCache.clear();
}
}

View File

@@ -4,12 +4,11 @@ import android.content.Context;
import com.limelight.LimeLog;
import com.limelight.binding.PlatformBinding;
import com.limelight.nvstream.http.ComputerDetails;
import com.limelight.nvstream.http.NvHTTP;
import com.limelight.utils.ServerHelper;
import java.io.IOException;
import java.io.InputStream;
import java.net.InetAddress;
public class NetworkAssetLoader {
private final Context context;
@@ -21,12 +20,12 @@ public class NetworkAssetLoader {
}
public InputStream getBitmapStream(CachedAppAssetLoader.LoaderTuple tuple) {
NvHTTP http = new NvHTTP(getCurrentAddress(tuple.computer), uniqueId, null, PlatformBinding.getCryptoProvider(context));
InputStream in = null;
try {
NvHTTP http = new NvHTTP(ServerHelper.getCurrentAddressFromComputer(tuple.computer), uniqueId,
tuple.computer.serverCert, PlatformBinding.getCryptoProvider(context));
in = http.getBoxArt(tuple.app);
} catch (IOException e) {}
} catch (IOException ignored) {}
if (in != null) {
LimeLog.info("Network asset load complete: " + tuple);
@@ -37,13 +36,4 @@ public class NetworkAssetLoader {
return in;
}
private static InetAddress getCurrentAddress(ComputerDetails computer) {
if (computer.reachability == ComputerDetails.Reachability.LOCAL) {
return computer.localIp;
}
else {
return computer.remoteIp;
}
}
}

View File

@@ -0,0 +1,18 @@
package com.limelight.grid.assets;
import android.graphics.Bitmap;
public class ScaledBitmap {
public int originalWidth;
public int originalHeight;
public Bitmap bitmap;
public ScaledBitmap() {}
public ScaledBitmap(int originalWidth, int originalHeight, Bitmap bitmap) {
this.originalWidth = originalWidth;
this.originalHeight = originalHeight;
this.bitmap = bitmap;
}
}

View File

@@ -0,0 +1,23 @@
package com.limelight.nvstream;
import java.security.cert.X509Certificate;
import javax.crypto.SecretKey;
public class ConnectionContext {
public String serverAddress;
public X509Certificate serverCert;
public StreamConfiguration streamConfig;
public NvConnectionListener connListener;
public SecretKey riKey;
public int riKeyId;
// This is the version quad from the appversion tag of /serverinfo
public String serverAppVersion;
public String serverGfeVersion;
public int negotiatedWidth, negotiatedHeight;
public boolean negotiatedHdr;
public int videoCapabilities;
}

View File

@@ -0,0 +1,372 @@
package com.limelight.nvstream;
import android.app.ActivityManager;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.security.cert.X509Certificate;
import java.util.concurrent.Semaphore;
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
import org.xmlpull.v1.XmlPullParserException;
import com.limelight.LimeLog;
import com.limelight.nvstream.av.audio.AudioRenderer;
import com.limelight.nvstream.av.video.VideoDecoderRenderer;
import com.limelight.nvstream.http.GfeHttpResponseException;
import com.limelight.nvstream.http.LimelightCryptoProvider;
import com.limelight.nvstream.http.NvApp;
import com.limelight.nvstream.http.NvHTTP;
import com.limelight.nvstream.http.PairingManager;
import com.limelight.nvstream.input.MouseButtonPacket;
import com.limelight.nvstream.jni.MoonBridge;
public class NvConnection {
// Context parameters
private String host;
private LimelightCryptoProvider cryptoProvider;
private String uniqueId;
private ConnectionContext context;
private static Semaphore connectionAllowed = new Semaphore(1);
private final boolean isMonkey;
public NvConnection(String host, String uniqueId, StreamConfiguration config, LimelightCryptoProvider cryptoProvider, X509Certificate serverCert)
{
this.host = host;
this.cryptoProvider = cryptoProvider;
this.uniqueId = uniqueId;
this.context = new ConnectionContext();
this.context.streamConfig = config;
this.context.serverCert = serverCert;
// This is unique per connection
this.context.riKey = generateRiAesKey();
context.riKeyId = generateRiKeyId();
this.isMonkey = ActivityManager.isUserAMonkey();
}
private static SecretKey generateRiAesKey() {
try {
KeyGenerator keyGen = KeyGenerator.getInstance("AES");
// RI keys are 128 bits
keyGen.init(128);
return keyGen.generateKey();
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
throw new RuntimeException(e);
}
}
private static int generateRiKeyId() {
return new SecureRandom().nextInt();
}
public void stop() {
// Interrupt any pending connection. This is thread-safe.
MoonBridge.interruptConnection();
// Moonlight-core is not thread-safe with respect to connection start and stop, so
// we must not invoke that functionality in parallel.
synchronized (MoonBridge.class) {
MoonBridge.stopConnection();
MoonBridge.cleanupBridge();
}
// Now a pending connection can be processed
connectionAllowed.release();
}
private boolean startApp() throws XmlPullParserException, IOException
{
NvHTTP h = new NvHTTP(context.serverAddress, uniqueId, context.serverCert, cryptoProvider);
String serverInfo = h.getServerInfo();
context.serverAppVersion = h.getServerVersion(serverInfo);
if (context.serverAppVersion == null) {
context.connListener.displayMessage("Server version malformed");
return false;
}
// May be missing for older servers
context.serverGfeVersion = h.getGfeVersion(serverInfo);
if (h.getPairState(serverInfo) != PairingManager.PairState.PAIRED) {
context.connListener.displayMessage("Device not paired with computer");
return false;
}
context.negotiatedHdr = context.streamConfig.getEnableHdr();
if ((h.getServerCodecModeSupport(serverInfo) & 0x200) == 0 && context.negotiatedHdr) {
context.connListener.displayTransientMessage("Your GPU does not support streaming HDR. The stream will be SDR.");
context.negotiatedHdr = false;
}
//
// Decide on negotiated stream parameters now
//
// Check for a supported stream resolution
if ((context.streamConfig.getWidth() > 4096 || context.streamConfig.getHeight() > 4096) &&
(h.getServerCodecModeSupport(serverInfo) & 0x200) == 0) {
context.connListener.displayMessage("Your host PC does not support streaming at resolutions above 4K.");
return false;
}
else if ((context.streamConfig.getWidth() > 4096 || context.streamConfig.getHeight() > 4096) &&
!context.streamConfig.getHevcSupported()) {
context.connListener.displayMessage("Your streaming device must support H.265 to stream at resolutions above 4K.");
return false;
}
else if (context.streamConfig.getHeight() >= 2160 && !h.supports4K(serverInfo)) {
// Client wants 4K but the server can't do it
context.connListener.displayTransientMessage("You must update GeForce Experience to stream in 4K. The stream will be 1080p.");
// Lower resolution to 1080p
context.negotiatedWidth = 1920;
context.negotiatedHeight = 1080;
}
else {
// Take what the client wanted
context.negotiatedWidth = context.streamConfig.getWidth();
context.negotiatedHeight = context.streamConfig.getHeight();
}
//
// Video stream format will be decided during the RTSP handshake
//
NvApp app = context.streamConfig.getApp();
// If the client did not provide an exact app ID, do a lookup with the applist
if (!context.streamConfig.getApp().isInitialized()) {
LimeLog.info("Using deprecated app lookup method - Please specify an app ID in your StreamConfiguration instead");
app = h.getAppByName(context.streamConfig.getApp().getAppName());
if (app == null) {
context.connListener.displayMessage("The app " + context.streamConfig.getApp().getAppName() + " is not in GFE app list");
return false;
}
}
// If there's a game running, resume it
if (h.getCurrentGame(serverInfo) != 0) {
try {
if (h.getCurrentGame(serverInfo) == app.getAppId()) {
if (!h.resumeApp(context)) {
context.connListener.displayMessage("Failed to resume existing session");
return false;
}
} else {
return quitAndLaunch(h, context);
}
} catch (GfeHttpResponseException e) {
if (e.getErrorCode() == 470) {
// This is the error you get when you try to resume a session that's not yours.
// Because this is fairly common, we'll display a more detailed message.
context.connListener.displayMessage("This session wasn't started by this device," +
" so it cannot be resumed. End streaming on the original " +
"device or the PC itself and try again. (Error code: "+e.getErrorCode()+")");
return false;
}
else if (e.getErrorCode() == 525) {
context.connListener.displayMessage("The application is minimized. Resume it on the PC manually or " +
"quit the session and start streaming again.");
return false;
} else {
throw e;
}
}
LimeLog.info("Resumed existing game session");
return true;
}
else {
return launchNotRunningApp(h, context);
}
}
protected boolean quitAndLaunch(NvHTTP h, ConnectionContext context) throws IOException,
XmlPullParserException {
try {
if (!h.quitApp()) {
context.connListener.displayMessage("Failed to quit previous session! You must quit it manually");
return false;
}
} catch (GfeHttpResponseException e) {
if (e.getErrorCode() == 599) {
context.connListener.displayMessage("This session wasn't started by this device," +
" so it cannot be quit. End streaming on the original " +
"device or the PC itself. (Error code: "+e.getErrorCode()+")");
return false;
}
else {
throw e;
}
}
return launchNotRunningApp(h, context);
}
private boolean launchNotRunningApp(NvHTTP h, ConnectionContext context)
throws IOException, XmlPullParserException {
// Launch the app since it's not running
if (!h.launchApp(context, context.streamConfig.getApp().getAppId(), context.negotiatedHdr)) {
context.connListener.displayMessage("Failed to launch application");
return false;
}
LimeLog.info("Launched new game session");
return true;
}
public void start(final AudioRenderer audioRenderer, final VideoDecoderRenderer videoDecoderRenderer, final NvConnectionListener connectionListener)
{
new Thread(new Runnable() {
public void run() {
context.connListener = connectionListener;
context.videoCapabilities = videoDecoderRenderer.getCapabilities();
String appName = context.streamConfig.getApp().getAppName();
context.serverAddress = host;
context.connListener.stageStarting(appName);
try {
if (!startApp()) {
context.connListener.stageFailed(appName, 0, 0);
return;
}
context.connListener.stageComplete(appName);
} catch (GfeHttpResponseException e) {
e.printStackTrace();
context.connListener.displayMessage(e.getMessage());
context.connListener.stageFailed(appName, 0, e.getErrorCode());
return;
} catch (XmlPullParserException | IOException e) {
e.printStackTrace();
context.connListener.displayMessage(e.getMessage());
context.connListener.stageFailed(appName, MoonBridge.ML_PORT_FLAG_TCP_47984 | MoonBridge.ML_PORT_FLAG_TCP_47989, 0);
return;
}
ByteBuffer ib = ByteBuffer.allocate(16);
ib.putInt(context.riKeyId);
// Acquire the connection semaphore to ensure we only have one
// connection going at once.
try {
connectionAllowed.acquire();
} catch (InterruptedException e) {
context.connListener.displayMessage(e.getMessage());
context.connListener.stageFailed(appName, 0, 0);
return;
}
// Moonlight-core is not thread-safe with respect to connection start and stop, so
// we must not invoke that functionality in parallel.
synchronized (MoonBridge.class) {
MoonBridge.setupBridge(videoDecoderRenderer, audioRenderer, connectionListener);
int ret = MoonBridge.startConnection(context.serverAddress,
context.serverAppVersion, context.serverGfeVersion,
context.negotiatedWidth, context.negotiatedHeight,
context.streamConfig.getRefreshRate(), context.streamConfig.getBitrate(),
context.streamConfig.getMaxPacketSize(),
context.streamConfig.getRemote(), context.streamConfig.getAudioConfiguration().toInt(),
context.streamConfig.getHevcSupported(),
context.negotiatedHdr,
context.streamConfig.getHevcBitratePercentageMultiplier(),
context.streamConfig.getClientRefreshRateX100(),
context.riKey.getEncoded(), ib.array(),
context.videoCapabilities);
if (ret != 0) {
// LiStartConnection() failed, so the caller is not expected
// to stop the connection themselves. We need to release their
// semaphore count for them.
connectionAllowed.release();
}
}
}
}).start();
}
public void sendMouseMove(final short deltaX, final short deltaY)
{
if (!isMonkey) {
MoonBridge.sendMouseMove(deltaX, deltaY);
}
}
public void sendMousePosition(short x, short y, short referenceWidth, short referenceHeight)
{
if (!isMonkey) {
MoonBridge.sendMousePosition(x, y, referenceWidth, referenceHeight);
}
}
public void sendMouseButtonDown(final byte mouseButton)
{
if (!isMonkey) {
MoonBridge.sendMouseButton(MouseButtonPacket.PRESS_EVENT, mouseButton);
}
}
public void sendMouseButtonUp(final byte mouseButton)
{
if (!isMonkey) {
MoonBridge.sendMouseButton(MouseButtonPacket.RELEASE_EVENT, mouseButton);
}
}
public void sendControllerInput(final short controllerNumber,
final short activeGamepadMask, final short buttonFlags,
final byte leftTrigger, final byte rightTrigger,
final short leftStickX, final short leftStickY,
final short rightStickX, final short rightStickY)
{
if (!isMonkey) {
MoonBridge.sendMultiControllerInput(controllerNumber, activeGamepadMask, buttonFlags,
leftTrigger, rightTrigger, leftStickX, leftStickY, rightStickX, rightStickY);
}
}
public void sendControllerInput(final short buttonFlags,
final byte leftTrigger, final byte rightTrigger,
final short leftStickX, final short leftStickY,
final short rightStickX, final short rightStickY)
{
if (!isMonkey) {
MoonBridge.sendControllerInput(buttonFlags, leftTrigger, rightTrigger, leftStickX,
leftStickY, rightStickX, rightStickY);
}
}
public void sendKeyboardInput(final short keyMap, final byte keyDirection, final byte modifier) {
if (!isMonkey) {
MoonBridge.sendKeyboardInput(keyMap, keyDirection, modifier);
}
}
public void sendMouseScroll(final byte scrollClicks) {
if (!isMonkey) {
MoonBridge.sendMouseScroll(scrollClicks);
}
}
public void sendMouseHighResScroll(final short scrollAmount) {
if (!isMonkey) {
MoonBridge.sendMouseHighResScroll(scrollAmount);
}
}
public static String findExternalAddressForMdns(String stunHostname, int stunPort) {
return MoonBridge.findExternalAddressIP4(stunHostname, stunPort);
}
}

View File

@@ -0,0 +1,16 @@
package com.limelight.nvstream;
public interface NvConnectionListener {
void stageStarting(String stage);
void stageComplete(String stage);
void stageFailed(String stage, int portFlags, int errorCode);
void connectionStarted();
void connectionTerminated(int errorCode);
void connectionStatusUpdate(int connectionStatus);
void displayMessage(String message);
void displayTransientMessage(String message);
void rumble(short controllerNumber, short lowFreqMotor, short highFreqMotor);
}

View File

@@ -0,0 +1,214 @@
package com.limelight.nvstream;
import com.limelight.nvstream.http.NvApp;
import com.limelight.nvstream.jni.MoonBridge;
public class StreamConfiguration {
public static final int INVALID_APP_ID = 0;
public static final int STREAM_CFG_LOCAL = 0;
public static final int STREAM_CFG_REMOTE = 1;
public static final int STREAM_CFG_AUTO = 2;
private NvApp app;
private int width, height;
private int refreshRate;
private int launchRefreshRate;
private int clientRefreshRateX100;
private int bitrate;
private boolean sops;
private boolean enableAdaptiveResolution;
private boolean playLocalAudio;
private int maxPacketSize;
private int remote;
private MoonBridge.AudioConfiguration audioConfiguration;
private boolean supportsHevc;
private int hevcBitratePercentageMultiplier;
private boolean enableHdr;
private int attachedGamepadMask;
public static class Builder {
private StreamConfiguration config = new StreamConfiguration();
public StreamConfiguration.Builder setApp(NvApp app) {
config.app = app;
return this;
}
public StreamConfiguration.Builder setRemoteConfiguration(int remote) {
config.remote = remote;
return this;
}
public StreamConfiguration.Builder setResolution(int width, int height) {
config.width = width;
config.height = height;
return this;
}
public StreamConfiguration.Builder setRefreshRate(int refreshRate) {
config.refreshRate = refreshRate;
return this;
}
public StreamConfiguration.Builder setLaunchRefreshRate(int refreshRate) {
config.launchRefreshRate = refreshRate;
return this;
}
public StreamConfiguration.Builder setBitrate(int bitrate) {
config.bitrate = bitrate;
return this;
}
public StreamConfiguration.Builder setEnableSops(boolean enable) {
config.sops = enable;
return this;
}
public StreamConfiguration.Builder enableAdaptiveResolution(boolean enable) {
config.enableAdaptiveResolution = enable;
return this;
}
public StreamConfiguration.Builder enableLocalAudioPlayback(boolean enable) {
config.playLocalAudio = enable;
return this;
}
public StreamConfiguration.Builder setMaxPacketSize(int maxPacketSize) {
config.maxPacketSize = maxPacketSize;
return this;
}
public StreamConfiguration.Builder setHevcBitratePercentageMultiplier(int multiplier) {
config.hevcBitratePercentageMultiplier = multiplier;
return this;
}
public StreamConfiguration.Builder setEnableHdr(boolean enableHdr) {
config.enableHdr = enableHdr;
return this;
}
public StreamConfiguration.Builder setAttachedGamepadMask(int attachedGamepadMask) {
config.attachedGamepadMask = attachedGamepadMask;
return this;
}
public StreamConfiguration.Builder setAttachedGamepadMaskByCount(int gamepadCount) {
config.attachedGamepadMask = 0;
for (int i = 0; i < 4; i++) {
if (gamepadCount > i) {
config.attachedGamepadMask |= 1 << i;
}
}
return this;
}
public StreamConfiguration.Builder setClientRefreshRateX100(int refreshRateX100) {
config.clientRefreshRateX100 = refreshRateX100;
return this;
}
public StreamConfiguration.Builder setAudioConfiguration(MoonBridge.AudioConfiguration audioConfig) {
config.audioConfiguration = audioConfig;
return this;
}
public StreamConfiguration.Builder setHevcSupported(boolean supportsHevc) {
config.supportsHevc = supportsHevc;
return this;
}
public StreamConfiguration build() {
return config;
}
}
private StreamConfiguration() {
// Set default attributes
this.app = new NvApp("Steam");
this.width = 1280;
this.height = 720;
this.refreshRate = 60;
this.launchRefreshRate = 60;
this.bitrate = 10000;
this.maxPacketSize = 1024;
this.remote = STREAM_CFG_AUTO;
this.sops = true;
this.enableAdaptiveResolution = false;
this.audioConfiguration = MoonBridge.AUDIO_CONFIGURATION_STEREO;
this.supportsHevc = false;
this.enableHdr = false;
this.attachedGamepadMask = 0;
}
public int getWidth() {
return width;
}
public int getHeight() {
return height;
}
public int getRefreshRate() {
return refreshRate;
}
public int getLaunchRefreshRate() {
return launchRefreshRate;
}
public int getBitrate() {
return bitrate;
}
public int getMaxPacketSize() {
return maxPacketSize;
}
public NvApp getApp() {
return app;
}
public boolean getSops() {
return sops;
}
public boolean getAdaptiveResolutionEnabled() {
return enableAdaptiveResolution;
}
public boolean getPlayLocalAudio() {
return playLocalAudio;
}
public int getRemote() {
return remote;
}
public MoonBridge.AudioConfiguration getAudioConfiguration() {
return audioConfiguration;
}
public boolean getHevcSupported() {
return supportsHevc;
}
public int getHevcBitratePercentageMultiplier() {
return hevcBitratePercentageMultiplier;
}
public boolean getEnableHdr() {
return enableHdr;
}
public int getAttachedGamepadMask() {
return attachedGamepadMask;
}
public int getClientRefreshRateX100() {
return clientRefreshRateX100;
}
}

View File

@@ -0,0 +1,57 @@
package com.limelight.nvstream.av;
public class ByteBufferDescriptor {
public byte[] data;
public int offset;
public int length;
public ByteBufferDescriptor nextDescriptor;
public ByteBufferDescriptor(byte[] data, int offset, int length)
{
this.data = data;
this.offset = offset;
this.length = length;
}
public ByteBufferDescriptor(ByteBufferDescriptor desc)
{
this.data = desc.data;
this.offset = desc.offset;
this.length = desc.length;
}
public void reinitialize(byte[] data, int offset, int length)
{
this.data = data;
this.offset = offset;
this.length = length;
this.nextDescriptor = null;
}
public void print()
{
print(offset, length);
}
public void print(int length)
{
print(this.offset, length);
}
public void print(int offset, int length)
{
for (int i = offset; i < offset+length;) {
if (i + 8 <= offset+length) {
System.out.printf("%x: %02x %02x %02x %02x %02x %02x %02x %02x\n", i,
data[i], data[i+1], data[i+2], data[i+3], data[i+4], data[i+5], data[i+6], data[i+7]);
i += 8;
}
else {
System.out.printf("%x: %02x \n", i, data[i]);
i++;
}
}
System.out.println();
}
}

View File

@@ -0,0 +1,15 @@
package com.limelight.nvstream.av.audio;
import com.limelight.nvstream.jni.MoonBridge;
public interface AudioRenderer {
int setup(MoonBridge.AudioConfiguration audioConfiguration, int sampleRate, int samplesPerFrame);
void start();
void stop();
void playDecodedAudio(short[] audioData);
void cleanup();
}

View File

@@ -0,0 +1,18 @@
package com.limelight.nvstream.av.video;
public abstract class VideoDecoderRenderer {
public abstract int setup(int format, int width, int height, int redrawRate);
public abstract void start();
public abstract void stop();
// This is called once for each frame-start NALU. This means it will be called several times
// for an IDR frame which contains several parameter sets and the I-frame data.
public abstract int submitDecodeUnit(byte[] decodeUnitData, int decodeUnitLength, int decodeUnitType,
int frameNumber, long receiveTimeMs, long enqueueTimeMs);
public abstract void cleanup();
public abstract int getCapabilities();
}

View File

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

View File

@@ -0,0 +1,85 @@
package com.limelight.nvstream.http;
import java.security.cert.X509Certificate;
public class ComputerDetails {
public enum State {
ONLINE, OFFLINE, UNKNOWN
}
// Persistent attributes
public String uuid;
public String name;
public String localAddress;
public String remoteAddress;
public String manualAddress;
public String ipv6Address;
public String macAddress;
public X509Certificate serverCert;
// Transient attributes
public State state;
public String activeAddress;
public PairingManager.PairState pairState;
public int runningGameId;
public String rawAppList;
public ComputerDetails() {
// Use defaults
state = State.UNKNOWN;
}
public ComputerDetails(ComputerDetails details) {
// Copy details from the other computer
update(details);
}
public void update(ComputerDetails details) {
this.state = details.state;
this.name = details.name;
this.uuid = details.uuid;
if (details.activeAddress != null) {
this.activeAddress = details.activeAddress;
}
// We can get IPv4 loopback addresses with GS IPv6 Forwarder
if (details.localAddress != null && !details.localAddress.startsWith("127.")) {
this.localAddress = details.localAddress;
}
if (details.remoteAddress != null) {
this.remoteAddress = details.remoteAddress;
}
if (details.manualAddress != null) {
this.manualAddress = details.manualAddress;
}
if (details.ipv6Address != null) {
this.ipv6Address = details.ipv6Address;
}
if (details.macAddress != null && !details.macAddress.equals("00:00:00:00:00:00")) {
this.macAddress = details.macAddress;
}
if (details.serverCert != null) {
this.serverCert = details.serverCert;
}
this.pairState = details.pairState;
this.runningGameId = details.runningGameId;
this.rawAppList = details.rawAppList;
}
@Override
public String toString() {
StringBuilder str = new StringBuilder();
str.append("Name: ").append(name).append("\n");
str.append("State: ").append(state).append("\n");
str.append("Active Address: ").append(activeAddress).append("\n");
str.append("UUID: ").append(uuid).append("\n");
str.append("Local Address: ").append(localAddress).append("\n");
str.append("Remote Address: ").append(remoteAddress).append("\n");
str.append("IPv6 Address: ").append(ipv6Address).append("\n");
str.append("Manual Address: ").append(manualAddress).append("\n");
str.append("MAC Address: ").append(macAddress).append("\n");
str.append("Pair State: ").append(pairState).append("\n");
str.append("Running Game ID: ").append(runningGameId).append("\n");
return str.toString();
}
}

View File

@@ -0,0 +1,28 @@
package com.limelight.nvstream.http;
import java.io.IOException;
public class GfeHttpResponseException extends IOException {
private static final long serialVersionUID = 1543508830807804222L;
private int errorCode;
private String errorMsg;
public GfeHttpResponseException(int errorCode, String errorMsg) {
this.errorCode = errorCode;
this.errorMsg = errorMsg;
}
public int getErrorCode() {
return errorCode;
}
public String getErrorMessage() {
return errorMsg;
}
@Override
public String getMessage() {
return "GeForce Experience returned error: "+errorMsg+" (Error code: "+errorCode+")";
}
}

View File

@@ -0,0 +1,11 @@
package com.limelight.nvstream.http;
import java.security.cert.X509Certificate;
import java.security.interfaces.RSAPrivateKey;
public interface LimelightCryptoProvider {
X509Certificate getClientCertificate();
RSAPrivateKey getClientPrivateKey();
byte[] getPemEncodedClientCertificate();
String encodeBase64String(byte[] data);
}

View File

@@ -0,0 +1,70 @@
package com.limelight.nvstream.http;
import com.limelight.LimeLog;
public class NvApp {
private String appName = "";
private int appId;
private boolean initialized;
private boolean hdrSupported;
public NvApp() {}
public NvApp(String appName) {
this.appName = appName;
}
public NvApp(String appName, int appId, boolean hdrSupported) {
this.appName = appName;
this.appId = appId;
this.hdrSupported = hdrSupported;
this.initialized = true;
}
public void setAppName(String appName) {
this.appName = appName;
}
public void setAppId(String appId) {
try {
this.appId = Integer.parseInt(appId);
this.initialized = true;
} catch (NumberFormatException e) {
LimeLog.warning("Malformed app ID: "+appId);
}
}
public void setAppId(int appId) {
this.appId = appId;
this.initialized = true;
}
public void setHdrSupported(boolean hdrSupported) {
this.hdrSupported = hdrSupported;
}
public String getAppName() {
return this.appName;
}
public int getAppId() {
return this.appId;
}
public boolean isHdrSupported() {
return this.hdrSupported;
}
public boolean isInitialized() {
return this.initialized;
}
@Override
public String toString() {
StringBuilder str = new StringBuilder();
str.append("Name: ").append(appName).append("\n");
str.append("HDR: ").append(hdrSupported ? "Yes" : "No").append("\n");
str.append("ID: ").append(appId).append("\n");
return str.toString();
}
}

View File

@@ -0,0 +1,771 @@
package com.limelight.nvstream.http;
import android.os.Build;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.io.StringReader;
import java.net.InetAddress;
import java.net.MalformedURLException;
import java.net.Socket;
import java.net.URI;
import java.net.URISyntaxException;
import java.security.KeyManagementException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.Principal;
import java.security.PrivateKey;
import java.security.SecureRandom;
import java.security.cert.Certificate;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.LinkedList;
import java.util.ListIterator;
import java.util.Stack;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.KeyManager;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLHandshakeException;
import javax.net.ssl.SSLPeerUnverifiedException;
import javax.net.ssl.SSLSession;
import javax.net.ssl.SSLSocket;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManager;
import javax.net.ssl.TrustManagerFactory;
import javax.net.ssl.X509KeyManager;
import javax.net.ssl.X509TrustManager;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
import org.xmlpull.v1.XmlPullParserFactory;
import com.limelight.BuildConfig;
import com.limelight.LimeLog;
import com.limelight.nvstream.ConnectionContext;
import com.limelight.nvstream.http.PairingManager.PairState;
import okhttp3.ConnectionPool;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import okhttp3.ResponseBody;
public class NvHTTP {
private String uniqueId;
private PairingManager pm;
public static final int HTTPS_PORT = 47984;
public static final int HTTP_PORT = 47989;
public static final int CONNECTION_TIMEOUT = 3000;
public static final int READ_TIMEOUT = 5000;
// Print URL and content to logcat on debug builds
private static boolean verbose = BuildConfig.DEBUG;
public String baseUrlHttps;
public String baseUrlHttp;
private OkHttpClient httpClient;
private OkHttpClient httpClientWithReadTimeout;
private X509TrustManager defaultTrustManager;
private X509TrustManager trustManager;
private X509KeyManager keyManager;
private X509Certificate serverCert;
void setServerCert(X509Certificate serverCert) {
this.serverCert = serverCert;
}
private static X509TrustManager getDefaultTrustManager() {
try {
TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
tmf.init((KeyStore) null);
for (TrustManager tm : tmf.getTrustManagers()) {
if (tm instanceof X509TrustManager) {
return (X509TrustManager) tm;
}
}
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException(e);
} catch (KeyStoreException e) {
throw new RuntimeException(e);
}
throw new IllegalStateException("No X509 trust manager found");
}
private void initializeHttpState(final LimelightCryptoProvider cryptoProvider) {
keyManager = new X509KeyManager() {
public String chooseClientAlias(String[] keyTypes,
Principal[] issuers, Socket socket) { return "Limelight-RSA"; }
public String chooseServerAlias(String keyType, Principal[] issuers,
Socket socket) { return null; }
public X509Certificate[] getCertificateChain(String alias) {
return new X509Certificate[] {cryptoProvider.getClientCertificate()};
}
public String[] getClientAliases(String keyType, Principal[] issuers) { return null; }
public PrivateKey getPrivateKey(String alias) {
return cryptoProvider.getClientPrivateKey();
}
public String[] getServerAliases(String keyType, Principal[] issuers) { return null; }
};
defaultTrustManager = getDefaultTrustManager();
trustManager = new X509TrustManager() {
public X509Certificate[] getAcceptedIssuers() {
return new X509Certificate[0];
}
public void checkClientTrusted(X509Certificate[] certs, String authType) {
throw new IllegalStateException("Should never be called");
}
public void checkServerTrusted(X509Certificate[] certs, String authType) throws CertificateException {
try {
// Try the default trust manager first to allow pairing with certificates
// that chain up to a trusted root CA. This will raise CertificateException
// if the certificate is not trusted (expected for GFE's self-signed certs).
defaultTrustManager.checkServerTrusted(certs, authType);
} catch (CertificateException e) {
// Check the server certificate if we've paired to this host
if (certs.length == 1 && NvHTTP.this.serverCert != null) {
if (!certs[0].equals(NvHTTP.this.serverCert)) {
throw new CertificateException("Certificate mismatch");
}
}
else {
// The cert chain doesn't look like a self-signed cert or we don't have
// a certificate pinned, so re-throw the original validation error.
throw e;
}
}
}
};
HostnameVerifier hv = new HostnameVerifier() {
public boolean verify(String hostname, SSLSession session) {
try {
Certificate[] certificates = session.getPeerCertificates();
if (certificates.length == 1 && certificates[0].equals(NvHTTP.this.serverCert)) {
// Allow any hostname if it's our pinned cert
return true;
}
} catch (SSLPeerUnverifiedException e) {
e.printStackTrace();
}
// Fall back to default HostnameVerifier for validating CA-issued certs
return HttpsURLConnection.getDefaultHostnameVerifier().verify(hostname, session);
}
};
httpClient = new OkHttpClient.Builder()
.connectionPool(new ConnectionPool(0, 1, TimeUnit.MILLISECONDS))
.hostnameVerifier(hv)
.readTimeout(0, TimeUnit.MILLISECONDS)
.connectTimeout(CONNECTION_TIMEOUT, TimeUnit.MILLISECONDS)
.build();
httpClientWithReadTimeout = httpClient.newBuilder()
.readTimeout(READ_TIMEOUT, TimeUnit.MILLISECONDS)
.build();
}
public NvHTTP(String address, String uniqueId, X509Certificate serverCert, LimelightCryptoProvider cryptoProvider) throws IOException {
// Use the same UID for all Moonlight clients so we can quit games
// started by other Moonlight clients.
this.uniqueId = "0123456789ABCDEF";
this.serverCert = serverCert;
initializeHttpState(cryptoProvider);
try {
// The URI constructor takes care of escaping IPv6 literals
this.baseUrlHttps = new URI("https", null, address, HTTPS_PORT, null, null, null).toString();
this.baseUrlHttp = new URI("http", null, address, HTTP_PORT, null, null, null).toString();
} catch (URISyntaxException e) {
// Encapsulate URISyntaxException into IOException for callers to handle more easily
throw new IOException(e);
}
this.pm = new PairingManager(this, cryptoProvider);
}
String buildUniqueIdUuidString() {
return "uniqueid="+uniqueId+"&uuid="+UUID.randomUUID();
}
static String getXmlString(Reader r, String tagname) throws XmlPullParserException, IOException {
XmlPullParserFactory factory = XmlPullParserFactory.newInstance();
factory.setNamespaceAware(true);
XmlPullParser xpp = factory.newPullParser();
xpp.setInput(r);
int eventType = xpp.getEventType();
Stack<String> currentTag = new Stack<String>();
while (eventType != XmlPullParser.END_DOCUMENT) {
switch (eventType) {
case (XmlPullParser.START_TAG):
if (xpp.getName().equals("root")) {
verifyResponseStatus(xpp);
}
currentTag.push(xpp.getName());
break;
case (XmlPullParser.END_TAG):
currentTag.pop();
break;
case (XmlPullParser.TEXT):
if (currentTag.peek().equals(tagname)) {
return xpp.getText();
}
break;
}
eventType = xpp.next();
}
return null;
}
static String getXmlString(String str, String tagname) throws XmlPullParserException, IOException {
return getXmlString(new StringReader(str), tagname);
}
private static void verifyResponseStatus(XmlPullParser xpp) throws GfeHttpResponseException {
// We use Long.parseLong() because in rare cases GFE can send back a status code of
// 0xFFFFFFFF, which will cause Integer.parseInt() to throw a NumberFormatException due
// to exceeding Integer.MAX_VALUE. We'll get the desired error code of -1 by just casting
// the resulting long into an int.
int statusCode = (int)Long.parseLong(xpp.getAttributeValue(XmlPullParser.NO_NAMESPACE, "status_code"));
if (statusCode != 200) {
String statusMsg = xpp.getAttributeValue(XmlPullParser.NO_NAMESPACE, "status_message");
if (statusCode == -1 && "Invalid".equals(statusMsg)) {
// Special case handling an audio capture error which GFE doesn't
// provide any useful status message for.
statusCode = 418;
statusMsg = "Missing audio capture device. Reinstall GeForce Experience.";
}
throw new GfeHttpResponseException(statusCode, statusMsg);
}
}
public String getServerInfo() throws IOException, XmlPullParserException {
String resp;
//
// TODO: Shield Hub uses HTTP for this and is able to get an accurate PairStatus with HTTP.
// For some reason, we always see PairStatus is 0 over HTTP and only 1 over HTTPS. It looks
// like there are extra request headers required to make this stuff work over HTTP.
//
// When we have a pinned cert, use HTTPS to fetch serverinfo and fall back on cert mismatch
if (serverCert != null) {
try {
try {
resp = openHttpConnectionToString(baseUrlHttps + "/serverinfo?"+buildUniqueIdUuidString(), true);
} catch (SSLHandshakeException e) {
// Detect if we failed due to a server cert mismatch
if (e.getCause() instanceof CertificateException) {
// Jump to the GfeHttpResponseException exception handler to retry
// over HTTP which will allow us to pair again to update the cert
throw new GfeHttpResponseException(401, "Server certificate mismatch");
}
else {
throw e;
}
}
// This will throw an exception if the request came back with a failure status.
// We want this because it will throw us into the HTTP case if the client is unpaired.
getServerVersion(resp);
}
catch (GfeHttpResponseException e) {
if (e.getErrorCode() == 401) {
// Cert validation error - fall back to HTTP
return openHttpConnectionToString(baseUrlHttp + "/serverinfo", true);
}
// If it's not a cert validation error, throw it
throw e;
}
return resp;
}
else {
// No pinned cert, so use HTTP
return openHttpConnectionToString(baseUrlHttp + "/serverinfo", true);
}
}
public ComputerDetails getComputerDetails() throws IOException, XmlPullParserException {
ComputerDetails details = new ComputerDetails();
String serverInfo = getServerInfo();
details.name = getXmlString(serverInfo, "hostname");
if (details.name == null || details.name.isEmpty()) {
details.name = "UNKNOWN";
}
details.uuid = getXmlString(serverInfo, "uniqueid");
details.macAddress = getXmlString(serverInfo, "mac");
details.localAddress = getXmlString(serverInfo, "LocalIP");
// This may be null, but that's okay
details.remoteAddress = getXmlString(serverInfo, "ExternalIP");
// This has some extra logic to always report unpaired if the pinned cert isn't there
details.pairState = getPairState(serverInfo);
details.runningGameId = getCurrentGame(serverInfo);
// We could reach it so it's online
details.state = ComputerDetails.State.ONLINE;
return details;
}
// This hack is Android-specific but we do it on all platforms
// because it doesn't really matter
private OkHttpClient performAndroidTlsHack(OkHttpClient client) {
// Doing this each time we create a socket is required
// to avoid the SSLv3 fallback that causes connection failures
try {
SSLContext sc = SSLContext.getInstance("TLS");
sc.init(new KeyManager[] { keyManager }, new TrustManager[] { trustManager }, new SecureRandom());
// TLS 1.2 is not enabled by default prior to Android 5.0, so we'll need a custom
// SSLSocketFactory in order to connect to GFE 3.20.4 which requires TLSv1.2 or later.
// We don't just always use TLSv12SocketFactory because explicitly specifying TLS versions
// prevents later TLS versions from being negotiated even if client and server otherwise
// support them.
return client.newBuilder().sslSocketFactory(
Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP ?
sc.getSocketFactory() : new TLSv12SocketFactory(sc),
trustManager).build();
} catch (NoSuchAlgorithmException | KeyManagementException e) {
throw new RuntimeException(e);
}
}
// Read timeout should be enabled for any HTTP query that requires no outside action
// on the GFE server. Examples of queries that DO require outside action are launch, resume, and quit.
// The initial pair query does require outside action (user entering a PIN) but subsequent pairing
// queries do not.
private ResponseBody openHttpConnection(String url, boolean enableReadTimeout) throws IOException {
Request request = new Request.Builder().url(url).get().build();
Response response;
if (enableReadTimeout) {
response = performAndroidTlsHack(httpClientWithReadTimeout).newCall(request).execute();
}
else {
response = performAndroidTlsHack(httpClient).newCall(request).execute();
}
ResponseBody body = response.body();
if (response.isSuccessful()) {
return body;
}
// Unsuccessful, so close the response body
if (body != null) {
body.close();
}
if (response.code() == 404) {
throw new FileNotFoundException(url);
}
else {
throw new GfeHttpResponseException(response.code(), response.message());
}
}
String openHttpConnectionToString(String url, boolean enableReadTimeout) throws IOException {
try {
if (verbose) {
LimeLog.info("Requesting URL: "+url);
}
ResponseBody resp = openHttpConnection(url, enableReadTimeout);
String respString = resp.string();
resp.close();
if (verbose) {
LimeLog.info(url+" -> "+respString);
}
return respString;
} catch (IOException e) {
if (verbose) {
e.printStackTrace();
}
throw e;
}
}
public String getServerVersion(String serverInfo) throws XmlPullParserException, IOException {
return getXmlString(serverInfo, "appversion");
}
public PairingManager.PairState getPairState() throws IOException, XmlPullParserException {
return getPairState(getServerInfo());
}
public PairingManager.PairState getPairState(String serverInfo) throws IOException, XmlPullParserException {
if (!NvHTTP.getXmlString(serverInfo, "PairStatus").equals("1")) {
return PairState.NOT_PAIRED;
}
return PairState.PAIRED;
}
public long getMaxLumaPixelsH264(String serverInfo) throws XmlPullParserException, IOException {
String str = getXmlString(serverInfo, "MaxLumaPixelsH264");
if (str != null) {
return Long.parseLong(str);
} else {
return 0;
}
}
public long getMaxLumaPixelsHEVC(String serverInfo) throws XmlPullParserException, IOException {
String str = getXmlString(serverInfo, "MaxLumaPixelsHEVC");
if (str != null) {
return Long.parseLong(str);
} else {
return 0;
}
}
// Possible meaning of bits
// Bit 0: H.264 Baseline
// Bit 1: H.264 High
// ----
// Bit 8: HEVC Main
// Bit 9: HEVC Main10
// Bit 10: HEVC Main10 4:4:4
// Bit 11: ???
public long getServerCodecModeSupport(String serverInfo) throws XmlPullParserException, IOException {
String str = getXmlString(serverInfo, "ServerCodecModeSupport");
if (str != null) {
return Long.parseLong(str);
} else {
return 0;
}
}
public String getGpuType(String serverInfo) throws XmlPullParserException, IOException {
return getXmlString(serverInfo, "gputype");
}
public String getGfeVersion(String serverInfo) throws XmlPullParserException, IOException {
return getXmlString(serverInfo, "GfeVersion");
}
public boolean supports4K(String serverInfo) throws XmlPullParserException, IOException {
// Only allow 4K on GFE 3.x
String gfeVersionStr = getXmlString(serverInfo, "GfeVersion");
if (gfeVersionStr == null || gfeVersionStr.startsWith("2.")) {
return false;
}
return true;
}
public int getCurrentGame(String serverInfo) throws IOException, XmlPullParserException {
// GFE 2.8 started keeping currentgame set to the last game played. As a result, it no longer
// has the semantics that its name would indicate. To contain the effects of this change as much
// as possible, we'll force the current game to zero if the server isn't in a streaming session.
String serverState = getXmlString(serverInfo, "state");
if (serverState != null && serverState.endsWith("_SERVER_BUSY")) {
String game = getXmlString(serverInfo, "currentgame");
return Integer.parseInt(game);
}
else {
return 0;
}
}
public NvApp getAppById(int appId) throws IOException, XmlPullParserException {
LinkedList<NvApp> appList = getAppList();
for (NvApp appFromList : appList) {
if (appFromList.getAppId() == appId) {
return appFromList;
}
}
return null;
}
/* NOTE: Only use this function if you know what you're doing.
* It's totally valid to have two apps named the same thing,
* or even nothing at all! Look apps up by ID if at all possible
* using the above function */
public NvApp getAppByName(String appName) throws IOException, XmlPullParserException {
LinkedList<NvApp> appList = getAppList();
for (NvApp appFromList : appList) {
if (appFromList.getAppName().equalsIgnoreCase(appName)) {
return appFromList;
}
}
return null;
}
public PairingManager getPairingManager() {
return pm;
}
public static LinkedList<NvApp> getAppListByReader(Reader r) throws XmlPullParserException, IOException {
XmlPullParserFactory factory = XmlPullParserFactory.newInstance();
factory.setNamespaceAware(true);
XmlPullParser xpp = factory.newPullParser();
xpp.setInput(r);
int eventType = xpp.getEventType();
LinkedList<NvApp> appList = new LinkedList<NvApp>();
Stack<String> currentTag = new Stack<String>();
boolean rootTerminated = false;
while (eventType != XmlPullParser.END_DOCUMENT) {
switch (eventType) {
case (XmlPullParser.START_TAG):
if (xpp.getName().equals("root")) {
verifyResponseStatus(xpp);
}
currentTag.push(xpp.getName());
if (xpp.getName().equals("App")) {
appList.addLast(new NvApp());
}
break;
case (XmlPullParser.END_TAG):
currentTag.pop();
if (xpp.getName().equals("root")) {
rootTerminated = true;
}
break;
case (XmlPullParser.TEXT):
NvApp app = appList.getLast();
if (currentTag.peek().equals("AppTitle")) {
app.setAppName(xpp.getText());
} else if (currentTag.peek().equals("ID")) {
app.setAppId(xpp.getText());
} else if (currentTag.peek().equals("IsHdrSupported")) {
app.setHdrSupported(xpp.getText().equals("1"));
}
break;
}
eventType = xpp.next();
}
// Throw a malformed XML exception if we've not seen the root tag ended
if (!rootTerminated) {
throw new XmlPullParserException("Malformed XML: Root tag was not terminated");
}
// Ensure that all apps in the list are initialized
ListIterator<NvApp> i = appList.listIterator();
while (i.hasNext()) {
NvApp app = i.next();
// Remove uninitialized apps
if (!app.isInitialized()) {
LimeLog.warning("GFE returned incomplete app: "+app.getAppId()+" "+app.getAppName());
i.remove();
}
}
return appList;
}
public String getAppListRaw() throws MalformedURLException, IOException {
return openHttpConnectionToString(baseUrlHttps + "/applist?"+buildUniqueIdUuidString(), true);
}
public LinkedList<NvApp> getAppList() throws GfeHttpResponseException, IOException, XmlPullParserException {
if (verbose) {
// Use the raw function so the app list is printed
return getAppListByReader(new StringReader(getAppListRaw()));
}
else {
ResponseBody resp = openHttpConnection(baseUrlHttps + "/applist?" + buildUniqueIdUuidString(), true);
LinkedList<NvApp> appList = getAppListByReader(new InputStreamReader(resp.byteStream()));
resp.close();
return appList;
}
}
public void unpair() throws IOException {
openHttpConnectionToString(baseUrlHttp + "/unpair?"+buildUniqueIdUuidString(), true);
}
public InputStream getBoxArt(NvApp app) throws IOException {
ResponseBody resp = openHttpConnection(baseUrlHttps + "/appasset?"+ buildUniqueIdUuidString() +
"&appid=" + app.getAppId() + "&AssetType=2&AssetIdx=0", true);
return resp.byteStream();
}
public int getServerMajorVersion(String serverInfo) throws XmlPullParserException, IOException {
return getServerAppVersionQuad(serverInfo)[0];
}
public int[] getServerAppVersionQuad(String serverInfo) throws XmlPullParserException, IOException {
String serverVersion = getServerVersion(serverInfo);
if (serverVersion == null) {
throw new IllegalArgumentException("Missing server version field");
}
String[] serverVersionSplit = serverVersion.split("\\.");
if (serverVersionSplit.length != 4) {
throw new IllegalArgumentException("Malformed server version field: "+serverVersion);
}
int[] ret = new int[serverVersionSplit.length];
for (int i = 0; i < ret.length; i++) {
ret[i] = Integer.parseInt(serverVersionSplit[i]);
}
return ret;
}
final private static char[] hexArray = "0123456789ABCDEF".toCharArray();
private static String bytesToHex(byte[] bytes) {
char[] hexChars = new char[bytes.length * 2];
for ( int j = 0; j < bytes.length; j++ ) {
int v = bytes[j] & 0xFF;
hexChars[j * 2] = hexArray[v >>> 4];
hexChars[j * 2 + 1] = hexArray[v & 0x0F];
}
return new String(hexChars);
}
public boolean launchApp(ConnectionContext context, int appId, boolean enableHdr) throws IOException, XmlPullParserException {
// Using an FPS value over 60 causes SOPS to default to 720p60,
// so force it to 0 to ensure the correct resolution is set. We
// used to use 60 here but that locked the frame rate to 60 FPS
// on GFE 3.20.3.
int fps = context.streamConfig.getLaunchRefreshRate() > 60 ? 0 : context.streamConfig.getLaunchRefreshRate();
// Using an unsupported resolution (not 720p, 1080p, or 4K) causes
// GFE to force SOPS to 720p60. This is fine for < 720p resolutions like
// 360p or 480p, but it is not ideal for 1440p and other resolutions.
// When we detect an unsupported resolution, disable SOPS unless it's under 720p.
// FIXME: Detect support resolutions using the serverinfo response, not a hardcoded list
boolean enableSops = context.streamConfig.getSops();
if (context.negotiatedWidth * context.negotiatedHeight > 1280 * 720 &&
context.negotiatedWidth * context.negotiatedHeight != 1920 * 1080 &&
context.negotiatedWidth * context.negotiatedHeight != 3840 * 2160) {
LimeLog.info("Disabling SOPS due to non-standard resolution: "+context.negotiatedWidth+"x"+context.negotiatedHeight);
enableSops = false;
}
String xmlStr = openHttpConnectionToString(baseUrlHttps +
"/launch?" + buildUniqueIdUuidString() +
"&appid=" + appId +
"&mode=" + context.negotiatedWidth + "x" + context.negotiatedHeight + "x" + fps +
"&additionalStates=1&sops=" + (enableSops ? 1 : 0) +
"&rikey="+bytesToHex(context.riKey.getEncoded()) +
"&rikeyid="+context.riKeyId +
(!enableHdr ? "" : "&hdrMode=1&clientHdrCapVersion=0&clientHdrCapSupportedFlagsInUint32=0&clientHdrCapMetaDataId=NV_STATIC_METADATA_TYPE_1&clientHdrCapDisplayData=0x0x0x0x0x0x0x0x0x0x0") +
"&localAudioPlayMode=" + (context.streamConfig.getPlayLocalAudio() ? 1 : 0) +
"&surroundAudioInfo=" + context.streamConfig.getAudioConfiguration().getSurroundAudioInfo() +
(context.streamConfig.getAttachedGamepadMask() != 0 ? "&remoteControllersBitmap=" + context.streamConfig.getAttachedGamepadMask() : "") +
(context.streamConfig.getAttachedGamepadMask() != 0 ? "&gcmap=" + context.streamConfig.getAttachedGamepadMask() : ""),
false);
String gameSession = getXmlString(xmlStr, "gamesession");
return gameSession != null && !gameSession.equals("0");
}
public boolean resumeApp(ConnectionContext context) throws IOException, XmlPullParserException {
String xmlStr = openHttpConnectionToString(baseUrlHttps + "/resume?" + buildUniqueIdUuidString() +
"&rikey="+bytesToHex(context.riKey.getEncoded()) +
"&rikeyid="+context.riKeyId +
"&surroundAudioInfo=" + context.streamConfig.getAudioConfiguration().getSurroundAudioInfo(),
false);
String resume = getXmlString(xmlStr, "resume");
return Integer.parseInt(resume) != 0;
}
public boolean quitApp() throws IOException, XmlPullParserException {
String xmlStr = openHttpConnectionToString(baseUrlHttps + "/cancel?" + buildUniqueIdUuidString(), false);
String cancel = getXmlString(xmlStr, "cancel");
if (Integer.parseInt(cancel) == 0) {
return false;
}
// Newer GFE versions will just return success even if quitting fails
// if we're not the original requestor.
if (getCurrentGame(getServerInfo()) != 0) {
// Generate a synthetic GfeResponseException letting the caller know
// that they can't kill someone else's stream.
throw new GfeHttpResponseException(599, "");
}
return true;
}
// Based on example code from https://blog.dev-area.net/2015/08/13/android-4-1-enable-tls-1-1-and-tls-1-2/
private static class TLSv12SocketFactory extends SSLSocketFactory {
private SSLSocketFactory internalSSLSocketFactory;
public TLSv12SocketFactory(SSLContext context) {
internalSSLSocketFactory = context.getSocketFactory();
}
@Override
public String[] getDefaultCipherSuites() {
return internalSSLSocketFactory.getDefaultCipherSuites();
}
@Override
public String[] getSupportedCipherSuites() {
return internalSSLSocketFactory.getSupportedCipherSuites();
}
@Override
public Socket createSocket() throws IOException {
return enableTLSv12OnSocket(internalSSLSocketFactory.createSocket());
}
@Override
public Socket createSocket(Socket s, String host, int port, boolean autoClose) throws IOException {
return enableTLSv12OnSocket(internalSSLSocketFactory.createSocket(s, host, port, autoClose));
}
@Override
public Socket createSocket(String host, int port) throws IOException {
return enableTLSv12OnSocket(internalSSLSocketFactory.createSocket(host, port));
}
@Override
public Socket createSocket(String host, int port, InetAddress localHost, int localPort) throws IOException {
return enableTLSv12OnSocket(internalSSLSocketFactory.createSocket(host, port, localHost, localPort));
}
@Override
public Socket createSocket(InetAddress host, int port) throws IOException {
return enableTLSv12OnSocket(internalSSLSocketFactory.createSocket(host, port));
}
@Override
public Socket createSocket(InetAddress address, int port, InetAddress localAddress, int localPort) throws IOException {
return enableTLSv12OnSocket(internalSSLSocketFactory.createSocket(address, port, localAddress, localPort));
}
private Socket enableTLSv12OnSocket(Socket socket) {
if (socket instanceof SSLSocket) {
// TLS 1.2 is not enabled by default prior to Android 5.0. We must enable it
// explicitly to ensure we can communicate with GFE 3.20.4 which blocks TLS 1.0.
((SSLSocket)socket).setEnabledProtocols(new String[] {"TLSv1.2"});
}
return socket;
}
}
}

View File

@@ -0,0 +1,342 @@
package com.limelight.nvstream.http;
import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import org.xmlpull.v1.XmlPullParserException;
import com.limelight.LimeLog;
import java.security.cert.Certificate;
import java.io.*;
import java.security.*;
import java.security.cert.*;
import java.util.Arrays;
import java.util.Locale;
public class PairingManager {
private NvHTTP http;
private PrivateKey pk;
private X509Certificate cert;
private SecretKey aesKey;
private byte[] pemCertBytes;
private X509Certificate serverCert;
public enum PairState {
NOT_PAIRED,
PAIRED,
PIN_WRONG,
FAILED,
ALREADY_IN_PROGRESS
}
public PairingManager(NvHTTP http, LimelightCryptoProvider cryptoProvider) {
this.http = http;
this.cert = cryptoProvider.getClientCertificate();
this.pemCertBytes = cryptoProvider.getPemEncodedClientCertificate();
this.pk = cryptoProvider.getClientPrivateKey();
}
final private static char[] hexArray = "0123456789ABCDEF".toCharArray();
private static String bytesToHex(byte[] bytes) {
char[] hexChars = new char[bytes.length * 2];
for ( int j = 0; j < bytes.length; j++ ) {
int v = bytes[j] & 0xFF;
hexChars[j * 2] = hexArray[v >>> 4];
hexChars[j * 2 + 1] = hexArray[v & 0x0F];
}
return new String(hexChars);
}
private static byte[] hexToBytes(String s) {
int len = s.length();
if (len % 2 != 0) {
throw new IllegalArgumentException("Illegal string length: "+len);
}
byte[] data = new byte[len / 2];
for (int i = 0; i < len; i += 2) {
data[i / 2] = (byte) ((Character.digit(s.charAt(i), 16) << 4)
+ Character.digit(s.charAt(i+1), 16));
}
return data;
}
private X509Certificate extractPlainCert(String text) throws XmlPullParserException, IOException
{
String certText = NvHTTP.getXmlString(text, "plaincert");
if (certText != null) {
byte[] certBytes = hexToBytes(certText);
try {
CertificateFactory cf = CertificateFactory.getInstance("X.509");
return (X509Certificate)cf.generateCertificate(new ByteArrayInputStream(certBytes));
} catch (CertificateException e) {
e.printStackTrace();
throw new RuntimeException(e);
}
}
else {
return null;
}
}
private byte[] generateRandomBytes(int length)
{
byte[] rand = new byte[length];
new SecureRandom().nextBytes(rand);
return rand;
}
private static byte[] saltPin(byte[] salt, String pin) throws UnsupportedEncodingException {
byte[] saltedPin = new byte[salt.length + pin.length()];
System.arraycopy(salt, 0, saltedPin, 0, salt.length);
System.arraycopy(pin.getBytes("UTF-8"), 0, saltedPin, salt.length, pin.length());
return saltedPin;
}
private static boolean verifySignature(byte[] data, byte[] signature, Certificate cert) {
try {
Signature sig = Signature.getInstance("SHA256withRSA");
sig.initVerify(cert.getPublicKey());
sig.update(data);
return sig.verify(signature);
} catch (NoSuchAlgorithmException | SignatureException | InvalidKeyException e) {
e.printStackTrace();
throw new RuntimeException(e);
}
}
private static byte[] signData(byte[] data, PrivateKey key) {
try {
Signature sig = Signature.getInstance("SHA256withRSA");
sig.initSign(key);
sig.update(data);
byte[] signature = new byte[256];
sig.sign(signature, 0, signature.length);
return signature;
} catch (NoSuchAlgorithmException | SignatureException | InvalidKeyException e) {
e.printStackTrace();
throw new RuntimeException(e);
}
}
private static byte[] decryptAes(byte[] encryptedData, SecretKey secretKey) {
try {
Cipher cipher = Cipher.getInstance("AES/ECB/NoPadding");
int blockRoundedSize = ((encryptedData.length + 15) / 16) * 16;
byte[] blockRoundedEncrypted = Arrays.copyOf(encryptedData, blockRoundedSize);
byte[] fullDecrypted = new byte[blockRoundedSize];
cipher.init(Cipher.DECRYPT_MODE, secretKey);
cipher.doFinal(blockRoundedEncrypted, 0,
blockRoundedSize, fullDecrypted);
return fullDecrypted;
} catch (GeneralSecurityException e) {
e.printStackTrace();
throw new RuntimeException(e);
}
}
private static byte[] encryptAes(byte[] data, SecretKey secretKey) {
try {
Cipher cipher = Cipher.getInstance("AES/ECB/NoPadding");
int blockRoundedSize = ((data.length + 15) / 16) * 16;
byte[] blockRoundedData = Arrays.copyOf(data, blockRoundedSize);
cipher.init(Cipher.ENCRYPT_MODE, secretKey);
return cipher.doFinal(blockRoundedData);
} catch (GeneralSecurityException e) {
e.printStackTrace();
throw new RuntimeException(e);
}
}
private static SecretKey generateAesKey(PairingHashAlgorithm hashAlgo, byte[] keyData) {
byte[] aesTruncated = Arrays.copyOf(hashAlgo.hashData(keyData), 16);
return new SecretKeySpec(aesTruncated, "AES");
}
private static byte[] concatBytes(byte[] a, byte[] b) {
byte[] c = new byte[a.length + b.length];
System.arraycopy(a, 0, c, 0, a.length);
System.arraycopy(b, 0, c, a.length, b.length);
return c;
}
public static String generatePinString() {
SecureRandom r = new SecureRandom();
return String.format((Locale)null, "%d%d%d%d",
r.nextInt(10), r.nextInt(10),
r.nextInt(10), r.nextInt(10));
}
public X509Certificate getPairedCert() {
return serverCert;
}
public PairState pair(String serverInfo, String pin) throws IOException, XmlPullParserException {
PairingHashAlgorithm hashAlgo;
int serverMajorVersion = http.getServerMajorVersion(serverInfo);
LimeLog.info("Pairing with server generation: "+serverMajorVersion);
if (serverMajorVersion >= 7) {
// Gen 7+ uses SHA-256 hashing
hashAlgo = new Sha256PairingHash();
}
else {
// Prior to Gen 7, SHA-1 is used
hashAlgo = new Sha1PairingHash();
}
// Generate a salt for hashing the PIN
byte[] salt = generateRandomBytes(16);
// Combine the salt and pin, then create an AES key from them
byte[] saltAndPin = saltPin(salt, pin);
aesKey = generateAesKey(hashAlgo, saltAndPin);
// Send the salt and get the server cert. This doesn't have a read timeout
// because the user must enter the PIN before the server responds
String getCert = http.openHttpConnectionToString(http.baseUrlHttp +
"/pair?"+http.buildUniqueIdUuidString()+"&devicename=roth&updateState=1&phrase=getservercert&salt="+
bytesToHex(salt)+"&clientcert="+bytesToHex(pemCertBytes),
false);
if (!NvHTTP.getXmlString(getCert, "paired").equals("1")) {
return PairState.FAILED;
}
// Save this cert for retrieval later
serverCert = extractPlainCert(getCert);
if (serverCert == null) {
// Attempting to pair while another device is pairing will cause GFE
// to give an empty cert in the response.
http.openHttpConnectionToString(http.baseUrlHttp + "/unpair?"+http.buildUniqueIdUuidString(), true);
return PairState.ALREADY_IN_PROGRESS;
}
// Require this cert for TLS to this host
http.setServerCert(serverCert);
// Generate a random challenge and encrypt it with our AES key
byte[] randomChallenge = generateRandomBytes(16);
byte[] encryptedChallenge = encryptAes(randomChallenge, aesKey);
// Send the encrypted challenge to the server
String challengeResp = http.openHttpConnectionToString(http.baseUrlHttp +
"/pair?"+http.buildUniqueIdUuidString()+"&devicename=roth&updateState=1&clientchallenge="+bytesToHex(encryptedChallenge),
true);
if (!NvHTTP.getXmlString(challengeResp, "paired").equals("1")) {
http.openHttpConnectionToString(http.baseUrlHttp + "/unpair?"+http.buildUniqueIdUuidString(), true);
return PairState.FAILED;
}
// Decode the server's response and subsequent challenge
byte[] encServerChallengeResponse = hexToBytes(NvHTTP.getXmlString(challengeResp, "challengeresponse"));
byte[] decServerChallengeResponse = decryptAes(encServerChallengeResponse, aesKey);
byte[] serverResponse = Arrays.copyOfRange(decServerChallengeResponse, 0, hashAlgo.getHashLength());
byte[] serverChallenge = Arrays.copyOfRange(decServerChallengeResponse, hashAlgo.getHashLength(), hashAlgo.getHashLength() + 16);
// Using another 16 bytes secret, compute a challenge response hash using the secret, our cert sig, and the challenge
byte[] clientSecret = generateRandomBytes(16);
byte[] challengeRespHash = hashAlgo.hashData(concatBytes(concatBytes(serverChallenge, cert.getSignature()), clientSecret));
byte[] challengeRespEncrypted = encryptAes(challengeRespHash, aesKey);
String secretResp = http.openHttpConnectionToString(http.baseUrlHttp +
"/pair?"+http.buildUniqueIdUuidString()+"&devicename=roth&updateState=1&serverchallengeresp="+bytesToHex(challengeRespEncrypted),
true);
if (!NvHTTP.getXmlString(secretResp, "paired").equals("1")) {
http.openHttpConnectionToString(http.baseUrlHttp + "/unpair?"+http.buildUniqueIdUuidString(), true);
return PairState.FAILED;
}
// Get the server's signed secret
byte[] serverSecretResp = hexToBytes(NvHTTP.getXmlString(secretResp, "pairingsecret"));
byte[] serverSecret = Arrays.copyOfRange(serverSecretResp, 0, 16);
byte[] serverSignature = Arrays.copyOfRange(serverSecretResp, 16, 272);
// Ensure the authenticity of the data
if (!verifySignature(serverSecret, serverSignature, serverCert)) {
// Cancel the pairing process
http.openHttpConnectionToString(http.baseUrlHttp + "/unpair?"+http.buildUniqueIdUuidString(), true);
// Looks like a MITM
return PairState.FAILED;
}
// Ensure the server challenge matched what we expected (aka the PIN was correct)
byte[] serverChallengeRespHash = hashAlgo.hashData(concatBytes(concatBytes(randomChallenge, serverCert.getSignature()), serverSecret));
if (!Arrays.equals(serverChallengeRespHash, serverResponse)) {
// Cancel the pairing process
http.openHttpConnectionToString(http.baseUrlHttp + "/unpair?"+http.buildUniqueIdUuidString(), true);
// Probably got the wrong PIN
return PairState.PIN_WRONG;
}
// Send the server our signed secret
byte[] clientPairingSecret = concatBytes(clientSecret, signData(clientSecret, pk));
String clientSecretResp = http.openHttpConnectionToString(http.baseUrlHttp +
"/pair?"+http.buildUniqueIdUuidString()+"&devicename=roth&updateState=1&clientpairingsecret="+bytesToHex(clientPairingSecret),
true);
if (!NvHTTP.getXmlString(clientSecretResp, "paired").equals("1")) {
http.openHttpConnectionToString(http.baseUrlHttp + "/unpair?"+http.buildUniqueIdUuidString(), true);
return PairState.FAILED;
}
// Do the initial challenge (seems neccessary for us to show as paired)
String pairChallenge = http.openHttpConnectionToString(http.baseUrlHttps +
"/pair?"+http.buildUniqueIdUuidString()+"&devicename=roth&updateState=1&phrase=pairchallenge", true);
if (!NvHTTP.getXmlString(pairChallenge, "paired").equals("1")) {
http.openHttpConnectionToString(http.baseUrlHttp + "/unpair?"+http.buildUniqueIdUuidString(), true);
return PairState.FAILED;
}
return PairState.PAIRED;
}
private interface PairingHashAlgorithm {
int getHashLength();
byte[] hashData(byte[] data);
}
private static class Sha1PairingHash implements PairingHashAlgorithm {
public int getHashLength() {
return 20;
}
public byte[] hashData(byte[] data) {
try {
MessageDigest md = MessageDigest.getInstance("SHA-1");
return md.digest(data);
}
catch (NoSuchAlgorithmException e) {
e.printStackTrace();
throw new RuntimeException(e);
}
}
}
private static class Sha256PairingHash implements PairingHashAlgorithm {
public int getHashLength() {
return 32;
}
public byte[] hashData(byte[] data) {
try {
MessageDigest md = MessageDigest.getInstance("SHA-256");
return md.digest(data);
}
catch (NoSuchAlgorithmException e) {
e.printStackTrace();
throw new RuntimeException(e);
}
}
}
}

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