Compare commits

...

1094 Commits
v8.3 ... master

Author SHA1 Message Date
Cameron Gutman
f10085f552 Update to libopus v1.5.2
ARMv7 now compiled with floating point enabled
2024-07-27 17:09:42 -05:00
Cameron Gutman
eb7f0887bf Enable support for 16KB pages 2024-07-27 16:55:21 -05:00
Cameron Gutman
34a9132d60 Update to NDK r27 2024-07-27 16:44:01 -05:00
Cameron Gutman
24d3fb000a Update AGP and Gradle 2024-07-27 16:43:44 -05:00
ReenigneArcher
b7b6adaff7 feat(activity): allow PC Name and AppName for ShortcutTrampoline (#1387) 2024-07-27 16:23:23 -05:00
Matheus Vargem
85ed72802f feat: Add Guide menu to on-screen virtual controller (#1265)
* Add `Guide Button` to OSC controller

In order to allow the `Guide Button` to be pressed without a physical gamepad, add the button to the On-screen virtual controller

Co-authored-by: ReenigneArcher <42013603+ReenigneArcher@users.noreply.github.com>
2024-07-27 16:21:17 -05:00
Cameron Gutman
f54f8c83e7 Update to libopus v1.5.1 2024-03-17 22:53:24 -05:00
Cameron Gutman
124bfdf418 Bump all dependencies to latest versions
Most of these were blocked on min API level 21
2024-03-17 17:57:56 -05:00
Cameron Gutman
01507d9995 Enable BTI on ARMv8 2024-03-17 17:56:33 -05:00
Cameron Gutman
070c82bc44 Update to NDK 26c 2024-03-17 17:53:43 -05:00
Cameron Gutman
17df15293f Drop support for Jelly Bean and KitKat
NDK support for these is already gone
2024-03-17 17:43:42 -05:00
komurlu
6551076613 Add Vendor 8BitDo to Xbox360Controller (#1333)
8BitDo Ultimate Bluetooth Controller was not being recognized in XInput mode when using the dongle.
Adding 8BitDo's Vendor ID to SUPPORTED_VENDORS seems to fix it.
2024-02-27 22:49:33 -06:00
Cameron Gutman
1b1b100e63 Version 12.1 2024-02-27 22:45:27 -06:00
Cameron Gutman
e70014bb28 Merge remote-tracking branch 'origin/weblate' 2024-02-21 23:54:35 -06:00
Cameron Gutman
082cc84a71 Remove sustained performance mode
Our CPU usage is so low that it's doubtful we'd ever trigger thermal throttling.
2024-02-17 19:16:35 -06:00
Cameron Gutman
2ba7f0d989 Update to AGP 8.2.2 2024-02-17 18:54:59 -06:00
Cameron Gutman
9a7381b35f Update moonlight-common-c with RTSP encryption 2024-02-17 18:46:55 -06:00
Cameron Gutman
613ecfff44 Add Game Mode configuration 2024-02-17 18:46:28 -06:00
Vag Ko
f638548a02 Translated using Weblate (Greek)
Currently translated at 71.3% (179 of 251 strings)

Translation: Moonlight Game Streaming/moonlight-android
Translate-URL: https://hosted.weblate.org/projects/moonlight/moonlight-android/el/
2024-02-12 22:02:05 +01:00
Jorys Paulin
224bab68bf Translated using Weblate (French)
Currently translated at 100.0% (251 of 251 strings)

Translation: Moonlight Game Streaming/moonlight-android
Translate-URL: https://hosted.weblate.org/projects/moonlight/moonlight-android/fr/
2024-02-10 21:02:03 +01:00
Jorys Paulin
6aca18bd76 Translated using Weblate (French)
Currently translated at 100.0% (251 of 251 strings)

Translation: Moonlight Game Streaming/moonlight-android
Translate-URL: https://hosted.weblate.org/projects/moonlight/moonlight-android/fr/
2024-02-05 13:01:49 +01:00
Mike
3c58e2fbba Translated using Weblate (Polish)
Currently translated at 100.0% (251 of 251 strings)

Translation: Moonlight Game Streaming/moonlight-android
Translate-URL: https://hosted.weblate.org/projects/moonlight/moonlight-android/pl/
2024-01-30 21:01:56 +01:00
bittin1ddc447d824349b2
aaaebde05e Translated using Weblate (Swedish)
Currently translated at 100.0% (251 of 251 strings)

Translation: Moonlight Game Streaming/moonlight-android
Translate-URL: https://hosted.weblate.org/projects/moonlight/moonlight-android/sv/
2024-01-24 07:01:52 +01:00
137615
8ff9f70bd7 Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (251 of 251 strings)

Translation: Moonlight Game Streaming/moonlight-android
Translate-URL: https://hosted.weblate.org/projects/moonlight/moonlight-android/zh_Hans/
2024-01-23 05:01:53 +01:00
Vadym Nekhai
523ca862b9 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (251 of 251 strings)

Translation: Moonlight Game Streaming/moonlight-android
Translate-URL: https://hosted.weblate.org/projects/moonlight/moonlight-android/uk/
2024-01-21 16:01:49 +01:00
gallegonovato
78b8d1e9f3 Translated using Weblate (Spanish)
Currently translated at 100.0% (251 of 251 strings)

Translation: Moonlight Game Streaming/moonlight-android
Translate-URL: https://hosted.weblate.org/projects/moonlight/moonlight-android/es/
2024-01-21 16:01:48 +01:00
Anonymous
83c698b36d Translated using Weblate (Chinese (Simplified))
Currently translated at 96.0% (241 of 251 strings)

Translation: Moonlight Game Streaming/moonlight-android
Translate-URL: https://hosted.weblate.org/projects/moonlight/moonlight-android/zh_Hans/
2024-01-20 04:03:09 +01:00
Anonymous
27fe37f221 Translated using Weblate (Romanian)
Currently translated at 60.9% (153 of 251 strings)

Translation: Moonlight Game Streaming/moonlight-android
Translate-URL: https://hosted.weblate.org/projects/moonlight/moonlight-android/ro/
2024-01-20 04:03:09 +01:00
Anonymous
3722106daf Translated using Weblate (Dutch)
Currently translated at 70.1% (176 of 251 strings)

Translation: Moonlight Game Streaming/moonlight-android
Translate-URL: https://hosted.weblate.org/projects/moonlight/moonlight-android/nl/
2024-01-20 04:03:08 +01:00
Cameron Gutman
3ea9ef1ef2 Merge remote-tracking branch 'origin/weblate' 2024-01-19 21:01:22 -06:00
Cameron Gutman
fcd27b48b2 Eat all exceptions from attempting to start ACTION_REQUEST_CHANNEL_BROWSABLE
Fixes #1302
2024-01-19 20:59:56 -06:00
Cameron Gutman
6ff37a17ec Tweak strings for rumble strength options 2024-01-19 20:51:13 -06:00
Cameron Gutman
f2c6e9e32e Update moonlight-common-c with finalized encryption changes 2024-01-19 19:19:00 -06:00
Alek Lefebvre
dbf1b88a3d Adjust emulated rumble strength (#1288) 2024-01-19 19:12:21 -06:00
joaomacp
3aab9eb13b Analog stick for scrolling in mouse emulation mode 2024-01-15 15:45:00 -06:00
Cameron Gutman
3f9f8f7b3b Opt in for video encryption on platforms with fast AES implementations 2024-01-15 15:05:38 -06:00
Cameron Gutman
f7520ba40c Move encryption enablement logic into JNI code 2024-01-15 14:59:01 -06:00
Cameron Gutman
7b13f12817 Update moonlight-common-c with new encryption support 2024-01-15 14:54:59 -06:00
Jorys Paulin
57e19d75e2 Translated using Weblate (French)
Currently translated at 100.0% (242 of 242 strings)

Translation: Moonlight Game Streaming/moonlight-android
Translate-URL: https://hosted.weblate.org/projects/moonlight/moonlight-android/fr/
2024-01-09 15:06:17 +00:00
Guillaume Zin
4330a223c6 Handle start+select as home button on Thrustmaster Score A gamepad (#1299) 2023-12-31 14:13:22 -06:00
Cameron Gutman
69387c32ad Display error codes when the connection is terminated 2023-12-30 21:14:22 -06:00
Cameron Gutman
a102ec4ee8 Fix inverted logic 2023-12-30 20:35:00 -06:00
Cameron Gutman
2a094437dd Don't override HDR color format for AV1 2023-12-30 20:33:31 -06:00
Cameron Gutman
4142907376 Don't consume special key combos that we don't handle 2023-12-30 19:50:36 -06:00
Cameron Gutman
e63dc9a93b Suppress bogus UnspecifiedRegisterReceiverFlag warning 2023-12-29 23:55:30 -06:00
Cameron Gutman
1fe19e912e Update moonlight-common-c for X-SS-Connect-Data support 2023-12-29 22:30:56 -06:00
Cameron Gutman
56ad48446e Bump AGP to 8.2.0 2023-12-29 22:29:18 -06:00
Cameron Gutman
a18aa26985 Update moonlight-common-c to remove per-codec bitrate adjustments 2023-12-03 22:20:30 -06:00
Cameron Gutman
5443cc014a Add support for ECDSA server keys 2023-11-29 22:37:12 -06:00
Cameron Gutman
7d77e1c1f2 Don't hardcode signature length 2023-11-29 22:32:00 -06:00
Cameron Gutman
ca82cd9752 Use generic PrivateKey type rather than RSAPrivateKey 2023-11-29 22:20:50 -06:00
bittin1ddc447d824349b2
dbd86bb861 Translated using Weblate (Swedish)
Currently translated at 100.0% (242 of 242 strings)

Translation: Moonlight Game Streaming/moonlight-android
Translate-URL: https://hosted.weblate.org/projects/moonlight/moonlight-android/sv/
2023-11-25 05:08:16 +00:00
Cameron Gutman
0af56b4981 Remove roundIcon attribute
This should only be used if the round icon is actually different.
2023-11-24 21:02:50 -06:00
Cameron Gutman
f1be5365bb Version 12.0.2 2023-11-01 20:35:57 -05:00
Cameron Gutman
c356862ac1 Avoid unnecessary reinitialization of PS4/PS5 gamepads during stream exit
onInputDeviceChanged() is triggered by starting/stopping pointer capture, so we should
unregister our callbacks before that happens to avoid triggering several gamepad context
reinitializations right as the stream is exiting
2023-10-29 16:45:07 -05:00
Cameron Gutman
fc77322f59 Merge remote-tracking branch 'origin/weblate' 2023-10-29 15:51:59 -05:00
Cameron Gutman
032e944d49 Update moonlight-common-c with multi-homed host fix 2023-10-29 15:34:09 -05:00
Cameron Gutman
0da47da8d8 Move shortcut creation/updates off the main thread for the common cases
These caused quite a few ANRs due to long Binder calls in ShortcutManager.getDynamicShortcuts()
2023-10-26 00:24:44 -05:00
Cameron Gutman
ebfe843299 Don't process incoming input device requests from the host after we've stopped 2023-10-26 00:05:24 -05:00
Cameron Gutman
827d2362b7 Don't create LightsSessions for devices without an RGB LED 2023-10-25 23:53:35 -05:00
Cameron Gutman
4fa1eb4088 Fix max FPS detection on Lollipop and earlier 2023-10-25 23:47:40 -05:00
Cameron Gutman
885b59fd52 Fix NPE when receiving non-view-associated mouse events with absolute mouse mode enabled 2023-10-25 23:13:54 -05:00
Cameron Gutman
ff5d9f72aa Fix ANRs trying to get battery state during controller arrival 2023-10-25 23:13:11 -05:00
Marocco2
030bb91789 Translated using Weblate (Italian)
Currently translated at 100.0% (242 of 242 strings)

Translation: Moonlight Game Streaming/moonlight-android
Translate-URL: https://hosted.weblate.org/projects/moonlight/moonlight-android/it/
2023-10-22 09:01:23 +00:00
Jorys Paulin
34788b2808 Translated using Weblate (French)
Currently translated at 100.0% (242 of 242 strings)

Translation: Moonlight Game Streaming/moonlight-android
Translate-URL: https://hosted.weblate.org/projects/moonlight/moonlight-android/fr/
2023-10-19 06:09:40 +02:00
Cameron Gutman
abc4123c52 Version 12.0.1 2023-10-17 22:39:49 -05:00
Cameron Gutman
26f8c0842e Merge remote-tracking branch 'origin/weblate' 2023-10-16 23:57:21 -05:00
Cameron Gutman
d430d83ba8 Add clickpad button emulation combo (Select+L1) 2023-10-16 23:56:56 -05:00
Cameron Gutman
a52f189fb1 Update ShieldControllerExtensions to fix crash in getBatteryPercentage() 2023-10-16 23:55:39 -05:00
Cameron Gutman
dc1045b69e Don't lie to our own clickpad heuristics when overriding the controller type 2023-10-16 23:38:12 -05:00
Cameron Gutman
3a89dbf4ab Update moonlight-common-c 2023-10-16 23:17:08 -05:00
Cameron Gutman
d69b4eca1e Disable gamepad motion sensors by default on Android 12 due to an OS bug 2023-10-16 23:16:55 -05:00
Marocco2
568bba82f0 Translated using Weblate (Italian)
Currently translated at 100.0% (242 of 242 strings)

Translation: Moonlight Game Streaming/moonlight-android
Translate-URL: https://hosted.weblate.org/projects/moonlight/moonlight-android/it/
2023-10-16 04:19:16 +00:00
Smoukus
b52e6c88ec Translated using Weblate (German)
Currently translated at 92.5% (224 of 242 strings)

Translation: Moonlight Game Streaming/moonlight-android
Translate-URL: https://hosted.weblate.org/projects/moonlight/moonlight-android/de/
2023-10-16 04:19:14 +00:00
Cameron Gutman
720595091e Change AV1 bitrate multiplier to match HEVC 2023-10-15 12:49:42 -05:00
Cameron Gutman
fe929c8e58 Fix debug build assert 2023-10-13 23:23:42 -05:00
Cameron Gutman
9ecec1eb3c Add support for H.264/HEVC bitstreams with multiple sets of parameter set NALUs 2023-10-13 23:21:50 -05:00
Cameron Gutman
79532f6f14 Version 12.0 2023-10-13 23:02:46 -05:00
Cameron Gutman
b400ba385e Update moonlight-common-c 2023-10-12 00:33:55 -05:00
Cameron Gutman
9915007f30 Check for a null UsbManager object
Apparently this can happen on some (broken?) devices
2023-10-12 00:22:28 -05:00
Cameron Gutman
0168a55596 Propagate the controller number to the standalone DS4 touchpad device 2023-10-07 23:26:26 -05:00
Cameron Gutman
229eff49fb Consume unhandled gamepad mouse events to prevent duplicate mouse actions 2023-10-07 23:04:36 -05:00
Cameron Gutman
0e3b472f78 Fix DS4 clickpad button on devices that expose the touchpad as a mouse 2023-10-07 22:58:07 -05:00
Cameron Gutman
5f29b30d34 Fix DS4 clickpad button on Nvidia Shield (again) 2023-10-07 22:53:19 -05:00
Cameron Gutman
9480363362 Update fastlane metadata with new features 2023-10-07 21:07:53 -05:00
Cameron Gutman
5dd80edde4 Wait 1 second after input device reconfiguration to enable motion sensors 2023-10-07 21:07:02 -05:00
Cameron Gutman
2243cf2017 Rewrite NsdManagerDiscoveryAgent lifecycle to avoid listener reuse 2023-10-07 20:42:13 -05:00
Cameron Gutman
d7791c8543 Adjust default bitrate logic to match new Qt logic 2023-10-07 19:54:02 -05:00
Cameron Gutman
30822c1ba5 Only check for motion sensors on Sony or Nintendo gamepads on Android 12
This works around a bug in Android 12 that leads to random crashes when input devices change.
2023-10-07 19:07:15 -05:00
Cameron Gutman
d250f4dc60 Move battery updates to a background HandlerThread
They can cause long Binder transactions that lead to ANRs.
2023-10-07 00:49:03 -05:00
Cameron Gutman
bc27492206 Split ControllerHandler teardown into stop() and destroy() functions 2023-10-07 00:35:36 -05:00
Cameron Gutman
2b63203a5b Update moonlight-common-c 2023-10-06 23:12:21 -05:00
Cameron Gutman
dc9a26f57b Update to AGP 8.1.2 2023-10-06 23:11:41 -05:00
Jorys Paulin
6f2d7464ba Translated using Weblate (French)
Currently translated at 100.0% (242 of 242 strings)

Translation: Moonlight Game Streaming/moonlight-android
Translate-URL: https://hosted.weblate.org/projects/moonlight/moonlight-android/fr/
2023-09-30 19:01:03 +02:00
Jen Kung-chih
6996c101b4 Translated using Weblate (Chinese (Traditional))
Currently translated at 100.0% (242 of 242 strings)

Translation: Moonlight Game Streaming/moonlight-android
Translate-URL: https://hosted.weblate.org/projects/moonlight/moonlight-android/zh_Hant/
2023-09-24 23:00:42 +02:00
Cameron Gutman
99dc773c7a Merge remote-tracking branch 'origin/weblate' 2023-09-23 12:59:56 -04:00
Cameron Gutman
6ec3f9455a Update moonlight-common-c with frame corruption fixes 2023-09-23 12:58:57 -04:00
Cameron Gutman
6589a568e2 Add freeform window metadata for ChromeOS 2023-09-23 02:48:53 -04:00
Cameron Gutman
55da48e28c Remove USB options from settings page if USB host mode is not supported 2023-09-23 02:21:53 -04:00
Cameron Gutman
081cca48fb Implement cursor visibility and quit key shortcuts
Fixes #1255
2023-09-23 02:20:26 -04:00
137615
6b6a93725c Translated using Weblate (Chinese (Traditional))
Currently translated at 100.0% (242 of 242 strings)

Translation: Moonlight Game Streaming/moonlight-android
Translate-URL: https://hosted.weblate.org/projects/moonlight/moonlight-android/zh_Hant/
2023-09-20 12:01:18 +00:00
137615
c280a52d33 Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (242 of 242 strings)

Translation: Moonlight Game Streaming/moonlight-android
Translate-URL: https://hosted.weblate.org/projects/moonlight/moonlight-android/zh_Hans/
2023-09-20 12:01:17 +00:00
sanhoe
b659439f0b Translated using Weblate (Korean)
Currently translated at 100.0% (242 of 242 strings)

Translation: Moonlight Game Streaming/moonlight-android
Translate-URL: https://hosted.weblate.org/projects/moonlight/moonlight-android/ko/
2023-09-20 12:01:16 +00:00
gallegonovato
6453b3c45c Translated using Weblate (Spanish)
Currently translated at 100.0% (242 of 242 strings)

Translation: Moonlight Game Streaming/moonlight-android
Translate-URL: https://hosted.weblate.org/projects/moonlight/moonlight-android/es/
2023-09-20 12:01:15 +00:00
Cameron Gutman
978a879c43 Batch async control stream callbacks 2023-09-18 23:25:59 -05:00
Cameron Gutman
833ef3630b Invoke control stream callbacks on a separate thread 2023-09-17 15:00:21 -05:00
Cameron Gutman
024b8c93bc Merge remote-tracking branch 'origin/weblate' 2023-09-16 23:12:05 -05:00
Cameron Gutman
d32c4f86a7 Fix unguarded use of Lollipop API 2023-09-16 22:53:13 -05:00
Cameron Gutman
cafc4450b2 Override controller type when motion sensor emulation is enabled
Without this, the host will still select an Xbox controller even if it can't support motion sensors.
2023-09-16 22:48:38 -05:00
Cameron Gutman
7d69b53958 Tweak preference strings 2023-09-16 22:27:01 -05:00
Cameron Gutman
4d3e883e49 Correct sensors for device orientation 2023-09-16 22:26:11 -05:00
Cameron Gutman
8f9a687872 Add device sensor fallback option
Correction for device orientation is not implemented yet
2023-09-16 20:25:54 -05:00
Cameron Gutman
08d509d831 Use the device vibrator for devices with built-in gamepads 2023-09-16 19:14:22 -05:00
Cameron Gutman
b06dec8449 Fix detection of G Cloud gamepad as an internal controller 2023-09-16 19:09:40 -05:00
Cameron Gutman
28c93b934b Update moonlight-common-c 2023-09-16 16:03:52 -05:00
Cameron Gutman
394a57a26d Disable native touch passthrough for now 2023-09-16 15:59:07 -05:00
bittin1ddc447d824349b2
3856b57a6f Translated using Weblate (Swedish)
Currently translated at 100.0% (239 of 239 strings)

Translation: Moonlight Game Streaming/moonlight-android
Translate-URL: https://hosted.weblate.org/projects/moonlight/moonlight-android/sv/
2023-09-08 11:59:27 +02:00
Zan 1456
9a3a076890 Translated using Weblate (Hungarian)
Currently translated at 100.0% (239 of 239 strings)

Translation: Moonlight Game Streaming/moonlight-android
Translate-URL: https://hosted.weblate.org/projects/moonlight/moonlight-android/hu/
2023-09-08 11:59:26 +02:00
Cameron Gutman
314dfcddcf Link to Sunshine 2023-09-03 18:05:00 -05:00
Cameron Gutman
1a85bda997 Update to AGP 8.1.1 2023-09-02 23:32:30 -05:00
Cameron Gutman
e8b30d5a88 Set KEY_PRIORITY during low latency option probing 2023-09-02 23:32:16 -05:00
Cameron Gutman
96bd1a7799 Update moonlight-common-c 2023-09-02 22:46:59 -05:00
sanhoe
cb0a1f13bc Translated using Weblate (Korean)
Currently translated at 100.0% (239 of 239 strings)

Translation: Moonlight Game Streaming/moonlight-android
Translate-URL: https://hosted.weblate.org/projects/moonlight/moonlight-android/ko/
2023-08-08 04:53:26 +02:00
Cameron Gutman
e92fdeef47 Fix RGB LED capability flag on Android 12 and 13 2023-08-03 23:02:11 -05:00
Cameron Gutman
62bae62386 Fix end of stream toast for AV1 2023-08-03 21:10:06 -05:00
Cameron Gutman
2636d79b86 Fix handling of ACTION_CANCEL for multi-pointer gestures 2023-08-03 02:29:55 -05:00
Cameron Gutman
c9c1ef91fd Fix mishandling ACTION_MOVE events for native pen/touch events 2023-07-27 23:15:26 -05:00
weng weng
ce7bba3e09 Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (239 of 239 strings)

Translation: Moonlight Game Streaming/moonlight-android
Translate-URL: https://hosted.weblate.org/projects/moonlight/moonlight-android/zh_Hans/
2023-07-26 13:04:56 +02:00
S. MohammadMahdi Zamanian
bfd53f39bd Added translation using Weblate (Persian) 2023-07-26 09:23:11 +02:00
Cameron Gutman
554fee037c Fix race condition when stopDiscovery() is called during onServiceFound()/onServiceLost() 2023-07-25 18:46:31 -05:00
Cameron Gutman
67b2853ef0 Add contact area and orientation for pen/touch events 2023-07-22 17:18:57 -05:00
Cameron Gutman
0e29e13d03 Use private API to detect clickpads on Android O and earlier 2023-07-22 14:34:27 -05:00
Jen Kung-chih
35f2a238e9 Translated using Weblate (Chinese (Traditional))
Currently translated at 100.0% (239 of 239 strings)

Translation: Moonlight Game Streaming/moonlight-android
Translate-URL: https://hosted.weblate.org/projects/moonlight/moonlight-android/zh_Hant/
2023-07-19 18:05:46 +02:00
Jorys Paulin
fcb34ab6ee Translated using Weblate (French)
Currently translated at 100.0% (239 of 239 strings)

Translation: Moonlight Game Streaming/moonlight-android
Translate-URL: https://hosted.weblate.org/projects/moonlight/moonlight-android/fr/
2023-07-13 11:51:59 +02:00
Cameron Gutman
3fbf682785 Pass gamepad touch input natively by default
Most devices will have a touchscreen which already provides mouse input
and Steam Input is can provide gamepad touchpad mouse control if desired.
2023-07-12 01:21:35 -05:00
Cameron Gutman
16086a6d3f Send the touchpad button on the gamepad even when using the touchpad for mouse control 2023-07-12 01:16:33 -05:00
Cameron Gutman
18b6aae381 Disable gamepad sensors while in PiP mode 2023-07-12 01:07:14 -05:00
Cameron Gutman
642c353164 Fix handling of onInputDeviceChanged when using sensors and lights
DS4 triggers this path when we release our pointer capture on activity pause
2023-07-12 00:32:28 -05:00
Cameron Gutman
42f64e5e88 Rename videoformat_hevcauto translations to videoformat_auto 2023-07-11 20:18:31 -05:00
Cameron Gutman
6af748b2cc Remove videoformat_hevcauto translations that still reference HEVC 2023-07-11 20:12:10 -05:00
Cameron Gutman
4fe97b69c7 Fix invalid format string 2023-07-11 20:10:52 -05:00
Cameron Gutman
04d46272dd Remove now unused videoformat_hevcnever string 2023-07-11 20:10:38 -05:00
Cameron Gutman
1c0290dc7a Merge remote-tracking branch 'origin/weblate' 2023-07-11 20:05:21 -05:00
Cameron Gutman
38588402e3 Use NsdManager for mDNS discovery on Android 14 2023-07-11 19:58:50 -05:00
Luna Jernberg
dfe3b8888d Translated using Weblate (Swedish)
Currently translated at 99.5% (238 of 239 strings)

Translation: Moonlight Game Streaming/moonlight-android
Translate-URL: https://hosted.weblate.org/projects/moonlight/moonlight-android/sv/
2023-07-11 17:49:16 +02:00
ThomasTech
e8f4022f1e Translated using Weblate (French)
Currently translated at 95.3% (228 of 239 strings)

Translation: Moonlight Game Streaming/moonlight-android
Translate-URL: https://hosted.weblate.org/projects/moonlight/moonlight-android/fr/
2023-07-11 17:49:15 +02:00
gallegonovato
e37bb32c82 Translated using Weblate (Spanish)
Currently translated at 100.0% (239 of 239 strings)

Translation: Moonlight Game Streaming/moonlight-android
Translate-URL: https://hosted.weblate.org/projects/moonlight/moonlight-android/es/
2023-07-11 17:49:15 +02:00
Cameron Gutman
1f72c82acb Consolidate AV1 and HEVC options into a single preference 2023-07-09 15:04:26 -05:00
Cameron Gutman
e6876926a4 Add options to control gamepad touchpad and motion and reorganize input settings 2023-07-09 14:46:46 -05:00
Cameron Gutman
2b8a43ab13 Update locale selections to add Hebrew, Swedish, and Indonesian languages 2023-07-09 14:00:14 -05:00
Cameron Gutman
8737466368 Merge branch 'weblate' of github.com:moonlight-stream/moonlight-android 2023-07-09 13:47:07 -05:00
Cameron Gutman
83916fa43e Map the Menu key for keyboards 2023-07-08 23:21:19 -05:00
Cameron Gutman
1fe5a12a45 Update moonlight-common-c 2023-07-08 23:19:33 -05:00
Cameron Gutman
fa7f3115ed Add AV1 autoselection by performance point criteria 2023-07-07 23:09:05 -05:00
Cameron Gutman
4dc6143440 Override the stock DS4 touchpad button mapping for Shield devices
Nvidia's stock mapping treats the clickpad as a second select button.
2023-07-05 23:00:54 -05:00
Cameron Gutman
f1503aa56c Implement battery reporting for Shield controllers 2023-07-05 18:57:01 -05:00
Cameron Gutman
67f344b755 Move serverCodecModeSupport into SERVER_INFORMATION struct 2023-07-02 23:56:34 -05:00
Cameron Gutman
f1bcc217a9 Update to new HDR support option in moonlight-common-c 2023-07-02 23:48:19 -05:00
Cameron Gutman
458460515d Plumb AV1 preference and rework HEVC preference to match 2023-07-02 23:48:13 -05:00
Cameron Gutman
3a78095574 Initial implementation of AV1 2023-07-02 22:49:42 -05:00
Cameron Gutman
d6bbfa1af1 Fix unmapped paddle and share button presses 2023-07-02 20:32:57 -05:00
Cameron Gutman
4e1b778f31 Import some SDL code and use it for controller classification
Imported as of 4aee17b039981f2bb79892f3d3e3e17bd6b66530
2023-07-02 20:20:55 -05:00
Cameron Gutman
5f4496036c Don't use GameManager loading flag
This is meant to affect CPU throttling, which we don't need.
2023-07-02 19:08:58 -05:00
Cameron Gutman
d4079940b4 Implement controller LED and battery state extensions 2023-07-02 19:03:31 -05:00
Cameron Gutman
803ad116fb Pull in latest moonlight-common-c change 2023-06-28 17:28:41 -05:00
Cameron Gutman
27701eda49 Handle paddle buttons on Xbox Elite controllers 2023-06-28 17:28:25 -05:00
Cameron Gutman
71c831b02d Handle 2-direction d-pad key events 2023-06-28 17:22:05 -05:00
Cameron Gutman
0d72a0e009 Implement latest pen/touch protocol updates 2023-06-27 22:21:39 -05:00
Cameron Gutman
69a4502f90 Fix pen/touch coordinates when reported from the background touch view 2023-06-25 00:50:17 -05:00
Cameron Gutman
daaa7f4e63 Fix pen rotation values 2023-06-25 00:26:32 -05:00
Cameron Gutman
d1579e9b0d Support PS4/PS5 touchpad click on older kernels 2023-06-24 23:45:31 -05:00
Cameron Gutman
5890fff240 Add pen and touch events 2023-06-24 23:13:59 -05:00
Cameron Gutman
d6f6307050 Fix units of gyro motion and deduplicate sensor events 2023-06-24 23:12:28 -05:00
Cameron Gutman
6bf9c31860 Pull in updated moonlight-common-c pen/touch APIs 2023-06-24 21:29:44 -05:00
Cameron Gutman
a2e628f3f8 Add controller arrival metadata support 2023-06-24 20:23:43 -05:00
Cameron Gutman
8f5416ff31 Target Android 14 2023-06-24 19:38:34 -05:00
Cameron Gutman
db86f18133 Use new per-activity predictive back support on Android 14
We can't enable it on the Game activity, but we an use it for all other activities.
2023-06-24 19:38:07 -05:00
Cameron Gutman
419e4e656e Use an explicit intent for requesting USB permission 2023-06-24 19:37:12 -05:00
Cameron Gutman
eed4327d26 Add controller touchpad support 2023-06-24 19:09:10 -05:00
Cameron Gutman
5c6eaf2602 Add controller gyro and accelerometer support 2023-06-24 19:06:58 -05:00
Cameron Gutman
71169ed740 Add trigger rumble support 2023-06-24 17:59:10 -05:00
Cameron Gutman
dca8d93aa8 Add Share button mapping for Xbox Series X controller 2023-06-24 16:16:02 -05:00
Cameron Gutman
6cb152f602 Increase gamepad limit to 16 for Sunshine hosts 2023-06-24 16:15:00 -05:00
Cameron Gutman
ddefda3afa Plumb new Sunshine protocol extensions 2023-06-24 15:19:50 -05:00
Cameron Gutman
7f15f45beb Only display the host processing latency if it was present 2023-06-24 15:00:54 -05:00
Timothy Lusk
46f887efec Add host processing latency to performance stats overlay 2023-06-24 14:19:05 -05:00
semjon00
90afecd766 Add option to stream at device native FPS
Useful for phones with overclocked refresh rate
2023-06-24 14:16:49 -05:00
Cameron Gutman
388343c3ee Update to AGP 8.0.2 2023-06-24 14:15:38 -05:00
ns6089
c8df37e89e Compensate for choreographer vsync offset 2023-06-24 13:52:20 -05:00
Clxff H3r4ld0
cc85d5c343 Translated using Weblate (Indonesian)
Currently translated at 79.5% (183 of 230 strings)

Translation: Moonlight Game Streaming/moonlight-android
Translate-URL: https://hosted.weblate.org/projects/moonlight/moonlight-android/id/
2023-05-27 17:50:51 +02:00
Jorys Paulin
f8437cdb8f Translated using Weblate (French)
Currently translated at 100.0% (230 of 230 strings)

Translation: Moonlight Game Streaming/moonlight-android
Translate-URL: https://hosted.weblate.org/projects/moonlight/moonlight-android/fr/
2023-05-21 16:51:09 +02:00
shakedex
c99a210905 Translated using Weblate (Hebrew)
Currently translated at 57.3% (132 of 230 strings)

Translation: Moonlight Game Streaming/moonlight-android
Translate-URL: https://hosted.weblate.org/projects/moonlight/moonlight-android/he/
2023-04-30 10:50:54 +02:00
Yutaro Urata
e33673c9e9 Translated using Weblate (Japanese)
Currently translated at 51.7% (119 of 230 strings)

Translation: Moonlight Game Streaming/moonlight-android
Translate-URL: https://hosted.weblate.org/projects/moonlight/moonlight-android/ja/
2023-04-30 10:50:53 +02:00
shakedex
79283e93cb Translated using Weblate (Hebrew)
Currently translated at 50.4% (116 of 230 strings)

Translation: Moonlight Game Streaming/moonlight-android
Translate-URL: https://hosted.weblate.org/projects/moonlight/moonlight-android/he/
2023-04-27 01:51:19 +02:00
shakedex
a60e85a3a4 Added translation using Weblate (Hebrew) 2023-04-25 23:56:54 +02:00
Simon Nilsson
80910ed38d Translated using Weblate (Swedish)
Currently translated at 100.0% (230 of 230 strings)

Translation: Moonlight Game Streaming/moonlight-android
Translate-URL: https://hosted.weblate.org/projects/moonlight/moonlight-android/sv/
2023-03-31 22:09:41 +02:00
Simon Nilsson
825a338474 Added translation using Weblate (Swedish) 2023-03-29 16:56:54 +02:00
Cameron Gutman
19b6e94824 Add horizontal scrolling in mouse emulation mode 2023-03-04 12:31:31 -06:00
Cameron Gutman
0b581011c5 Version 11.0 2023-02-28 21:31:32 -06:00
Cliff Heraldo
6b7669bb75 Translated using Weblate (Indonesian)
Currently translated at 32.1% (74 of 230 strings)

Translation: Moonlight Game Streaming/moonlight-android
Translate-URL: https://hosted.weblate.org/projects/moonlight/moonlight-android/id/
2023-02-27 10:37:51 +01:00
Cameron Gutman
67bcc56c6d Remove strict codec recovery timeout
Codec recovery depends on incoming frames, so it cannot be reliably time-constrained
2023-02-26 13:26:01 -06:00
Cameron Gutman
7d8cfa3c6a Remove URL filtering to fix wiki links to outside pages 2023-02-25 20:37:49 -06:00
Cameron Gutman
f659af29da Fix mDNS detection of hosts with the same IP address
This is the case for PCs running GFE and Sunshine side-by-side.
2023-02-25 20:29:51 -06:00
Cameron Gutman
94b202f7b6 Update moonlight-common-c 2023-02-25 13:13:05 -06:00
Cameron Gutman
ca86fdafab Merge remote-tracking branch 'origin/weblate' 2023-02-25 13:01:55 -06:00
Cameron Gutman
370dbb1a10 Send non-ASCII soft keys as UTF-8 2023-02-25 12:49:55 -06:00
luten145
f77543cd9b Added clipboard support
You can paste Android's clipboard contents.
2023-02-25 12:30:58 -06:00
Steffen_LT
7104e0d725 Relative mouse fix on Chromebooks with touchscreens 2023-02-25 12:28:49 -06:00
weng weng
230a67cac0 Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (230 of 230 strings)

Translation: Moonlight Game Streaming/moonlight-android
Translate-URL: https://hosted.weblate.org/projects/moonlight/moonlight-android/zh_Hans/
2023-02-24 08:36:54 +01:00
Jen Kung-chih
a695f38974 Translated using Weblate (Chinese (Traditional))
Currently translated at 100.0% (230 of 230 strings)

Translation: Moonlight Game Streaming/moonlight-android
Translate-URL: https://hosted.weblate.org/projects/moonlight/moonlight-android/zh_Hant/
2023-02-23 07:38:22 +01:00
Cameron Gutman
151c09f098 Merge remote-tracking branch 'origin/weblate' 2023-02-21 22:44:00 -06:00
gallegonovato
8200f5690d Translated using Weblate (Spanish)
Currently translated at 100.0% (230 of 230 strings)

Translation: Moonlight Game Streaming/moonlight-android
Translate-URL: https://hosted.weblate.org/projects/moonlight/moonlight-android/es/
2023-02-22 05:43:42 +01:00
Cameron Gutman
77a8cf2704 Remove the touchscreen trackpad option hiding entirely
Fixes #1184
2023-02-21 22:42:35 -06:00
Cameron Gutman
fe424961e1 Update Fastlane metadata 2023-02-21 22:37:24 -06:00
Cameron Gutman
b668cb78ff Update to AGP 7.4.1 2023-02-21 22:36:04 -06:00
bladeoner
48278419b0 Update Gradle to version 7.6 2023-02-21 22:29:32 -06:00
Cameron Gutman
632da03667 Merge remote-tracking branch 'origin/weblate' 2023-02-21 02:15:03 -06:00
Cameron Gutman
d36b73fc1b Fix crash with IPv4-mapped IPv6 addresses 2023-02-20 23:31:53 -06:00
Cameron Gutman
292ed35555 Update moonlight-common-c 2023-02-20 23:24:32 -06:00
Cameron Gutman
02d0ad496f Fix error message being displayed even after successful WoL 2023-02-20 23:01:46 -06:00
Cameron Gutman
eb2fc7af40 Add GameStream EOL notice for GFE PCs 2023-02-20 23:01:08 -06:00
Cameron Gutman
6550deedbb Fix handling of missing addresses 2023-02-20 22:35:27 -06:00
Cameron Gutman
80acd9b9eb Modernize HTTPS launch/resume for Sunshine 2023-02-20 22:04:41 -06:00
Cameron Gutman
b961636f02 Plumb HDR metadata into MediaCodec 2023-02-20 21:42:45 -06:00
Cameron Gutman
f4df0714b5 Implement horizontal scrolling with Sunshine 2023-02-20 19:56:01 -06:00
Cameron Gutman
91dd7b7049 Plumb non-normalized key flag extension for Sunshine 2023-02-20 19:52:52 -06:00
Karim Mreisi
121bef7d2d fix: use address and port for details hash 2023-02-20 13:32:55 -06:00
Karim Mreisi
3a9eabf50b fix: support host names with _
Use a JSON to properly encapsule different computer addresses and their
port, instead of using "_" as separator.

Fix usage of '_' in computer host names / domain names.
2023-02-20 13:32:55 -06:00
bladeoner
c8198b4091 Update bug and feature_request forms 2023-02-20 13:25:03 -06:00
Cameron Gutman
e4538e4a51 Only remove touchscreen-trackpad option on TV devices
Some VR headset devices can make use of this without a proper touchscreen
2023-02-20 13:22:01 -06:00
Cameron Gutman
b47f3ef397 Remove input batching to replace with common-c implementation 2023-02-20 13:19:59 -06:00
Jen Kung-chih
6e096c0ac3 Translated using Weblate (Chinese (Traditional))
Currently translated at 100.0% (229 of 229 strings)

Translation: Moonlight Game Streaming/moonlight-android
Translate-URL: https://hosted.weblate.org/projects/moonlight/moonlight-android/zh_Hant/
2023-02-14 07:37:50 +01:00
weng weng
ee3b4686bf Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (229 of 229 strings)

Translation: Moonlight Game Streaming/moonlight-android
Translate-URL: https://hosted.weblate.org/projects/moonlight/moonlight-android/zh_Hans/
2023-02-14 07:37:49 +01:00
gallegonovato
759b77eafe Translated using Weblate (Spanish)
Currently translated at 100.0% (229 of 229 strings)

Translation: Moonlight Game Streaming/moonlight-android
Translate-URL: https://hosted.weblate.org/projects/moonlight/moonlight-android/es/
2023-02-12 22:38:54 +01:00
Cameron Gutman
de15ec666f Merge remote-tracking branch 'origin/weblate' 2023-02-11 14:27:49 -06:00
Cameron Gutman
3de86f15af Disable HEVC RFI on platforms with older Tegra BSPs
Fixes #1177
2023-02-11 14:26:54 -06:00
Cameron Gutman
1b9dff719c Remove most NVIDIA-specific references in code 2023-02-11 14:25:54 -06:00
Jorys Paulin
abcf4e3d4a Translated using Weblate (French)
Currently translated at 100.0% (228 of 228 strings)

Translation: Moonlight Game Streaming/moonlight-android
Translate-URL: https://hosted.weblate.org/projects/moonlight/moonlight-android/fr/
2023-02-09 11:38:54 +01:00
Jen Kung-chih
9823abe686 Translated using Weblate (Chinese (Traditional))
Currently translated at 100.0% (228 of 228 strings)

Translation: Moonlight Game Streaming/moonlight-android
Translate-URL: https://hosted.weblate.org/projects/moonlight/moonlight-android/zh_Hant/
2023-01-24 09:51:09 +01:00
LUTEN
723e08f69b Translated using Weblate (Korean)
Currently translated at 100.0% (228 of 228 strings)

Translation: Moonlight Game Streaming/moonlight-android
Translate-URL: https://hosted.weblate.org/projects/moonlight/moonlight-android/ko/
2023-01-12 08:49:04 +01:00
ssantos
daf0a0891e Translated using Weblate (Portuguese)
Currently translated at 100.0% (228 of 228 strings)

Translation: Moonlight Game Streaming/moonlight-android
Translate-URL: https://hosted.weblate.org/projects/moonlight/moonlight-android/pt/
2023-01-10 19:51:36 +01:00
Jen Kung-chih
49dc68f77f Translated using Weblate (Chinese (Traditional))
Currently translated at 100.0% (228 of 228 strings)

Translation: Moonlight Game Streaming/moonlight-android
Translate-URL: https://hosted.weblate.org/projects/moonlight/moonlight-android/zh_Hant/
2022-12-30 06:51:13 +01:00
gallegonovato
f81a1c36cc Translated using Weblate (Spanish)
Currently translated at 100.0% (228 of 228 strings)

Translation: Moonlight Game Streaming/moonlight-android
Translate-URL: https://hosted.weblate.org/projects/moonlight/moonlight-android/es/
2022-12-24 15:51:06 +01:00
Dan
9a11a771dd Translated using Weblate (Ukrainian)
Currently translated at 100.0% (228 of 228 strings)

Translation: Moonlight Game Streaming/moonlight-android
Translate-URL: https://hosted.weblate.org/projects/moonlight/moonlight-android/uk/
2022-12-21 01:50:21 +01:00
Translator-3000
b56a4b8b49 Translated using Weblate (Italian)
Currently translated at 100.0% (228 of 228 strings)

Translation: Moonlight Game Streaming/moonlight-android
Translate-URL: https://hosted.weblate.org/projects/moonlight/moonlight-android/it/
2022-12-19 18:49:33 +01:00
Cameron Gutman
1aa963992b Improve Xbox 360W controller numbering 2022-12-16 00:35:19 -06:00
Cameron Gutman
1b601324d0 Remove automatic detection logic for CONFIG_JOYSTICK_XPAD_LEDS=y
It doesn't work due to Android app sandboxing
2022-12-16 00:20:06 -06:00
Cameron Gutman
1aea723ef0 Rework Xbox360W support to handle multiple controllers 2022-12-15 23:33:48 -06:00
Cameron Gutman
1e828a10b9 Request our desired refresh rate rather than the actual frame rate
This ensures we'll get the highest compatible refresh rate rather than the lowest.
2022-12-15 22:53:25 -06:00
Cameron Gutman
970423f873 Use setFrameRate() instead of preferredDisplayModeId if the modes differ only by refresh rate
This seems to avoid a bug on the Chromecast 4K where it can decide to switch to 4K24 instead of 4K60

Fixes #1151
2022-12-14 23:05:22 -06:00
Cameron Gutman
2d7493fd1e Improve check for kernel support for Xbox360W LED configuration 2022-12-14 21:49:42 -06:00
sivan-koren
fa7eb1c4b1 Set Lights on XBOX360 Wireless Controllers for New Android/Google TVs Through 2023 - Fixes: #1061 (#1157) 2022-12-14 21:29:41 -06:00
SpeedPartner
4916af3697 Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (228 of 228 strings)

Translation: Moonlight Game Streaming/moonlight-android
Translate-URL: https://hosted.weblate.org/projects/moonlight/moonlight-android/zh_Hans/
2022-12-08 14:47:25 +01:00
Eric
8484bf1cb9 Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (228 of 228 strings)

Translation: Moonlight Game Streaming/moonlight-android
Translate-URL: https://hosted.weblate.org/projects/moonlight/moonlight-android/zh_Hans/
2022-12-08 14:47:25 +01:00
SpeedPartner
005afb3c73 Translated using Weblate (Chinese (Simplified))
Currently translated at 98.6% (225 of 228 strings)

Translation: Moonlight Game Streaming/moonlight-android
Translate-URL: https://hosted.weblate.org/projects/moonlight/moonlight-android/zh_Hans/
2022-12-07 14:04:03 +01:00
Jen Kung-chih
84b0d004b9 Translated using Weblate (Chinese (Traditional))
Currently translated at 100.0% (228 of 228 strings)

Translation: Moonlight Game Streaming/moonlight-android
Translate-URL: https://hosted.weblate.org/projects/moonlight/moonlight-android/zh_Hant/
2022-12-07 11:47:26 +01:00
Cameron Gutman
aa41bf8d97 Version 10.11 2022-12-04 13:54:37 -06:00
Cameron Gutman
fba9a125bf Merge remote-tracking branch 'origin/weblate' 2022-12-01 18:40:24 -06:00
jonathanmasseurca
27312bd146 Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (228 of 228 strings)

Translation: Moonlight Game Streaming/moonlight-android
Translate-URL: https://hosted.weblate.org/projects/moonlight/moonlight-android/pt_BR/
2022-12-02 01:39:46 +01:00
Jen Kung-chih
8f0c267ab8 Translated using Weblate (Chinese (Traditional))
Currently translated at 100.0% (228 of 228 strings)

Translation: Moonlight Game Streaming/moonlight-android
Translate-URL: https://hosted.weblate.org/projects/moonlight/moonlight-android/zh_Hant/
2022-12-02 01:39:46 +01:00
Zaraza225
a15d6a6b42 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (228 of 228 strings)

Translation: Moonlight Game Streaming/moonlight-android
Translate-URL: https://hosted.weblate.org/projects/moonlight/moonlight-android/uk/
2022-12-02 01:39:45 +01:00
jonathanmasseurca
8f9061b250 Translated using Weblate (French)
Currently translated at 100.0% (228 of 228 strings)

Translation: Moonlight Game Streaming/moonlight-android
Translate-URL: https://hosted.weblate.org/projects/moonlight/moonlight-android/fr/
2022-12-02 01:39:45 +01:00
Cameron Gutman
ec57499e08 Handle escaping and unescaping IPv6 addresses in AddressTuple 2022-12-01 18:26:40 -06:00
Cameron Gutman
381598c5b6 Fix handling of IPv6 literals when adding a PC
Fixes #1152
2022-12-01 18:26:40 -06:00
Cameron Gutman
452d020da5 Merge remote-tracking branch 'origin/weblate' 2022-11-30 21:07:34 -06:00
Eric
b5f875c2e5 Translated using Weblate (Chinese (Simplified))
Currently translated at 99.1% (226 of 228 strings)

Translation: Moonlight Game Streaming/moonlight-android
Translate-URL: https://hosted.weblate.org/projects/moonlight/moonlight-android/zh_Hans/
2022-12-01 04:06:55 +01:00
Brandon Goldberg
a31daeda96 Translated using Weblate (Spanish)
Currently translated at 100.0% (228 of 228 strings)

Translation: Moonlight Game Streaming/moonlight-android
Translate-URL: https://hosted.weblate.org/projects/moonlight/moonlight-android/es/
2022-12-01 04:06:55 +01:00
Cameron Gutman
437f52f53a Tweak full range option text 2022-11-30 00:16:05 -06:00
Cameron Gutman
33f0f7ecf0 Use Ctrl+Alt+Shift+Z as the unbind toggle to match other Moonlight clients 2022-11-29 23:28:13 -06:00
Cameron Gutman
6777e79e70 Fix inverted mouse capture bug 2022-11-29 23:25:39 -06:00
Cameron Gutman
16d1e6181b Update moonlight-common-c 2022-11-29 19:16:33 -06:00
Cameron Gutman
a6c8db6c2c Introduce full range color option 2022-11-29 19:10:19 -06:00
Cameron Gutman
24aa0fecbe Revise HEVC option text 2022-11-29 19:05:30 -06:00
Cameron Gutman
1a776b1990 Add Czech translation to language list
Fixes #1147
2022-11-29 18:42:28 -06:00
Cameron Gutman
27df265c81 Merge remote-tracking branch 'origin/weblate' 2022-11-29 18:37:27 -06:00
Zaraza225
84c0372719 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (226 of 226 strings)

Translation: Moonlight Game Streaming/moonlight-android
Translate-URL: https://hosted.weblate.org/projects/moonlight/moonlight-android/uk/
2022-11-23 21:47:27 +01:00
Cameron Gutman
3879e57c4c Track network changes to invalidate PC online state appropriately 2022-11-21 23:15:19 -06:00
Cameron Gutman
dcc3dcdaba Only match ports if the PC is online 2022-11-21 23:00:51 -06:00
Loïc Hesling
d166635c7b Translated using Weblate (French)
Currently translated at 100.0% (226 of 226 strings)

Translation: Moonlight Game Streaming/moonlight-android
Translate-URL: https://hosted.weblate.org/projects/moonlight/moonlight-android/fr/
2022-11-17 12:47:40 +01:00
Cameron Gutman
33d484b7d1 Remove specific RFI opt-in for Sabrina since it's supported out of the box in Android 12 2022-11-13 19:10:35 -06:00
luten145
26bff28e4d Added MetaKey(WindowKey) Packet
Allows you to use Windows key combinations.

ex) Win+Tab , Win+D
2022-11-13 17:15:22 -06:00
Cameron Gutman
56eddff8d6 Default to Rec 709 on modern devices
Fixes #1138
Closes #1143
2022-11-13 13:47:41 -06:00
Cameron Gutman
37b9133eb6 Correct media performance class check
Media performance class is 12+ even though it has values for 11+
2022-11-13 13:27:43 -06:00
Cameron Gutman
4a64967b1f Ungrab meta key capture when toggling input capture 2022-11-13 13:19:23 -06:00
Translator-3000
23152b1264 Translated using Weblate (Italian)
Currently translated at 100.0% (226 of 226 strings)

Translation: Moonlight Game Streaming/moonlight-android
Translate-URL: https://hosted.weblate.org/projects/moonlight/moonlight-android/it/
2022-11-13 13:50:24 +01:00
Cameron Gutman
00415aac79 Version 10.10 2022-11-11 12:21:25 -06:00
Cameron Gutman
cbe602655c Pass active HTTPS port if the HTTP port matches the active address 2022-11-09 20:53:06 -06:00
Cameron Gutman
236d8b7030 Extend timeouts for the PC's active address 2022-11-09 20:31:58 -06:00
Cameron Gutman
392e3c7fe3 Increase connection timeouts when the PC is presumed to be online 2022-11-09 20:22:07 -06:00
Cameron Gutman
57f55e6856 Use the current HTTP port as the default if ExternalPort doesn't exist 2022-11-09 19:56:46 -06:00
Cameron Gutman
de54b27013 Plumb HTTPS port into the Game activity to avoid having to look it up again 2022-11-09 19:55:42 -06:00
Dominik Chrástecký
c11338039f Translated using Weblate (Czech)
Currently translated at 100.0% (226 of 226 strings)

Translation: Moonlight Game Streaming/moonlight-android
Translate-URL: https://hosted.weblate.org/projects/moonlight/moonlight-android/cs/
2022-11-09 16:47:44 +01:00
Jen Kung-chih
e712669d32 Translated using Weblate (Chinese (Traditional))
Currently translated at 100.0% (226 of 226 strings)

Translation: Moonlight Game Streaming/moonlight-android
Translate-URL: https://hosted.weblate.org/projects/moonlight/moonlight-android/zh_Hant/
2022-11-09 16:47:44 +01:00
Brandon Goldberg
3768ae33b7 Translated using Weblate (Spanish)
Currently translated at 100.0% (226 of 226 strings)

Translation: Moonlight Game Streaming/moonlight-android
Translate-URL: https://hosted.weblate.org/projects/moonlight/moonlight-android/es/
2022-11-09 16:47:43 +01:00
Cameron Gutman
fdc39f0041 Merge remote-tracking branch 'origin/weblate' 2022-11-06 19:04:43 -06:00
Cameron Gutman
7f3b0b03a6 Add C2 equivalents for OMX decoders for futureproofing 2022-11-06 18:17:48 -06:00
Cameron Gutman
4a6a39dd4c Disable HEVC RFI on Fire TV 3 due to decoder hangs 2022-11-06 18:13:19 -06:00
Cameron Gutman
6a8486a076 Fix propagation of external port after guessing 2022-11-06 18:06:18 -06:00
Cameron Gutman
08a8a3043f Update moonlight-common-c with improved high quality audio 2022-11-06 17:37:13 -06:00
Cameron Gutman
7af290b6e1 Implement support for non-default ports with Sunshine
Fixes #1115
2022-11-06 17:36:46 -06:00
Cameron Gutman
a896f9a28f Use the HTTPS port specified in the serverinfo response 2022-11-06 15:44:37 -06:00
Cameron Gutman
ea003483c4 Plumb port numbers from mDNS discovery 2022-11-06 14:41:02 -06:00
Cameron Gutman
5b73317e30 Fix error handling if the server address cannot be resolved 2022-11-06 14:34:31 -06:00
Howard Wu
1af64b9985 Set forceDarkAllowed to false
Some system like MIUI forced inverse color (which cannot be turned off for games with night mode on) causes games without covers to become white, which like the game title color causes unreadability, this change prevents that problem.

ref https://stackoverflow.com/questions/63777438/how-to-avoid-forced-dark-theme-in-my-app-when-devices-can-force-it-at-app-level
2022-11-04 22:01:08 -05:00
Cameron Gutman
af784cf79b Fix typo in boolean logic 2022-11-04 01:22:19 -05:00
Cameron Gutman
a2b2131beb Add support for codec flush recovery 2022-11-04 01:20:00 -05:00
Cameron Gutman
2433ce8d24 Fix crashes on Fire OS 8 2022-11-03 23:17:15 -05:00
Cameron Gutman
8b861750e5 Update moonlight-common-c with improved video and audio packet loss handling 2022-11-03 22:20:39 -05:00
Cameron Gutman
99fcd3c669 Improve LAN/WAN detection for IPv6 and cellular connections 2022-11-03 22:19:48 -05:00
Cameron Gutman
0ddd8df272 Use HEVC by default if the decoder supports FEATURE_LowLatency or the media performance class is 12+ 2022-10-31 01:05:01 -05:00
TacoTheDank
a96e508ffb Use try-with-resources 2022-10-31 00:33:09 -05:00
이정희
1f21d12d2b Translated using Weblate (Korean)
Currently translated at 100.0% (226 of 226 strings)

Translation: Moonlight Game Streaming/moonlight-android
Translate-URL: https://hosted.weblate.org/projects/moonlight/moonlight-android/ko/
2022-10-30 15:07:04 +01:00
sanhoe
dd782ac4b2 Translated using Weblate (Korean)
Currently translated at 100.0% (226 of 226 strings)

Translation: Moonlight Game Streaming/moonlight-android
Translate-URL: https://hosted.weblate.org/projects/moonlight/moonlight-android/ko/
2022-10-29 06:12:57 +02:00
Cameron Gutman
51594e00b8 Revert "Use Rec 2020 colorspace for WCG support even if HDR is off on the host"
Rec 2020 conversion causes colors to be blown out in SDR

This reverts commit 6c85f5f8c3.
2022-10-13 01:18:26 -05:00
Cameron Gutman
6c85f5f8c3 Use Rec 2020 colorspace for WCG support even if HDR is off on the host 2022-10-13 00:52:45 -05:00
Cameron Gutman
d0432de981 Plumb colorspace and color range into MediaCodecDecoderRenderer 2022-10-13 00:51:15 -05:00
Cameron Gutman
2cbc94e51d Allow a pairing attempt even if the PC is busy
Pairing while busy doesn't work with GFE but works with Sunshine
2022-10-12 22:15:41 -05:00
Cameron Gutman
3ea2aa1f74 Enable HEVC RFI on Fire TV and Chromecast devices 2022-10-12 21:50:40 -05:00
Cameron Gutman
1076b516d6 Enable HEVC RFI for decoders that support low latency options 2022-10-12 21:25:48 -05:00
bruh
4e87d25851 Translated using Weblate (Vietnamese)
Currently translated at 93.8% (212 of 226 strings)

Translation: Moonlight Game Streaming/moonlight-android
Translate-URL: https://hosted.weblate.org/projects/moonlight/moonlight-android/vi/
2022-10-12 18:26:37 +02:00
LedyBacer
dadd3c7292 Translated using Weblate (Russian)
Currently translated at 100.0% (226 of 226 strings)

Translation: Moonlight Game Streaming/moonlight-android
Translate-URL: https://hosted.weblate.org/projects/moonlight/moonlight-android/ru/
2022-10-12 18:26:37 +02:00
Jen Kung-chih
9f8abe35f9 Translated using Weblate (Chinese (Traditional))
Currently translated at 100.0% (226 of 226 strings)

Translation: Moonlight Game Streaming/moonlight-android
Translate-URL: https://hosted.weblate.org/projects/moonlight/moonlight-android/zh_Hant/
2022-10-10 19:59:49 +02:00
Eric
0f869a7414 Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (226 of 226 strings)

Translation: Moonlight Game Streaming/moonlight-android
Translate-URL: https://hosted.weblate.org/projects/moonlight/moonlight-android/zh_Hans/
2022-10-10 19:59:49 +02:00
Cameron Gutman
aede16c85c Version 10.9 2022-10-07 22:02:56 -05:00
Cameron Gutman
61a82e6394 Merge remote-tracking branch 'origin/weblate' 2022-10-07 21:55:19 -05:00
Sargon-Isa
5a92925d6a Translated using Weblate (German)
Currently translated at 100.0% (226 of 226 strings)

Translation: Moonlight Game Streaming/moonlight-android
Translate-URL: https://hosted.weblate.org/projects/moonlight/moonlight-android/de/
2022-10-08 04:54:57 +02:00
Cameron Gutman
fe697c918f Update moonlight-common-c with speculative RFI support 2022-10-07 21:54:00 -05:00
Cameron Gutman
bc57a285ce Fix unescaped character 2022-10-04 20:03:10 -05:00
Cameron Gutman
85d8943b64 Merge remote-tracking branch 'origin/weblate' 2022-10-04 19:56:52 -05:00
Cameron Gutman
aa6c32968b Add a special termination message for ML_ERROR_FRAME_CONVERSION 2022-10-04 19:51:49 -05:00
Cameron Gutman
ad1808fb4e Update moonlight-common-c with further fixes for GFE 3.26 2022-10-04 19:50:49 -05:00
Kamil Szyc
576610e4c3 Translated using Weblate (Polish)
Currently translated at 1.7% (4 of 225 strings)

Translation: Moonlight Game Streaming/moonlight-android
Translate-URL: https://hosted.weblate.org/projects/moonlight/moonlight-android/pl/
2022-10-04 12:24:06 +02:00
Martin Dimitrov
ace2266f14 Translated using Weblate (Bulgarian)
Currently translated at 59.5% (134 of 225 strings)

Translation: Moonlight Game Streaming/moonlight-android
Translate-URL: https://hosted.weblate.org/projects/moonlight/moonlight-android/bg/
2022-10-04 12:24:06 +02:00
Alexandru-Marian Buza
41cedfa6ec Fix requestMetaKeyEvent for Samsung devices with android 10+ (#1134)
Co-authored-by: Alexandru Buza <abuza@iqnox.com>
2022-10-03 22:50:04 -05:00
Cameron Gutman
d46fab33b3 Enable HEVC RFI for Exynos decoders 2022-10-03 22:23:59 -05:00
Cameron Gutman
585dc45595 Enable RFI for HEVC on Qualcomm and Nvidia decoders 2022-10-03 21:33:05 -05:00
Cameron Gutman
c3c9354a00 Update moonlight-common-c to support reliable RFI for HEVC 2022-10-03 21:32:11 -05:00
Cameron Gutman
bdc8d08e65 Switch back to AGP 7.2.2
AGP 7.3.0 produces invalid bytecode for ControllerHandler, causing dex validation errors on Android Jelly Bean and KitKat

Fixes #1132
2022-10-03 21:30:01 -05:00
Cameron Gutman
9c792d3272 Adjust RendererException text to attempt to parse correctly in Google Play App Vitals 2022-10-03 21:28:37 -05:00
Cameron Gutman
23bc4daf9f Refactor input event handling in the Game activity 2022-10-03 21:25:43 -05:00
Kamil Szyc
fd85ca2004 Added translation using Weblate (Polish) 2022-10-03 11:42:50 +02:00
Martin Dimitrov
aadf88add1 Added translation using Weblate (Bulgarian) 2022-10-02 19:24:17 +02:00
sanhoe
f14ce61ee3 Translated using Weblate (Korean)
Currently translated at 100.0% (225 of 225 strings)

Translation: Moonlight Game Streaming/moonlight-android
Translate-URL: https://hosted.weblate.org/projects/moonlight/moonlight-android/ko/
2022-09-29 12:16:23 +02:00
Cameron Gutman
539daf5789 Don't adjust maxBytesPerPicDenom and maxBitsPerMbDenom on newer devices 2022-09-23 21:27:27 -05:00
Cameron Gutman
e8ea2a8ec1 Version 10.8.4 2022-09-22 23:17:58 -05:00
Cameron Gutman
9ed3b3a9df Fixed streaming on certain devices with GFE 3.26 2022-09-22 23:16:40 -05:00
Cameron Gutman
12487553de Version 10.8.2 2022-09-22 21:57:42 -05:00
Cameron Gutman
9c1a618b4a Fix stuck analog stick when a touch event is cancelled
This can happen if a stylus hover event is received while touching an OSC element
2022-09-21 01:11:45 -05:00
Cameron Gutman
ac0e784417 Make StreamView transparent to touch events and handle everything in the background view
This is much simpler than trying to play games with touch handling between 2 views
2022-09-21 01:07:49 -05:00
Cameron Gutman
48cab6b203 Allow multi-finger gestures and absolute motion to pass seamlessly between the StreamView and background view 2022-09-21 00:21:43 -05:00
Cameron Gutman
e1c0472069 Properly split touch events between regions outside the StreamView and the OSC
This restores the ability to use area outside the StreamView for the virtual trackpad and adds the ability to use OSC and the non-StreamView region for input at the same time.

Fixes #1129
2022-09-20 22:29:54 -05:00
Cameron Gutman
2c498ce707 Throw a RendererException instead of a bare IllegalStateException upon codec recovery failure 2022-09-20 21:43:35 -05:00
Cameron Gutman
bc483edb29 Interrupt codec recovery when stopping the decoder 2022-09-18 18:53:37 -05:00
Cameron Gutman
9762f4c412 Only throw the codec exception on the last configuration attempt 2022-09-18 18:47:01 -05:00
Cameron Gutman
5bfce88fc5 Fix recovery timeout if no output frames are being received 2022-09-18 18:37:33 -05:00
Cameron Gutman
94ef66994d Trigger the decoder crash dialog if all recovery attempts fail 2022-09-18 18:29:45 -05:00
Cameron Gutman
257c29daca Improve handling of concurrent recoverable and non-recoverable errors and surface loss 2022-09-18 18:25:29 -05:00
Cameron Gutman
173483eb84 Only catch IllegalStateException or subclasses 2022-09-18 17:42:37 -05:00
Cameron Gutman
06099b2663 Only try to recover from CodecExceptions or IllegalStateExceptions 2022-09-18 00:20:41 -05:00
Cameron Gutman
33c1f0a71c Fix decoding crash if encoder didn't send VUI parameters 2022-09-18 00:04:29 -05:00
Cameron Gutman
a3d78f1d80 Merge remote-tracking branch 'origin/weblate' 2022-09-17 23:32:55 -05:00
Cameron Gutman
c573d213f8 Allow FFmpeg decoder on Waydroid 2022-09-17 14:51:03 -05:00
Cameron Gutman
c72707aef9 Don't begin codec recovery if stopping 2022-09-17 13:52:22 -05:00
Cameron Gutman
313ef06c86 Only exclude touch events from non-view processing
Mouse events that go out of the StreamView area are okay
2022-09-17 13:36:44 -05:00
Cameron Gutman
6b79340c15 Don't handle motion events outside of Views to avoid spurious stream input while using OSC 2022-09-17 13:34:14 -05:00
Cameron Gutman
d9a5b29372 Fix OSC handling of touches outside the StreamView 2022-09-17 13:32:40 -05:00
Cameron Gutman
d2b0e093fc Reduce power by avoiding resends when OSC state is not changing 2022-09-17 13:07:52 -05:00
Cameron Gutman
945e563912 Switch to a Handler for gamepad mouse emulation 2022-09-17 12:55:15 -05:00
Cameron Gutman
a7efa379eb Switch to a Handler for OSC retransmission 2022-09-16 18:21:56 -05:00
Cameron Gutman
d04df4ebe5 Fix D-Pad buttons not releasing until all D-Pad input has ceased 2022-09-16 17:41:52 -05:00
Cameron Gutman
2a2c84ef3a Implement fallbacks for a failed codec restart or reset 2022-09-16 03:48:49 -05:00
Cameron Gutman
bc97db893a Allow recovery of IllegalStateExceptions for older versions of Android 2022-09-16 03:28:57 -05:00
Cameron Gutman
f216834df7 Limit the number of codec recovery attempts 2022-09-16 03:27:22 -05:00
Cameron Gutman
be25a7d594 Fix a number of bugs in new codec recovery code 2022-09-16 03:19:36 -05:00
Cameron Gutman
10f43e8024 Try to adjust decoder exception to comply with Google Play crash message filtering 2022-09-16 00:32:34 -05:00
Cameron Gutman
bbb3e8d071 Only catch RuntimeExceptions for decoders to avoid eating important exceptions 2022-09-16 00:26:02 -05:00
Cameron Gutman
4c3af35156 Update AGP to 7.3.0 2022-09-16 00:09:22 -05:00
Cameron Gutman
8656228014 Break out of wait on InterruptedException 2022-09-16 00:09:09 -05:00
Cameron Gutman
03f9ea8435 Use Handlers instead of Timers for one-shot events 2022-09-16 00:08:48 -05:00
Cameron Gutman
9cf27d8fb1 Don't throw exceptions during codec recovery 2022-09-15 02:16:24 -05:00
Cameron Gutman
d1b24ea6af Consolidate touch tracking timers 2022-09-15 02:05:40 -05:00
Cameron Gutman
b07ffbde29 Consolidate OSC timers 2022-09-15 01:59:29 -05:00
Cameron Gutman
1673236940 Abort if the decoder doesn't recover within 5 seconds 2022-09-15 01:37:10 -05:00
Cameron Gutman
06861a2d17 Add support for recovering from non-transient CodecExceptions 2022-09-15 01:15:15 -05:00
Cameron Gutman
ef7ac62f97 Improve handling of transient CodecExceptions 2022-09-15 00:08:06 -05:00
Cameron Gutman
245a9f2751 Try a new input buffer if getInputBuffer() returns null 2022-09-14 23:54:07 -05:00
Cameron Gutman
1d38f158b5 Fix crash after the next fetchNextInputBuffer() if getInputBuffer() failed previously 2022-09-14 23:49:49 -05:00
Jorys Paulin
62a526854d Translated using Weblate (French)
Currently translated at 100.0% (225 of 225 strings)

Translation: Moonlight Game Streaming/moonlight-android
Translate-URL: https://hosted.weblate.org/projects/moonlight/moonlight-android/fr/
2022-09-14 15:22:45 +02:00
Jen Kung-chih
3dda940c92 Translated using Weblate (Chinese (Traditional))
Currently translated at 100.0% (225 of 225 strings)

Translation: Moonlight Game Streaming/moonlight-android
Translate-URL: https://hosted.weblate.org/projects/moonlight/moonlight-android/zh_Hant/
2022-09-13 08:19:00 +02:00
Howard Wu
ab77c4720d Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (225 of 225 strings)

Translation: Moonlight Game Streaming/moonlight-android
Translate-URL: https://hosted.weblate.org/projects/moonlight/moonlight-android/zh_Hans/
2022-09-13 08:18:59 +02:00
Cameron Gutman
c8f1f9325e Version 10.8.1 2022-09-11 23:47:34 -05:00
Cameron Gutman
658940d3fb Fix mishandling of IDR frames with a SEI or AUD NAL 2022-09-11 23:45:12 -05:00
Cameron Gutman
51b4ca401e Merge remote-tracking branch 'origin/weblate' 2022-09-11 23:42:58 -05:00
reloxx13
10e4ca4ef3 Translated using Weblate (German)
Currently translated at 100.0% (225 of 225 strings)

Translation: Moonlight Game Streaming/moonlight-android
Translate-URL: https://hosted.weblate.org/projects/moonlight/moonlight-android/de/
2022-09-12 06:41:34 +02:00
Cameron Gutman
2bcc2bdfe5 Version 10.8 2022-09-11 22:42:49 -05:00
Cameron Gutman
6462b580bb Merge remote-tracking branch 'origin/weblate' 2022-09-08 17:56:35 -05:00
Daniel Saburi
b83d91c944 Translated using Weblate (Portuguese)
Currently translated at 9.3% (21 of 225 strings)

Translation: Moonlight Game Streaming/moonlight-android
Translate-URL: https://hosted.weblate.org/projects/moonlight/moonlight-android/pt/
2022-09-09 00:55:56 +02:00
Daniel Saburi
07f842bc9e Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (225 of 225 strings)

Translation: Moonlight Game Streaming/moonlight-android
Translate-URL: https://hosted.weblate.org/projects/moonlight/moonlight-android/pt_BR/
2022-09-09 00:55:56 +02:00
Cameron Gutman
3913e845fa Add workaround for MITV4-ANSM0 low latency mode bug
Fixes #1122
2022-09-08 17:43:17 -05:00
Cameron Gutman
09f0913974 Only request unbuffered touch events on Android 11+ 2022-09-08 17:30:19 -05:00
Cameron Gutman
aa9ca35115 Fix refresh rate reduction for non-exact frame rate matches 2022-09-08 17:28:43 -05:00
Cameron Gutman
010dfdf834 Reload the settings page when switching between screens on a foldable device 2022-09-08 17:26:54 -05:00
Cameron Gutman
150fac9c09 Remove the TV refresh rate workaround now that users must opt-in to lowering the refresh rate 2022-09-06 23:10:39 -05:00
Cameron Gutman
ec3aef13d8 Merge remote-tracking branch 'origin/weblate' 2022-09-06 23:00:54 -05:00
Cameron Gutman
2b56005bd2 Display portrait resolution first 2022-09-06 22:52:43 -05:00
Cameron Gutman
9bc893b6ad Allow both portrait and landscape native orientations on square displays 2022-09-06 22:50:43 -05:00
Cameron Gutman
3feb92e788 Force landscape mode when using OSC 2022-09-06 22:07:44 -05:00
Cameron Gutman
1265952814 Allow streaming in any orientation when using a square display 2022-09-06 21:24:54 -05:00
Cameron Gutman
f5ad5d97db Fix tapping using the virtual trackpad on the Z Fold 4 2022-09-06 20:53:45 -05:00
Cameron Gutman
5ac0939731 Don't reduce refresh rate by default in balanced mode 2022-09-06 20:21:53 -05:00
Cameron Gutman
b653694860 Request unbuffered input events to reduce input latency 2022-09-06 19:09:31 -05:00
Cameron Gutman
49051a5095 Prefetch input buffers while waiting for the next frame to arrive 2022-09-06 00:59:45 -05:00
Cameron Gutman
7734de6465 Fix handling of 3 byte Annex B start sequences 2022-09-05 22:32:13 -05:00
TacoTheDank
edac646434 Regenerate drawables 2022-09-03 12:08:51 -05:00
sanhoe
51c7665fdc Translated using Weblate (Korean)
Currently translated at 100.0% (221 of 221 strings)

Translation: Moonlight Game Streaming/moonlight-android
Translate-URL: https://hosted.weblate.org/projects/moonlight/moonlight-android/ko/
2022-09-02 17:18:21 +02:00
Jen Kung-chih
37545821fc Translated using Weblate (Chinese (Traditional))
Currently translated at 100.0% (221 of 221 strings)

Translation: Moonlight Game Streaming/moonlight-android
Translate-URL: https://hosted.weblate.org/projects/moonlight/moonlight-android/zh_Hant/
2022-08-28 05:22:25 +02:00
Brandon Goldberg
8a1ed0f146 Translated using Weblate (Spanish)
Currently translated at 100.0% (221 of 221 strings)

Translation: Moonlight Game Streaming/moonlight-android
Translate-URL: https://hosted.weblate.org/projects/moonlight/moonlight-android/es/
2022-08-25 02:22:25 +02:00
Jen Kung-chih
7a0228fb81 Translated using Weblate (Chinese (Traditional))
Currently translated at 100.0% (221 of 221 strings)

Translation: Moonlight Game Streaming/moonlight-android
Translate-URL: https://hosted.weblate.org/projects/moonlight/moonlight-android/zh_Hant/
2022-08-21 18:21:42 +02:00
Sargon-Isa
3aecf9e031 Translated using Weblate (German)
Currently translated at 100.0% (221 of 221 strings)

Translation: Moonlight Game Streaming/moonlight-android
Translate-URL: https://hosted.weblate.org/projects/moonlight/moonlight-android/de/
2022-08-14 15:21:45 +02:00
Sargon-Isa
c2d4d221af Translated using Weblate (German)
Currently translated at 99.0% (219 of 221 strings)

Translation: Moonlight Game Streaming/moonlight-android
Translate-URL: https://hosted.weblate.org/projects/moonlight/moonlight-android/de/
2022-08-11 19:15:29 +02:00
Cameron Gutman
53f89fbe22 Update AGP to 7.2.2 2022-08-04 21:29:43 -05:00
Cameron Gutman
eb5f7ef7af Version 10.7 2022-08-04 21:13:01 -05:00
Cameron Gutman
e2fc76d21d Merge remote-tracking branch 'origin/weblate' 2022-08-02 18:36:49 -05:00
Wen-haur Chiu
1754103175 Translated using Weblate (Chinese (Traditional))
Currently translated at 100.0% (221 of 221 strings)

Translation: Moonlight Game Streaming/moonlight-android
Translate-URL: https://hosted.weblate.org/projects/moonlight/moonlight-android/zh_Hant/
2022-08-03 01:36:20 +02:00
Cameron Gutman
dacd00708f Don't sleep on the main thread in touch processing code 2022-08-02 18:20:05 -05:00
Cameron Gutman
a73129243c Compensate for button down time when computing extra minimum button down 2022-08-02 18:14:10 -05:00
Cameron Gutman
54a6aa9081 Remove emulated button sleeps 2022-08-02 18:08:53 -05:00
Cameron Gutman
0fbb53c606 Remove MediaCodecHelper.getMonotonicMillis() 2022-08-02 18:08:12 -05:00
Cameron Gutman
eb2e79977d Use event time on input events rather than current uptime 2022-08-02 18:07:15 -05:00
Cameron Gutman
b70a47f5e5 Negotiate the higher of the two decoder slices-per-frame preferences to provide best performance 2022-08-01 22:26:00 -05:00
Cameron Gutman
9d5ff72548 Update OpenSSL to fix _armv7_tick() crash
OpenSSL 1.1.1q + fe1a23ccf7
2022-08-01 21:50:27 -05:00
Grider
6b972b56a5 Add support for Samsung DeX mode desktop mouse(touchpad) events 2022-07-31 17:26:18 -05:00
Cameron Gutman
a80d30baf7 Revert "Add support for 8bitdo sn30 pro xCloud (#1102)"
The broken mapping is due to an old firmware (see #978).

This reverts commit bfc3116661.
2022-07-31 17:12:52 -05:00
Cameron Gutman
b9280e9a8e Correct language name for Brazilian Portuguese 2022-07-27 23:36:03 -05:00
Cameron Gutman
c6640d201c Merge remote-tracking branch 'origin/weblate' 2022-07-27 23:34:33 -05:00
LUTEN VR
795fdc3605 Translated using Weblate (Korean)
Currently translated at 99.5% (220 of 221 strings)

Translation: Moonlight Game Streaming/moonlight-android
Translate-URL: https://hosted.weblate.org/projects/moonlight/moonlight-android/ko/
2022-07-28 06:19:17 +02:00
Cameron Gutman
05311da33d Use 4 slices per frame for software decoders 2022-07-22 20:14:53 -05:00
Cameron Gutman
2e442cb1d1 Only use 4 slices per frame on old Qualcomm devices that benefit from it
Using it everywhere decreases encoding efficiency for no gain in performance
2022-07-22 18:43:59 -05:00
Cameron Gutman
fe322590cc Add an option to allow equalizer effects 2022-07-22 00:23:11 -05:00
Cameron Gutman
6cf9b25c04 Fix incorrect name for certain languages 2022-07-21 23:39:47 -05:00
Cameron Gutman
417babb3d4 Add Brazilian Portuguese to language options 2022-07-21 23:38:08 -05:00
Cameron Gutman
bdaaa6f0c7 Merge remote-tracking branch 'origin/weblate' 2022-07-21 21:21:35 -05:00
mltgames
7e92dd7fe4 Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (219 of 219 strings)

Translation: Moonlight Game Streaming/moonlight-android
Translate-URL: https://hosted.weblate.org/projects/moonlight/moonlight-android/pt_BR/
2022-07-21 14:18:18 +02:00
Wen-haur Chiu
50601e24ed Translated using Weblate (Chinese (Traditional))
Currently translated at 100.0% (219 of 219 strings)

Translation: Moonlight Game Streaming/moonlight-android
Translate-URL: https://hosted.weblate.org/projects/moonlight/moonlight-android/zh_Hant/
2022-07-21 14:18:18 +02:00
peerobo
bfc3116661 Add support for 8bitdo sn30 pro xCloud (#1102)
* add support for 8bitdo sn30 pro xCloud

Co-authored-by: peerobo <phuongrobotics@me.com>
2022-07-20 08:37:32 -05:00
Cameron Gutman
264b6e54f2 Add Codec2 variants in the decoder prefix lists 2022-07-19 23:51:11 -05:00
Cameron Gutman
1ed7ecc82f Don't use FLAG_BYPASS_INTERRUPTION_POLICY
See https://issuetracker.google.com/issues/235875658
2022-07-19 23:02:31 -05:00
Chase Payne
19b8032d06 Fixes an issue that caused televisions to have frame pacing problems when setting the refresh rate below 50hz 2022-07-12 23:56:38 -05:00
Wen-haur Chiu
1e254ea8f4 Translated using Weblate (Chinese (Traditional))
Currently translated at 100.0% (219 of 219 strings)

Translation: Moonlight Game Streaming/moonlight-android
Translate-URL: https://hosted.weblate.org/projects/moonlight/moonlight-android/zh_Hant/
2022-07-12 06:20:34 +02:00
Cameron Gutman
babfc99c35 Version 10.6 2022-07-07 23:17:08 -05:00
Bail Adnan Farid
5c802555a2 Translated using Weblate (Indonesian)
Currently translated at 35.6% (78 of 219 strings)

Translation: Moonlight Game Streaming/moonlight-android
Translate-URL: https://hosted.weblate.org/projects/moonlight/moonlight-android/id/
2022-07-07 16:15:42 +02:00
Bail Adnan Farid
fbc41c9a4e Added translation using Weblate (Indonesian) 2022-07-05 16:10:15 +02:00
Cameron Gutman
1eca461cb1 Merge remote-tracking branch 'origin/weblate' 2022-07-04 17:51:36 -05:00
Cameron Gutman
ebd327c7a6 Use new ShieldControllerExtensions library for Shield Controller rumble support
https://github.com/cgutman/ShieldControllerExtensions
2022-06-30 18:04:02 -05:00
Cameron Gutman
602febe876 Use onPictureInPictureRequested() to enter PiP on Android 11 2022-06-29 23:28:52 -05:00
Cameron Gutman
84fcd3ae6a Use requestMetaKeyEvent API on Samsung devices
Inspired by #1078
2022-06-28 22:07:40 -05:00
Cameron Gutman
84296c6e1c Toggle the IME with a 3 finger tap rather than only showing it 2022-06-28 21:40:59 -05:00
Jorys Paulin
6012e0ea8c Translated using Weblate (French)
Currently translated at 100.0% (219 of 219 strings)

Translation: Moonlight Game Streaming/moonlight-android
Translate-URL: https://hosted.weblate.org/projects/moonlight/moonlight-android/fr/
2022-06-27 15:16:50 +02:00
Cameron Gutman
9c76defad0 Add workaround for Galaxy S10 devices crashing during WifiLock acquisition 2022-06-26 13:59:39 -05:00
Cameron Gutman
ffd6fab35c Prevent use of proxies 2022-06-25 14:18:38 -05:00
Cameron Gutman
296f97f7ca Version 10.5 2022-06-23 23:37:19 -05:00
Artem
9cbef34f29 Translated using Weblate (Ukrainian)
Currently translated at 94.5% (207 of 219 strings)

Translation: Moonlight Game Streaming/moonlight-android
Translate-URL: https://hosted.weblate.org/projects/moonlight/moonlight-android/uk/
2022-06-22 22:20:52 +02:00
Cameron Gutman
7a65136d29 Disable predictive back gesture support because it breaks KEYCODE_BACK on InputDevices
Partially reverts b2e605838e
2022-06-18 16:19:02 -05:00
Cameron Gutman
acaebea846 Merge remote-tracking branch 'origin/weblate' 2022-06-18 15:02:06 -05:00
Cameron Gutman
ce850ac12f Fix crash on Xiaomi MiPad running newer custom ROMs
AVC Decoder: OMX.Nvidia.h264.decode
HEVC Decoder: OMX.Nvidia.h265.decode
AVC supported width range: [32, 3840]
AVC achievable FPS range: [146.0, 149.0]
HEVC supported width range: [32, 528]
HEVC achievable FPS range: UNSUPPORTED!
2022-06-18 15:00:10 -05:00
Cameron Gutman
a93422d3ed Handle failure to bind com.nvidia.blakepairing more robustly 2022-06-18 14:31:38 -05:00
Cameron Gutman
b2e605838e Opt in for new predictive back gesture support in Android 13 2022-06-18 14:26:13 -05:00
Cameron Gutman
2e14002442 Switch to the new native per-app language preference APIs on Android 13 2022-06-18 14:19:19 -05:00
Cameron Gutman
c743949df5 Don't crash if no performance data was provided for the codec using the M API 2022-06-18 10:37:16 -05:00
Cameron Gutman
f207a3f6d1 Use areSizeAndRateSupported() as a last resort if no performance data is available 2022-06-18 10:35:12 -05:00
Cameron Gutman
d6211605a1 Fix crash on shortcut launch if PC has no known MAC address 2022-06-18 10:23:06 -05:00
Cameron Gutman
b16676b54a Version 10.4 2022-06-18 10:18:37 -05:00
Licaon_Kter
dc9bfe5189 Fastlane mention Sunshine (#1086)
* Mention Sunshine

* Update full DE

* Short EN

* Full EN

* remove for clarity

* clarify here too
2022-06-18 09:57:37 -05:00
metezd
80620ed4c6 Translated using Weblate (Turkish)
Currently translated at 69.4% (152 of 219 strings)

Translation: Moonlight Game Streaming/moonlight-android
Translate-URL: https://hosted.weblate.org/projects/moonlight/moonlight-android/tr/
2022-06-15 14:17:48 +02:00
Wen-haur Chiu
76bd0ab696 Translated using Weblate (Chinese (Traditional))
Currently translated at 100.0% (219 of 219 strings)

Translation: Moonlight Game Streaming/moonlight-android
Translate-URL: https://hosted.weblate.org/projects/moonlight/moonlight-android/zh_Hant/
2022-06-15 14:17:47 +02:00
ㅤAbsurdUsername
e0914df58a Translated using Weblate (Italian)
Currently translated at 100.0% (219 of 219 strings)

Translation: Moonlight Game Streaming/moonlight-android
Translate-URL: https://hosted.weblate.org/projects/moonlight/moonlight-android/it/
2022-06-15 14:17:46 +02:00
Cameron Gutman
20039a422e Merge remote-tracking branch 'origin/weblate' 2022-06-13 21:44:02 -05:00
Cameron Gutman
22b9c9ca68 Use H.264 on Sabrina if possible for lowest latency 2022-06-13 21:40:41 -05:00
Eric
0c546e35ec Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (219 of 219 strings)

Translation: Moonlight Game Streaming/moonlight-android
Translate-URL: https://hosted.weblate.org/projects/moonlight/moonlight-android/zh_Hans/
2022-06-14 04:19:39 +02:00
Cameron Gutman
b70370ac09 Merge remote-tracking branch 'origin/weblate' 2022-06-13 21:14:40 -05:00
Cameron Gutman
aa10bb7dc5 Block HDR use on the known broken Shield TV firmware build 2022-06-13 20:23:18 -05:00
Cameron Gutman
c6100a9be1 Catch potential older NVIDIA devices that use partial HEVC acceleration 2022-06-13 19:25:29 -05:00
Cameron Gutman
529a2f7bf8 Prevent PiP entry while the USB permission dialog is open 2022-06-13 19:23:03 -05:00
ㅤAbsurdUsername
9ec7e916c5 Translated using Weblate (Italian)
Currently translated at 100.0% (221 of 221 strings)

Translation: Moonlight Game Streaming/moonlight-android
Translate-URL: https://hosted.weblate.org/projects/moonlight/moonlight-android/it/
2022-06-13 20:16:29 +02:00
Cameron Gutman
982b36cf98 Adjust app details text for new HDR behavior 2022-06-09 20:25:37 -05:00
Cameron Gutman
a73eab5e92 Handle stale controller token mappings upon device removal 2022-06-09 19:43:46 -05:00
Cameron Gutman
a8479ccb5f Implement support for rumble for Shield controllers on Shield devices 2022-06-09 18:51:23 -05:00
Cameron Gutman
f55e4e0e01 Don't dock expanded PiP overlays when browsing PCs and apps 2022-06-09 00:05:19 -05:00
Cameron Gutman
d08c32ce04 Map external keyboard keycodes to the QWERTY layout that GFE expects 2022-06-08 23:54:57 -05:00
Cameron Gutman
1d599c5e60 Target Android 13 2022-06-08 22:58:39 -05:00
Cameron Gutman
e888ae59e4 Ignore 3 finger tap gesture when cancelled 2022-06-08 22:58:23 -05:00
Cameron Gutman
951d544894 Provide GameState updates to GameManager on Android 13 2022-06-08 22:41:16 -05:00
Cameron Gutman
49898b34e1 Don't export UsbEventReceiver on Android 13 2022-06-08 22:16:58 -05:00
Cameron Gutman
3854a6a42e Add predictive back support to HelpActivity 2022-06-08 21:45:00 -05:00
Cameron Gutman
d4da5bc281 Disallow Game Mode downscaling on Android 12+ 2022-06-08 20:56:27 -05:00
Cameron Gutman
04954f5242 Add handling for MotionEvent.FLAG_CANCELED 2022-06-08 20:35:46 -05:00
Cameron Gutman
9fc5496526 Use VibrationAttributes to bypass interruption policy 2022-06-08 20:26:36 -05:00
Cameron Gutman
e363d24b1c Add PiP title and subtilte on Android 13 2022-06-08 20:04:12 -05:00
Cameron Gutman
801f4027a2 Add preferKeepClear marks on important UI elements 2022-06-08 20:03:23 -05:00
Cameron Gutman
c0dc344f76 Compile with API 33 SDK 2022-06-08 20:01:05 -05:00
TacoTheDank
b5b3d81f00 Clean up flavors by using buildConfigField 2022-06-08 19:44:59 -05:00
TacoTheDank
8dd8dbc1d1 Clean up app build.gradle deprecations 2022-06-08 19:44:59 -05:00
TacoTheDank
8f31aa59a8 Update gradle wrapper 2022-06-08 19:44:59 -05:00
Cameron Gutman
5b581b6c0f Update string for HEVC auto setting 2022-06-06 17:30:18 -05:00
Cameron Gutman
297ac64fde Enable HEVC on all Shield TV devices 2022-06-06 17:29:47 -05:00
Cameron Gutman
d4490f0e17 Fix performance point check for Android M - P 2022-06-06 17:26:59 -05:00
Cameron Gutman
d04e7a3231 Enable HEVC on untested decoders if it's the only way to meet the performance target 2022-06-04 17:37:14 -05:00
Cameron Gutman
5b456aba27 Use a separate HandlerThread for Choreographer callbacks 2022-06-04 17:00:58 -05:00
Cameron Gutman
0c065dcc1f Print vendor parameters on Android 12 2022-06-04 15:42:06 -05:00
Cameron Gutman
531f73329d Quiet down excessive exception logging in debug builds 2022-06-04 15:33:12 -05:00
Cameron Gutman
d6ba72032d Version 10.3 2022-06-03 20:56:48 -05:00
Cameron Gutman
bfdc7a2609 Merge remote-tracking branch 'origin/weblate' 2022-06-03 19:20:29 -05:00
Wen-haur Chiu
031abf03da Translated using Weblate (Chinese (Traditional))
Currently translated at 100.0% (221 of 221 strings)

Translation: Moonlight Game Streaming/moonlight-android
Translate-URL: https://hosted.weblate.org/projects/moonlight/moonlight-android/zh_Hant/
2022-06-04 02:19:58 +02:00
Cameron Gutman
6aac8e6be6 Use amazon.hardware.fire_tv feature to detect Fire TV devices 2022-06-03 19:03:56 -05:00
Wen-haur Chiu
8ff93d21c3 Translated using Weblate (Chinese (Traditional))
Currently translated at 100.0% (221 of 221 strings)

Translation: Moonlight Game Streaming/moonlight-android
Translate-URL: https://hosted.weblate.org/projects/moonlight/moonlight-android/zh_Hant/
2022-06-03 13:17:29 +02:00
weng weng
6df3d0bc44 Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (221 of 221 strings)

Translation: Moonlight Game Streaming/moonlight-android
Translate-URL: https://hosted.weblate.org/projects/moonlight/moonlight-android/zh_Hans/
2022-06-03 13:17:29 +02:00
Cameron Gutman
0b18e8fdb4 Update decoder errata details 2022-06-03 01:39:39 -05:00
Cameron Gutman
19d8ae0f78 Revamp low latency option handling
- Introduce a tiered solution where we try progressively fewer options until one works
- Use vdec-lowlatency for all devices, since we know at least the Fire TV 3 supports it with an Amlogic SoC
- Enable HEVC on Fire TV 3 since vdec-lowlatency avoids the HEVC decoder bug
2022-06-03 01:04:11 -05:00
Cameron Gutman
d7ffb5dddc Refactor decoder creation code to allow retries 2022-06-02 21:17:20 -05:00
Cameron Gutman
2859b73dfe Add Amlogic low latency vendor-defined option 2022-06-02 21:02:43 -05:00
Cameron Gutman
6f9021a5e6 Add magic performance boost for MediaTek devices 2022-06-01 22:06:11 -05:00
Cameron Gutman
3bfeaefdbd Update NDK to r23c 2022-06-01 19:24:03 -05:00
Cameron Gutman
db1eace975 Add support for Android 13 themed app icons 2022-06-01 19:08:17 -05:00
Cameron Gutman
cab0fa176e Version 10.2 2022-05-31 21:05:59 -05:00
Jorys Paulin
2f9ae107a2 Translated using Weblate (French)
Currently translated at 100.0% (221 of 221 strings)

Translation: Moonlight Game Streaming/moonlight-android
Translate-URL: https://hosted.weblate.org/projects/moonlight/moonlight-android/fr/
2022-05-31 10:14:36 +02:00
Cameron Gutman
18c93abcb3 Submit fused IDR frames on decoders that support adaptive playback even if they are blocked from using it 2022-05-29 22:21:15 -05:00
Cameron Gutman
bd64dfb661 Submit codec config data with a timestamp of 0 like MediaCodec does with csd-0 2022-05-29 22:10:49 -05:00
Cameron Gutman
82619063ee Plumb frame type information into the decoder 2022-05-29 21:58:28 -05:00
Cameron Gutman
5dbf18d66e Fix miscounting IDR frames in video stats 2022-05-29 21:10:41 -05:00
Cameron Gutman
6a34ff2728 Rewrite AES pairing functions to avoid Play Store's ECB warning
ECB is safe in this context because it's encrypting one-time messages
using a one-time key. All input data going through encryptAes() is
either random or partially random and passed through a secure hashing
function (SHA-256 on modern GFE versions).

Message authentication is not a concern either, because it is performed
by the pairing process itself via RSA signature verification. Any
ciphertext tampering would cause signature verification to fail later in
the pairing process.
2022-05-29 14:38:56 -05:00
Cameron Gutman
f7c7487756 Merge remote-tracking branch 'origin/weblate' 2022-05-28 18:01:30 -05:00
weng weng
f966cb4ca0 Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (218 of 218 strings)

Translation: Moonlight Game Streaming/moonlight-android
Translate-URL: https://hosted.weblate.org/projects/moonlight/moonlight-android/zh_Hans/
2022-05-29 01:01:12 +02:00
Jorys Paulin
549563a3d2 Translated using Weblate (French)
Currently translated at 100.0% (218 of 218 strings)

Translation: Moonlight Game Streaming/moonlight-android
Translate-URL: https://hosted.weblate.org/projects/moonlight/moonlight-android/fr/
2022-05-29 01:01:12 +02:00
Cameron Gutman
c5f2a3f8fe Tweak remote desktop mouse mode string 2022-05-28 18:00:42 -05:00
Cameron Gutman
81a3bbd5e8 Implement remote desktop optimized mouse mode 2022-05-28 16:38:22 -05:00
Cameron Gutman
1509a2a799 Fix default deadzone setting 2022-05-28 16:16:46 -05:00
Cameron Gutman
fc547b734f Fix crashes caused by calling NvHTTP with a null address 2022-05-28 15:54:21 -05:00
Cameron Gutman
b3700b5a19 Plumb LiSendMouseMoveAsMousePositionEvent() into JNI 2022-05-28 15:21:58 -05:00
Cameron Gutman
2b29682095 Update AGP 2022-05-28 15:13:10 -05:00
Cameron Gutman
286094ee33 Add dead zone configuration option
Fixes #1075
2022-05-28 15:12:58 -05:00
ToldYouThat
c7a061d24e Translated using Weblate (Turkish)
Currently translated at 25.6% (56 of 218 strings)

Translation: Moonlight Game Streaming/moonlight-android
Translate-URL: https://hosted.weblate.org/projects/moonlight/moonlight-android/tr/
2022-05-25 14:14:38 +02:00
Cameron Gutman
4bdc2e0aba Add F-Droid metadata for 274 2022-05-22 18:02:21 -05:00
Cameron Gutman
e69061082b Version 10.1.1 2022-05-22 17:16:37 -05:00
Cameron Gutman
1da2ec3cb1 Merge remote-tracking branch 'origin/weblate' 2022-05-22 17:15:44 -05:00
Cameron Gutman
8ffc3b80b2 Rework use of URLs in NvHTTP
- Fixes parsing inconsistencies between URI and HttpUrl
- Fixes a couple of serverinfo requests sent without uniqueid and UUID
- Avoids PairingManager having to look into NvHTTP internals
2022-05-22 16:47:45 -05:00
Cameron Gutman
08f8b6cb8e Keep the SpinnerDialog visible while the connectivity test runs 2022-05-22 15:36:38 -05:00
Cameron Gutman
fb09c9692c Fix handling of InterruptedExceptions 2022-05-22 15:31:06 -05:00
Cameron Gutman
4901b0c78f Stop parallel polling threads when we find a working address 2022-05-22 14:56:28 -05:00
Cameron Gutman
0a2117241f Wrap Choreographer calls to releaseOutputBuffer() in try/catch 2022-05-22 14:32:03 -05:00
Wen-haur Chiu
f352cfd15b Translated using Weblate (Chinese (Traditional))
Currently translated at 100.0% (218 of 218 strings)

Translation: Moonlight Game Streaming/moonlight-android
Translate-URL: https://hosted.weblate.org/projects/moonlight/moonlight-android/zh_Hant/
2022-05-21 14:16:28 +02:00
Cameron Gutman
ac7c5c1064 Improve handling of required XML tags 2022-05-20 17:15:26 -05:00
Cameron Gutman
077cb2103d Version 10.1 2022-05-18 22:44:46 -05:00
Cameron Gutman
cdeda011a4 Temporarily disable in-app links until they are translated 2022-05-18 22:40:47 -05:00
Cameron Gutman
894c146988 Fix JAVA_HOME path on VS2022 CI image 2022-05-18 00:57:05 -05:00
Cameron Gutman
61cc9e151f Use newer AppVeyor machine image 2022-05-18 00:38:47 -05:00
Cameron Gutman
cfe4c9ff21 Target Android 12L 2022-05-17 17:16:28 -05:00
Cameron Gutman
d4bd29b320 Properly deal with battery saver mode in capped FPS mode 2022-05-17 00:14:55 -05:00
Cameron Gutman
7f2f2056c3 Add in-app privacy policy link to comply with Google Play policies
Also added Setup Guide and Troubleshooting Guide links too.
2022-05-15 15:56:19 -05:00
Cameron Gutman
4dd3b2cfb7 Tweak capped FPS option text 2022-05-14 23:33:43 -05:00
Cameron Gutman
2e62ad0f00 Merge remote-tracking branch 'origin/weblate' 2022-05-14 23:31:51 -05:00
Cameron Gutman
41ef292b82 Fix frame rate cap not taking effect with the unlock FPS option enabled 2022-05-14 21:19:51 -05:00
Cameron Gutman
aa60671c88 Return the selected refresh rate now that the capped FPS mode is not default 2022-05-14 20:53:42 -05:00
Cameron Gutman
f1ccba39e8 Don't raise refresh rate above stream FPS except in min latency mode 2022-05-14 20:53:07 -05:00
Cameron Gutman
226e580a30 Prevent microstutter in balanced mode when streaming at 60 FPS on a 120 Hz display 2022-05-14 20:08:41 -05:00
Cameron Gutman
6f8e719200 Update AGP 2022-05-14 18:25:48 -05:00
Cameron Gutman
c127af1e05 Rewrite polling logic to avoid needing to poll using a separate socket first 2022-05-14 18:14:37 -05:00
Wen-haur Chiu
648904cc69 Translated using Weblate (Chinese (Traditional))
Currently translated at 100.0% (211 of 211 strings)

Translation: Moonlight Game Streaming/moonlight-android
Translate-URL: https://hosted.weblate.org/projects/moonlight/moonlight-android/zh_Hant/
2022-05-11 19:13:59 +02:00
Cameron Gutman
dc85ddb3f9 Reintroduce option of using old frame pacing algorithm using capped FPS 2022-05-08 15:20:08 -05:00
Cameron Gutman
23a7d8555f Avoid activity restarts in StreamSettings and AddComputerManually
We would ideally save and restore state, but this is fine for these specific
transient user activities.

Fixes #1052
Fixes #1055
2022-05-08 14:55:47 -05:00
Cameron Gutman
bc9e250d34 Merge remote-tracking branch 'origin/weblate' 2022-05-08 14:40:07 -05:00
Cameron Gutman
2203186527 Remove extra ViewGroup between OSC and StreamView
This allows touch events to be properly split
2022-05-08 14:39:32 -05:00
DankXylese
53d3d9ecb8 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (210 of 210 strings)

Translation: Moonlight Game Streaming/moonlight-android
Translate-URL: https://hosted.weblate.org/projects/moonlight/moonlight-android/uk/
2022-04-28 21:13:07 +02:00
Cameron Gutman
de549f67a1 Update README 2022-04-05 19:51:44 -05:00
Jorys Paulin
755c41481a Translated using Weblate (French)
Currently translated at 100.0% (210 of 210 strings)

Translation: Moonlight Game Streaming/moonlight-android
Translate-URL: https://hosted.weblate.org/projects/moonlight/moonlight-android/fr/
2022-04-04 10:12:11 +02:00
Dominik Chrástecký
aebc2126bc Added translation using Weblate (Czech) 2022-04-03 19:50:26 +02:00
Wen-haur Chiu
f43547fb31 Translated using Weblate (Chinese (Traditional))
Currently translated at 100.0% (210 of 210 strings)

Translation: Moonlight Game Streaming/moonlight-android
Translate-URL: https://hosted.weblate.org/projects/moonlight/moonlight-android/zh_Hant/
2022-03-25 18:11:43 +01:00
CorteX
398e4df7cf Translated using Weblate (Chinese (Simplified))
Currently translated at 98.0% (206 of 210 strings)

Translation: Moonlight Game Streaming/moonlight-android
Translate-URL: https://hosted.weblate.org/projects/moonlight/moonlight-android/zh_Hans/
2022-03-23 06:58:35 +01:00
reloxx13
ff68efc3f5 Translated using Weblate (German)
Currently translated at 100.0% (210 of 210 strings)

Translation: Moonlight Game Streaming/moonlight-android
Translate-URL: https://hosted.weblate.org/projects/moonlight/moonlight-android/de/
2022-03-21 09:59:13 +01:00
Caio Gabriel
8ba2f51bda Translated using Weblate (Portuguese)
Currently translated at 9.0% (19 of 210 strings)

Translation: Moonlight Game Streaming/moonlight-android
Translate-URL: https://hosted.weblate.org/projects/moonlight/moonlight-android/pt/
2022-03-19 22:58:22 +01:00
Caio Gabriel
87b79b278b Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (210 of 210 strings)

Translation: Moonlight Game Streaming/moonlight-android
Translate-URL: https://hosted.weblate.org/projects/moonlight/moonlight-android/pt_BR/
2022-03-19 22:58:21 +01:00
Caio Gabriel
121e3ea9be Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (210 of 210 strings)

Translation: Moonlight Game Streaming/moonlight-android
Translate-URL: https://hosted.weblate.org/projects/moonlight/moonlight-android/pt_BR/
2022-03-18 16:45:57 +01:00
Caio Gabriel
ec6ed79ee1 Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (210 of 210 strings)

Translation: Moonlight Game Streaming/moonlight-android
Translate-URL: https://hosted.weblate.org/projects/moonlight/moonlight-android/pt_BR/
2022-03-18 03:47:44 +01:00
Caio Gabriel
ca125826a7 Translated using Weblate (Portuguese)
Currently translated at 8.5% (18 of 210 strings)

Translation: Moonlight Game Streaming/moonlight-android
Translate-URL: https://hosted.weblate.org/projects/moonlight/moonlight-android/pt/
2022-03-17 22:58:19 +01:00
Cameron Gutman
dd0aecf108 Update BouncyCastle 2022-03-15 22:16:41 -05:00
Wen-haur Chiu
ef5cb2f0cd Translated using Weblate (Chinese (Traditional))
Currently translated at 100.0% (210 of 210 strings)

Translation: Moonlight Game Streaming/moonlight-android
Translate-URL: https://hosted.weblate.org/projects/moonlight/moonlight-android/zh_Hant/
2022-03-09 15:58:49 +01:00
Cameron Gutman
e5a7bb40e9 Version 10.0 2022-03-08 19:29:48 -06:00
Cameron Gutman
bfdda48fee Merge remote-tracking branch 'origin/weblate' 2022-03-05 17:22:58 -06:00
Cameron Gutman
ebea1bb5c1 Update AGP 2022-03-05 17:21:41 -06:00
bruh
14bc1552fc Translated using Weblate (Vietnamese)
Currently translated at 100.0% (210 of 210 strings)

Translation: Moonlight Game Streaming/moonlight-android
Translate-URL: https://hosted.weblate.org/projects/moonlight/moonlight-android/vi/
2022-03-03 11:56:18 +01:00
Emanuele Conti
a5b80d3944 Translated using Weblate (Italian)
Currently translated at 100.0% (210 of 210 strings)

Translation: Moonlight Game Streaming/moonlight-android
Translate-URL: https://hosted.weblate.org/projects/moonlight/moonlight-android/it/
2022-03-03 11:56:18 +01:00
Emanuele Conti
75d0eedc2b Translated using Weblate (Italian)
Currently translated at 90.9% (191 of 210 strings)

Translation: Moonlight Game Streaming/moonlight-android
Translate-URL: https://hosted.weblate.org/projects/moonlight/moonlight-android/it/
2022-02-23 16:58:16 +01:00
Benjamín Bustos
29ac7028fa Translated using Weblate (Spanish)
Currently translated at 56.1% (118 of 210 strings)

Translation: Moonlight Game Streaming/moonlight-android
Translate-URL: https://hosted.weblate.org/projects/moonlight/moonlight-android/es/
2022-02-23 16:58:15 +01:00
Cameron Gutman
8a63b61495 Avoid touchscreens when looking for pointer capture capable devices 2022-02-18 17:07:23 -06:00
Cameron Gutman
eb9e6443e2 Display frame pacing mode in crash logs 2022-02-18 17:00:21 -06:00
Cameron Gutman
362c466a16 Reintroduce never drop frames option 2022-02-18 16:04:49 -06:00
Cameron Gutman
5dac42646b Merge remote-tracking branch 'origin/weblate' 2022-02-17 23:48:52 -06:00
Cameron Gutman
c25faf6426 Replace frame pacing hack with Choreographer-based rendering
This mimics the frame pacing logic now present in the iOS client.
2022-02-17 23:48:02 -06:00
Wen-haur Chiu
81df1245b4 Translated using Weblate (Chinese (Traditional))
Currently translated at 100.0% (205 of 205 strings)

Translation: Moonlight Game Streaming/moonlight-android
Translate-URL: https://hosted.weblate.org/projects/moonlight/moonlight-android/zh_Hant/
2022-02-16 14:57:48 +01:00
Emanuele Conti
2bf4d92185 Translated using Weblate (Italian)
Currently translated at 72.6% (149 of 205 strings)

Translation: Moonlight Game Streaming/moonlight-android
Translate-URL: https://hosted.weblate.org/projects/moonlight/moonlight-android/it/
2022-02-16 14:57:48 +01:00
Wen-haur Chiu
ae6073fe80 Translated using Weblate (Chinese (Traditional))
Currently translated at 100.0% (205 of 205 strings)

Translation: Moonlight Game Streaming/moonlight-android
Translate-URL: https://hosted.weblate.org/projects/moonlight/moonlight-android/zh_Hant/
2022-02-14 12:55:53 +01:00
Wen-haur Chiu
d0463da2a1 Translated using Weblate (Chinese (Traditional))
Currently translated at 100.0% (205 of 205 strings)

Translation: Moonlight Game Streaming/moonlight-android
Translate-URL: https://hosted.weblate.org/projects/moonlight/moonlight-android/zh_Hant/
2022-02-13 05:52:36 +01:00
Cameron Gutman
c0f8001627 Ignore relative mouse inputs from non-mouse tools
Apparently this can happen for the SPen on Android 12
2022-02-12 19:50:59 -06:00
Cameron Gutman
f39bf61b04 Try to wake the host PC when connecting via the ShortcutTrampoline
Fixes #1024
2022-02-12 18:35:12 -06:00
Cameron Gutman
9c8237dab0 Add Greek language option 2022-02-12 17:52:31 -06:00
Cameron Gutman
b88251fa79 Fix translation-related Lint warnings/errors 2022-02-12 17:44:09 -06:00
Cameron Gutman
208855917e Move existing translated arrays.xml strings to strings.xml 2022-02-12 17:39:00 -06:00
Cameron Gutman
34bdf450e9 Merge remote-tracking branch 'origin/weblate' 2022-02-12 17:21:49 -06:00
Cameron Gutman
998fa1f4e9 Move translatable array strings into strings.xml for Weblate support 2022-02-12 17:20:56 -06:00
人工知能
5c80f7d58c Update arrays.xml (#1042)
fix translations
2022-02-12 14:37:54 -06:00
Cameron Gutman
7552181e24 Plumb setHdrMode callback into MediaCodecDecoderRenderer 2022-02-12 14:31:25 -06:00
Cameron Gutman
4b2e26050e Only enable pointer capture if a compatible input device is connected 2022-02-12 14:21:19 -06:00
Cameron Gutman
530b48de71 Move recapture on focus gain logic to InputCaptureProvider 2022-02-12 13:58:55 -06:00
Emanuele Conti
f4721901f8 Translated using Weblate (Italian)
Currently translated at 76.1% (144 of 189 strings)

Translation: Moonlight Game Streaming/moonlight-android
Translate-URL: https://hosted.weblate.org/projects/moonlight/moonlight-android/it/
2022-02-08 18:54:38 +01:00
Cameron Gutman
8b692269c1 Remove per-app HDR support check
It doesn't seem to make a difference anymore whether it's supported or not.
GFE seems happy to enter HDR mode anyway.
2022-02-07 20:23:11 -06:00
Cameron Gutman
079eca7b4d Update AGP and Gradle 2022-02-06 22:18:24 -06:00
Wh1t3st4r
fee40cdbe2 Translated using Weblate (Portuguese)
Currently translated at 7.9% (15 of 189 strings)

Translation: Moonlight Game Streaming/moonlight-android
Translate-URL: https://hosted.weblate.org/projects/moonlight/moonlight-android/pt/
2022-02-01 17:52:56 +01:00
Wh1t3st4r
66920bb4cb Added translation using Weblate (Portuguese) 2022-01-31 17:10:49 +01:00
Wh1t3st4r
fdbf810aa2 Added translation using Weblate (Portuguese (Brazil)) 2022-01-31 16:08:38 +01:00
GeraltOfTrivia
08bfc1de4a Translated using Weblate (Greek)
Currently translated at 98.9% (187 of 189 strings)

Translation: Moonlight Game Streaming/moonlight-android
Translate-URL: https://hosted.weblate.org/projects/moonlight/moonlight-android/el/
2022-01-25 18:56:12 +01:00
GeraltOfTrivia
76149328fe Added translation using Weblate (Greek) 2022-01-24 17:51:06 +01:00
Wout Rombouts
285f33f3f1 Translated using Weblate (Dutch)
Currently translated at 100.0% (189 of 189 strings)

Translation: Moonlight Game Streaming/moonlight-android
Translate-URL: https://hosted.weblate.org/projects/moonlight/moonlight-android/nl/
2022-01-19 12:55:54 +01:00
Wen-haur Chiu
b17c1b7588 Translated using Weblate (Chinese (Traditional))
Currently translated at 100.0% (189 of 189 strings)

Translation: Moonlight Game Streaming/moonlight-android
Translate-URL: https://hosted.weblate.org/projects/moonlight/moonlight-android/zh_Hant/
2022-01-13 16:54:04 +01:00
Wen-haur Chiu
5b25c90db8 Translated using Weblate (Chinese (Traditional))
Currently translated at 100.0% (189 of 189 strings)

Translation: Moonlight Game Streaming/moonlight-android
Translate-URL: https://hosted.weblate.org/projects/moonlight/moonlight-android/zh_Hant/
2021-12-28 10:52:10 +01:00
Wen-haur Chiu
931a0a5168 Translated using Weblate (Chinese (Traditional))
Currently translated at 100.0% (189 of 189 strings)

Translation: Moonlight Game Streaming/moonlight-android
Translate-URL: https://hosted.weblate.org/projects/moonlight/moonlight-android/zh_Hant/
2021-12-27 10:16:59 +01:00
Cameron Gutman
f6a46438bd Merge remote-tracking branch 'origin/weblate' 2021-12-15 20:56:10 -06:00
Cameron Gutman
4a60ec1755 Fix excessive high-res scroll speed on newer GFE versions 2021-12-14 22:02:12 -06:00
Cameron Gutman
ec222413dd Update NDK and AGP 2021-12-14 21:48:44 -06:00
Wen-haur Chiu
5a28239813 Translated using Weblate (Chinese (Traditional))
Currently translated at 100.0% (189 of 189 strings)

Translation: Moonlight Game Streaming/moonlight-android
Translate-URL: https://hosted.weblate.org/projects/moonlight/moonlight-android/zh_Hant/
2021-12-13 09:53:46 +01:00
Cameron Gutman
da45cba2ff Send fractional scroll events properly 2021-12-08 22:11:44 -06:00
Cameron Gutman
54bc34496a Merge remote-tracking branch 'origin/weblate' 2021-10-06 21:18:00 -05:00
Zero O
294910ac84 Translated using Weblate (Chinese (Traditional))
Currently translated at 100.0% (189 of 189 strings)

Translation: Moonlight Game Streaming/moonlight-android
Translate-URL: https://hosted.weblate.org/projects/moonlight/moonlight-android/zh_Hant/
2021-09-14 03:39:29 +02:00
Mert
71d2c6a5d5 Translated using Weblate (Turkish)
Currently translated at 21.1% (40 of 189 strings)

Translation: Moonlight Game Streaming/moonlight-android
Translate-URL: https://hosted.weblate.org/projects/moonlight/moonlight-android/tr/
2021-08-24 21:34:36 +02:00
Cameron Gutman
79bf17fe24 Add fastlane metadata for v9.10.1 2021-08-21 15:25:45 -05:00
Cameron Gutman
31f66031bc Version 9.10.1 2021-08-21 15:22:05 -05:00
Cameron Gutman
d3f2284791 Update NDK to r23 2021-08-18 00:33:57 -05:00
Cameron Gutman
ec647608c4 Allow state loss when committing SettingsFragment 2021-08-18 00:24:30 -05:00
Cameron Gutman
597582ddd8 Add workaround for NPE in getNetworkInterfaces() 2021-08-18 00:04:34 -05:00
Cameron Gutman
c6d9889182 Fix lint results path for Gradle 7.0 2021-08-10 02:12:03 -05:00
Cameron Gutman
7c58234174 Use JDK 11 for Gradle 7.0 2021-08-10 02:02:24 -05:00
Cameron Gutman
ae9282b0af Plumb UTF-8 text support through to NvConnection 2021-08-10 00:14:13 -05:00
Cameron Gutman
310ba646fc Update Gradle 2021-08-09 23:24:44 -05:00
LiuAnnan
d479908939 Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (189 of 189 strings)

Translation: Moonlight Game Streaming/moonlight-android
Translate-URL: https://hosted.weblate.org/projects/moonlight/moonlight-android/zh_Hans/
2021-07-30 13:33:24 +02:00
Nikita Epifanov
5cd5d68d22 Translated using Weblate (Russian)
Currently translated at 100.0% (189 of 189 strings)

Translation: Moonlight Game Streaming/moonlight-android
Translate-URL: https://hosted.weblate.org/projects/moonlight/moonlight-android/ru/
2021-07-23 10:32:56 +02:00
Cameron Gutman
3e0bf25acb Update moonlight-common-c 2021-07-17 14:16:40 -05:00
Cameron Gutman
f3d277c94a Update Maven dependencies 2021-07-17 14:01:59 -05:00
Cameron Gutman
04545ecbb0 Avoid tons of redundant calls to InputEvent.getSource() 2021-07-17 14:01:12 -05:00
Cameron Gutman
5350651d6f Fix crash when using USB driver on Android 12 2021-07-17 13:59:11 -05:00
Cameron Gutman
f2e2e28419 Fix NPE if we receive a SOURCE_CLASS_POSITION event with no associated device 2021-07-17 13:15:57 -05:00
Cameron Gutman
b9031785ac Fix crash if maxShortcutCountPerActivity is zero 2021-07-17 13:08:25 -05:00
Cameron Gutman
91a72474a1 Version 9.10 r2 2021-07-16 21:01:16 -05:00
Cameron Gutman
b6e7c425c6 Fix input from SOURCE_TRACKPAD devices 2021-07-16 20:44:01 -05:00
Cameron Gutman
834ace4566 Add SoC details and performance class to exception data 2021-07-16 20:00:03 -05:00
Cameron Gutman
54af70005d Fix spurious gamepad removal when entering PiP with PS4 controller on Android 12
The relative mouse axes AXIS_RELATIVE_X/Y are added/removed when gaining/losing input focus
2021-07-16 19:51:14 -05:00
Cameron Gutman
f2bf168925 Fix possible rumble crash if only the lower motor byte is non-zero 2021-07-16 19:25:10 -05:00
Cameron Gutman
27ffbd8dec Version 9.10 2021-07-16 19:23:37 -05:00
Cameron Gutman
eaa82592fe Merge remote-tracking branch 'origin/weblate' 2021-07-15 19:59:07 -05:00
Cameron Gutman
73784585a8 Fix new Android 12 rumble code based on real hardware testing
Independent rumble motor controller tested working on:
- DualShock 4 (USB and BT)
- DualShock 3 (USB)
- Xbox Series X (USB)
2021-07-15 19:51:08 -05:00
Cameron Gutman
262d562dd9 Implement enhanced rumble support for Android 12 devices
This allows independent control of large and small motors which
was not possible with the old single Vibrator API.

Currently untested on real hardware.
2021-07-14 20:18:35 -05:00
Cameron Gutman
ab4f904dc9 Target Android 12 2021-07-14 20:04:46 -05:00
Cameron Gutman
fc4fdd5ee2 Implement seamless PiP entry on Android 12 2021-07-14 20:00:53 -05:00
Cameron Gutman
41c5b62b1a Update AGP to 4.2.2 2021-07-14 19:58:12 -05:00
Cameron Gutman
239cb0435c Add new backup rules for Android 12 2021-07-14 19:58:01 -05:00
bruh
c6ccc7a6e2 Translated using Weblate (Vietnamese)
Currently translated at 100.0% (189 of 189 strings)

Translation: Moonlight Game Streaming/moonlight-android
Translate-URL: https://hosted.weblate.org/projects/moonlight/moonlight-android/vi/
2021-07-05 18:33:43 +02:00
Cameron Gutman
6cedb9019c Pass RTSP session URL to moonlight-common-c for dynamic ports 2021-07-02 17:41:07 -05:00
Furkan
8bc64f0438 Translated using Weblate (Turkish)
Currently translated at 19.0% (36 of 189 strings)

Translation: Moonlight Game Streaming/moonlight-android
Translate-URL: https://hosted.weblate.org/projects/moonlight/moonlight-android/tr/
2021-07-02 18:33:32 +02:00
WALKTHROUGH RAYMAND LEGENDS
89e6e39e58 Translated using Weblate (Hungarian)
Currently translated at 100.0% (189 of 189 strings)

Translation: Moonlight Game Streaming/moonlight-android
Translate-URL: https://hosted.weblate.org/projects/moonlight/moonlight-android/hu/
2021-07-02 18:33:31 +02:00
Furkan
645761f677 Added translation using Weblate (Turkish) 2021-07-01 17:53:08 +02:00
DankXylese
0fc60f7855 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (189 of 189 strings)

Translation: Moonlight Game Streaming/moonlight-android
Translate-URL: https://hosted.weblate.org/projects/moonlight/moonlight-android/uk/
2021-06-30 13:33:24 +02:00
LUTEN VR
ce38460d87 Translated using Weblate (Korean)
Currently translated at 100.0% (189 of 189 strings)

Translation: Moonlight Game Streaming/moonlight-android
Translate-URL: https://hosted.weblate.org/projects/moonlight/moonlight-android/ko/
2021-06-28 03:36:01 +02:00
Jorys Paulin
de8e759d3a Translated using Weblate (French)
Currently translated at 100.0% (189 of 189 strings)

Translation: Moonlight Game Streaming/moonlight-android
Translate-URL: https://hosted.weblate.org/projects/moonlight/moonlight-android/fr/
2021-06-28 03:36:00 +02:00
Cameron Gutman
06f6134538 Version 9.9.6 2021-06-25 00:53:28 -05:00
Cameron Gutman
ac352b3a23 Merge remote-tracking branch 'origin/weblate' 2021-06-25 00:26:06 -05:00
Cameron Gutman
9b8e65e552 Add cutout resolution options on Android 9 2021-06-25 00:24:26 -05:00
Cameron Gutman
35999a05f0 Minor code cleanup 2021-06-24 23:50:15 -05:00
Cameron Gutman
86ee30e9b4 Don't process drags for the non-primary finger 2021-06-24 23:19:06 -05:00
Allan Nordhøy
a81c4a1e23 Translated using Weblate (Norwegian Bokmål)
Currently translated at 88.8% (168 of 189 strings)

Translation: Moonlight Game Streaming/moonlight-android
Translate-URL: https://hosted.weblate.org/projects/moonlight/moonlight-android/nb_NO/
2021-06-24 07:32:33 +02:00
Cameron Gutman
394ce458a0 Add additional native resolution options on Android 10+ with display insets included
Fixes #956
Fixes #986
2021-06-22 23:56:45 -05:00
Cameron Gutman
f187e57899 Fix FPS display on stats overlay 2021-06-22 23:43:10 -05:00
Cameron Gutman
a15335872d Update moonlight-common-c to fix audio problems on old GFE and Sunshine versions 2021-06-22 22:12:32 -05:00
Cameron Gutman
beb77b4dab Add Hungarian language option 2021-06-22 21:58:45 -05:00
Cameron Gutman
aa80d8cd0a Change H.265 to HEVC 2021-06-22 21:53:15 -05:00
Cameron Gutman
77d197f14e Merge remote-tracking branch 'origin/weblate' 2021-06-22 21:50:57 -05:00
WALKTHROUGH RAYMAND LEGENDS
f98fbb778c Translated using Weblate (Hungarian)
Currently translated at 100.0% (188 of 188 strings)

Translation: Moonlight Game Streaming/moonlight-android
Translate-URL: https://hosted.weblate.org/projects/moonlight/moonlight-android/hu/
2021-06-15 14:32:25 +02:00
Cameron Gutman
c46a0106f2 Version 9.9.5 2021-06-14 23:34:16 -05:00
WALKTHROUGH RAYMAND LEGENDS
cbf3db0be0 Added translation using Weblate (Hungarian) 2021-06-13 21:33:00 +02:00
Cameron Gutman
21f3710083 Update moonlight-common-c with performance and audio improvements 2021-06-13 10:18:32 -05:00
Cameron Gutman
8ac5768f4f Change H.265 to HEVC to match other clients 2021-06-12 11:00:30 -05:00
Cameron Gutman
2458b9305c Merge remote-tracking branch 'origin/weblate' 2021-06-12 10:54:07 -05:00
Artem
a8909ea2a5 Translated using Weblate (Russian)
Currently translated at 99.4% (187 of 188 strings)

Translation: Moonlight Game Streaming/moonlight-android
Translate-URL: https://hosted.weblate.org/projects/moonlight/moonlight-android/ru/
2021-06-11 22:32:00 +02:00
Cameron Gutman
ac7c35c6c2 Version 9.9.4 2021-06-03 21:51:21 -05:00
Cameron Gutman
e4631b5a85 Update moonlight-common-c with audio FEC support 2021-06-03 21:23:55 -05:00
Cameron Gutman
e1c50b5dc5 Merge remote-tracking branch 'origin/weblate' 2021-06-03 21:20:56 -05:00
Zero O
c6c5a5cd12 Translated using Weblate (Chinese (Traditional))
Currently translated at 100.0% (188 of 188 strings)

Translation: Moonlight Game Streaming/moonlight-android
Translate-URL: https://hosted.weblate.org/projects/moonlight/moonlight-android/zh_Hant/
2021-06-02 05:33:02 +02:00
Zero O
bd4854a607 Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (188 of 188 strings)

Translation: Moonlight Game Streaming/moonlight-android
Translate-URL: https://hosted.weblate.org/projects/moonlight/moonlight-android/zh_Hans/
2021-06-02 05:33:01 +02:00
LUTEN VR
cd0181e6f4 Translated using Weblate (Korean)
Currently translated at 100.0% (188 of 188 strings)

Translation: Moonlight Game Streaming/moonlight-android
Translate-URL: https://hosted.weblate.org/projects/moonlight/moonlight-android/ko/
2021-05-29 13:33:23 +02:00
bruh
287b1d2b4d Translated using Weblate (Vietnamese)
Currently translated at 100.0% (188 of 188 strings)

Translation: Moonlight Game Streaming/moonlight-android
Translate-URL: https://hosted.weblate.org/projects/moonlight/moonlight-android/vi/
2021-05-28 08:32:48 +02:00
LUTEN VR
10c61bb0a7 Translated using Weblate (Korean)
Currently translated at 100.0% (188 of 188 strings)

Translation: Moonlight Game Streaming/moonlight-android
Translate-URL: https://hosted.weblate.org/projects/moonlight/moonlight-android/ko/
2021-05-24 02:42:49 +02:00
Cameron Gutman
92215ac34f Version 9.9.3.1 for Amazon 2021-05-22 14:40:25 -05:00
Cameron Gutman
f64d50d8c8 Hide the help button on Fire TV
The last reviewer complained that the GitHub wiki was not
entirely navigable via the Fire TV remote.
2021-05-22 14:36:25 -05:00
Cameron Gutman
b74e0ce48f Remove receive time from performance overlay
It is superseded by network latency
2021-05-22 14:08:46 -05:00
Cameron Gutman
27cb0029a8 Merge remote-tracking branch 'origin/weblate' 2021-05-22 14:07:37 -05:00
LUTEN VR
ce6f193f06 Translated using Weblate (Korean)
Currently translated at 80.5% (153 of 190 strings)

Translation: Moonlight Game Streaming/moonlight-android
Translate-URL: https://hosted.weblate.org/projects/moonlight/moonlight-android/ko/
2021-05-21 20:33:05 +02:00
Cameron Gutman
a862ffdde4 Version 9.9.3 2021-05-16 20:49:28 -05:00
Cameron Gutman
3f1cd8a118 Use HEVC at 4K on Qualcomm since RFI is temporarily disabled 2021-05-16 20:39:58 -05:00
Cameron Gutman
bb4b5838e3 Enable HEVC on Realtek SoCs 2021-05-16 20:39:00 -05:00
Cameron Gutman
ea98d64184 Consolidate performance overlay lines to reduce wasted space 2021-05-16 20:20:36 -05:00
Cameron Gutman
98f3c56da5 Remove duplicate Japanese language entry 2021-05-16 20:15:45 -05:00
Cameron Gutman
20b7619380 Update moonlight-common-c to avoid excessive ENet retransmissions when RTT variance is 0 2021-05-16 15:41:44 -05:00
Cameron Gutman
7b1c3f05c7 Update moonlight-common-c with with more accurate RTTs and minRequiredFecPackets 2021-05-16 14:52:23 -05:00
Cameron Gutman
9166998442 Fix casts of RTT info 2021-05-15 17:07:08 -05:00
Cameron Gutman
e1f6b577bf Switch to Maven Central repositories 2021-05-15 16:56:58 -05:00
Cameron Gutman
ba0d08b2a6 Update AGP 2021-05-15 16:56:32 -05:00
Cameron Gutman
e79c12a038 Add network latency to performance overlay 2021-05-15 16:56:19 -05:00
Cameron Gutman
2ca5182a28 Convert the big perf text block into strings for each line 2021-05-15 16:45:38 -05:00
Cameron Gutman
205e627209 Integrate Japanese and Vietnamese translations 2021-05-13 00:36:30 -05:00
Cameron Gutman
425d4f3f63 Merge remote-tracking branch 'origin/weblate' 2021-05-13 00:27:22 -05:00
bruh
d69843e122 Translated using Weblate (Vietnamese)
Currently translated at 100.0% (182 of 182 strings)

Translation: Moonlight Game Streaming/moonlight-android
Translate-URL: https://hosted.weblate.org/projects/moonlight/moonlight-android/vi/
2021-05-13 07:05:19 +02:00
bruh
d2586d3b59 Added translation using Weblate (Vietnamese) 2021-05-11 16:12:48 +02:00
Cameron Gutman
edab84c89b Bump version again 2021-05-06 22:26:40 -05:00
Cameron Gutman
dd08754f1f Actually update moonlight-common-c for 4K RFI workaround 2021-05-06 22:26:07 -05:00
Cameron Gutman
2cdfe85091 Version 9.9.2 2021-05-06 22:21:36 -05:00
Cameron Gutman
a11acef36f Update moonlight-common-c with 4K RFI and audio latency fix 2021-05-06 22:18:19 -05:00
Cameron Gutman
1e34dbf616 Don't add native resolutions on TVs 2021-05-06 20:45:02 -05:00
Cameron Gutman
b3d4763ef6 Fix native screen resolution on devices running Lollipop and earlier
Fixes #967
2021-05-06 20:31:06 -05:00
Cameron Gutman
fe630e9383 Merge remote-tracking branch 'origin/weblate' 2021-05-06 17:27:10 -05:00
Cameron Gutman
826a20785f Create debug symbols for our native libraries for Google Play 2021-05-05 20:29:40 -05:00
Cameron Gutman
75932d7621 Update Gradle 2021-05-05 20:29:15 -05:00
Zero O
62d095af4f Translated using Weblate (Chinese (Traditional))
Currently translated at 100.0% (182 of 182 strings)

Translation: Moonlight Game Streaming/moonlight-android
Translate-URL: https://hosted.weblate.org/projects/moonlight/moonlight-android/zh_Hant/
2021-05-01 04:32:16 +02:00
Zero O
1594735aa0 Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (182 of 182 strings)

Translation: Moonlight Game Streaming/moonlight-android
Translate-URL: https://hosted.weblate.org/projects/moonlight/moonlight-android/zh_Hans/
2021-05-01 04:32:16 +02:00
Cameron Gutman
cbd0bdf9fc Version 9.9.1 2021-04-29 18:32:08 -05:00
Cameron Gutman
d3e8e8fb9c Update moonlight-common-c with RTSP handshake retry logic 2021-04-29 18:23:41 -05:00
Cameron Gutman
66406c5a48 Version 9.9 2021-04-27 18:20:00 -05:00
Cameron Gutman
753c600dd2 Merge remote-tracking branch 'origin/weblate' 2021-04-27 17:46:37 -05:00
Cameron Gutman
b28b1df348 Update moonlight-common-c with multi-FEC and audio latency fixes 2021-04-27 17:44:14 -05:00
Cameron Gutman
b94649162e Allow compatibility aliases to match preferred decoders 2021-04-27 17:43:19 -05:00
Cameron Gutman
ee50e19dbd Fix use of Android 11 low latency decoding feature 2021-04-27 17:43:04 -05:00
Cameron Gutman
cc23f8b831 Revert vt-low-latency option
Fixes #973
2021-04-26 19:10:07 -05:00
Cameron Gutman
bac7b68bb1 One more attempt to fix exception parsing 2021-04-26 19:07:54 -05:00
Nikita Epifanov
f9a622c89b Translated using Weblate (Russian)
Currently translated at 100.0% (182 of 182 strings)

Translation: Moonlight Game Streaming/moonlight-android
Translate-URL: https://hosted.weblate.org/projects/moonlight/moonlight-android/ru/
2021-04-26 13:32:09 +02:00
Cameron Gutman
c321dc5e81 Version 9.8.7 2021-04-23 19:48:17 -05:00
Cameron Gutman
72f37c9df4 Enable audio stream encryption 2021-04-23 19:38:24 -05:00
Cameron Gutman
544eac0c8a Attempt to prevent possible error parsing exception string 2021-04-23 19:12:41 -05:00
Cameron Gutman
823593ddae Revert "Avoid Amlogic HEVC decoders until the latency issue is understood"
This reverts commit 3600e704c4.
2021-04-19 23:08:20 -05:00
Cameron Gutman
3600e704c4 Avoid Amlogic HEVC decoders until the latency issue is understood 2021-04-19 22:46:55 -05:00
Cameron Gutman
0c79d756a4 Add more specific problem text to the decoder exceptions 2021-04-19 22:44:17 -05:00
Cameron Gutman
eb531a7a88 Fix OpenSSL build script and rebuild 2021-04-18 21:47:06 -05:00
Cameron Gutman
d6634d30dc Update moonlight-common-c 2021-04-18 19:21:06 -05:00
Cameron Gutman
f87806b1b4 Update to OpenSSL 1.1.1k without no-asm 2021-04-18 18:23:56 -05:00
Cameron Gutman
2a5afeb5ff Don't use HEVC on Fire TV 3 2021-04-18 14:42:52 -05:00
Cameron Gutman
fc5495f1ec Add vendor low latency option for Exynos 2021-04-18 14:17:26 -05:00
Cameron Gutman
699cc361a2 Add additional vendor-specific low latency options for Qualcomm and HiSilicon SoCs 2021-04-18 12:49:25 -05:00
shower
31bf4f10c0 Translated using Weblate (Chinese (Simplified))
Currently translated at 93.9% (171 of 182 strings)

Translation: Moonlight Game Streaming/moonlight-android
Translate-URL: https://hosted.weblate.org/projects/moonlight/moonlight-android/zh_Hans/
2021-04-17 13:27:02 +02:00
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
Nikita Epifanov
44acf19742 Translated using Weblate (Russian)
Currently translated at 97.8% (178 of 182 strings)

Translation: Moonlight Game Streaming/moonlight-android
Translate-URL: https://hosted.weblate.org/projects/moonlight/moonlight-android/ru/
2021-04-08 11:26:58 +02:00
Jorys Paulin
bf20aa253e Translated using Weblate (French)
Currently translated at 100.0% (182 of 182 strings)

Translation: Moonlight Game Streaming/moonlight-android
Translate-URL: https://hosted.weblate.org/projects/moonlight/moonlight-android/fr/
2021-04-05 03:40:29 +02: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
Michael Pöhn
fdd4c0bbe1 german translation 2019-07-17 14:15:58 +02:00
341 changed files with 22840 additions and 7676 deletions

176
.github/ISSUE_TEMPLATE/bug_report.yml vendored Normal file
View File

@@ -0,0 +1,176 @@
name: Bug report
description: Follow the troubleshooting guide before reporting a bug
title: "[Issue]: "
labels: bug
body:
- type: markdown
attributes:
value: |
Thank you for taking the time to fill out this bug form!
**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
- type: textarea
id: describe-bug
attributes:
label: Describe the bug
description: A clear and concise description of what the bug is.
validations:
required: true
- type: textarea
id: steps-reproduce
attributes:
label: Steps to reproduce
description: Any special steps that are required for the bug to appear.
validations:
required: true
- type: textarea
id: affected-games
attributes:
label: Affected games
description: 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.
validations:
required: true
- type: dropdown
id: other-clients
attributes:
label: Other Moonlight clients
description: Does the issue occur when using Moonlight on PC or iOS?
options:
- "PC"
- "iOS"
validations:
required: true
- type: dropdown
id: settings-adjusted
attributes:
label: Moonlight adjusted settings
description: Have any settings been adjusted from defaults?
options:
- "Yes"
- "No"
validations:
required: true
- type: textarea
id: settings-adjusted-settings
attributes:
label: Moonlight adjusted settings (please complete the following information)
description: If the settings have been adjusted, which settings have been changed?
validations:
required: true
- type: dropdown
id: settings-default
attributes:
label: Moonlight default settings
description: Does the problem still occur after reverting settings back to default?
options:
- "Yes"
- "No"
validations:
required: true
- type: dropdown
id: gamepad-connected
attributes:
label: Gamepad-related connection issue
description: Do you have any gamepads connected to your host PC directly?
options:
- "Yes"
- "No"
validations:
required: true
- type: dropdown
id: gamepad-on-screen
attributes:
label: Gamepad-related input issue
description: If gamepad input is not working, does it work if you use Moonlight's on-screen controls?
options:
- "Yes"
- "No"
validations:
required: true
- type: dropdown
id: gamepad-test
attributes:
label: Gamepad-related streaming issue
description: |
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
options:
- "Yes"
- "No"
validations:
required: true
- type: input
id: android
attributes:
label: Android version
description: What is the Android version?
placeholder: e.g. Android 10
validations:
required: true
- type: input
id: device
attributes:
label: Device model
description: What is the device model?
placeholder: e.g. Samsung Galaxy S21
validations:
required: true
- type: input
id: server-os
attributes:
label: Server PC OS version
description: What is the PC OS version?
placeholder: e.g. Windows 10 1809
validations:
required: true
- type: input
id: server-geforce
attributes:
label: Server PC GeForce Experience version
description: What is the GeForce Experience version?
placeholder: e.g. 3.16.0.140
validations:
required: true
- type: input
id: server-driver
attributes:
label: Server PC Nvidia GPU driver version
description: What is the Nvidia GPU driver version?
placeholder: e.g. 417.35
validations:
required: true
- type: input
id: server-antivirus
attributes:
label: Server PC antivirus and firewall software
description: Which antivirus and firewall software are installed on the Server PC?
placeholder: e.g. Windows Defender and Windows Firewall
validations:
required: true
- type: textarea
id: screenshots
attributes:
label: Screenshots
description: If applicable, add screenshots to help explain your problem. If the issue is related to video glitching or poor quality, please include screenshots.
validations:
required: false
- type: textarea
id: logs
attributes:
label: Relevant log output
description: |
Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks.
render: Shell
validations:
required: false
- type: textarea
id: additional
attributes:
label: Additional context
description: Anything else you think may be relevant to the issue or special about your specific setup.
validations:
required: false

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

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

View File

@@ -0,0 +1,37 @@
name: Feature request
description: Suggest an idea for this project
title: "[Feature request]: "
labels: enhancement
body:
- type: markdown
attributes:
value: |
Thank you for taking the time to fill out this feature form!
- type: textarea
id: feature
attributes:
label: Is your feature request related to a problem? Please describe.
description: A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
validations:
required: true
- type: textarea
id: solution
attributes:
label: Describe the solution you'd like
description: A clear and concise description of what you want to happen.
validations:
required: true
- type: textarea
id: alternatives
attributes:
label: Describe alternatives you've considered
description: A clear and concise description of any alternative solutions or features you've considered.
validations:
required: true
- type: textarea
id: screenshots
attributes:
label: Screenshots
description: Add any other context or screenshots about the feature request here.
validations:
required: false

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!

1
.gitignore vendored
View File

@@ -3,6 +3,7 @@
*.ap_
*.aab
output.json
output-metadata.json
out/
# files for the dex VM

View File

@@ -1,15 +0,0 @@
language: android
dist: trusty
git:
depth: 1
android:
components:
- tools
- platform-tools
- build-tools-29.0.1
- android-29
install:
- yes | sdkmanager "ndk-bundle"

View File

@@ -1,56 +1,28 @@
# Moonlight Android
[![Travis CI Status](https://travis-ci.org/moonlight-stream/moonlight-android.svg?branch=master)](https://travis-ci.org/moonlight-stream/moonlight-android)
[![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/)
[Moonlight](https://moonlight-stream.org) is an open source implementation of NVIDIA's GameStream, as used by the NVIDIA Shield.
We reverse engineered the Shield streaming software and created a version that can be run on any Android device.
[Moonlight for Android](https://moonlight-stream.org) is an open source client for NVIDIA GameStream and [Sunshine](https://github.com/LizardByte/Sunshine).
Moonlight will allow you to stream your full collection of games from your Windows PC to your Android device,
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.
Check our [wiki](https://github.com/moonlight-stream/moonlight-docs/wiki) for more detailed information or a troubleshooting guide.
Moonlight also has a [PC client](https://github.com/moonlight-stream/moonlight-qt) and [iOS/tvOS client](https://github.com/moonlight-stream/moonlight-ios).
## Features
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/).
* Streams any of your games from your PC to your Android device
* Full gamepad support for MOGA, Xbox 360, PS3, OUYA, and Shield
* Automatically finds GameStream-compatible PCs on your network
## Installation
* Download and install Moonlight for Android from
[Google Play](https://play.google.com/store/apps/details?id=com.limelight), [F-Droid](https://f-droid.org/packages/com.limelight/), [Amazon App Store](http://www.amazon.com/gp/product/B00JK4MFN2), or directly from the [releases page](https://github.com/moonlight-stream/moonlight-android/releases)
* Download [GeForce Experience](http://www.geforce.com/geforce-experience) and install on your Windows PC
## Requirements
* [GameStream compatible](http://shield.nvidia.com/play-pc-games/) computer with an NVIDIA GeForce GTX 600 series or higher desktop or mobile GPU (GT-series and AMD GPUs not supported)
* Android device running 4.1 (Jelly Bean) or higher
* High-end wireless router (802.11n dual-band recommended)
## Usage
* 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 Moonlight and tap on your PC in the list
* Accept the pairing confirmation on your PC and add the PIN if needed
* Tap your PC again to view the list of apps to stream
* Play games!
## Contribute
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
## 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)
## 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
* Build the APK using Android Studio or gradle
## Authors

View File

@@ -1,23 +1,34 @@
apply plugin: 'com.android.application'
android {
compileSdkVersion 29
ndkVersion "27.0.12077973"
compileSdk 34
namespace 'com.limelight'
defaultConfig {
minSdkVersion 16
targetSdkVersion 29
minSdk 21
targetSdk 34
versionName "8.3"
versionCode = 200
versionName "12.1"
versionCode = 314
// Generate native debug symbols to allow Google Play to symbolicate our native crashes
ndk.debugSymbolLevel = 'FULL'
}
flavorDimensions "root"
flavorDimensions.add("root")
buildFeatures {
buildConfig = true
}
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
maxSdk 25
externalNativeBuild {
ndkBuild {
@@ -27,6 +38,7 @@ android {
applicationId "com.limelight.root"
dimension "root"
buildConfigField "boolean", "ROOT_BUILD", "true"
}
nonRoot {
@@ -38,12 +50,19 @@ android {
applicationId "com.limelight"
dimension "root"
buildConfigField "boolean", "ROOT_BUILD", "false"
}
}
lintOptions {
compileOptions {
encoding "UTF-8"
sourceCompatibility JavaVersion.VERSION_11
targetCompatibility JavaVersion.VERSION_11
}
lint {
disable 'MissingTranslation'
lintConfig file("lint.xml")
lintConfig file('lint.xml')
}
bundle {
@@ -53,7 +72,7 @@ android {
enableSplit = false
}
density {
// FIXME: This should not be neccessary but we get
// FIXME: This should not be necessary but we get
// weird crashes due to missing drawable resources
// when this split is enabled.
enableSplit = false
@@ -63,9 +82,10 @@ android {
buildTypes {
debug {
applicationIdSuffix ".debug"
resValue "string", "app_label", "Moonlight (Debug)"
resValue "string", "app_label_root", "Moonlight (Root Debug)"
minifyEnabled true
useProguard false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
release {
@@ -100,6 +120,8 @@ android {
//
// TL;DR: Leave the following line alone!
applicationIdSuffix ".unofficial"
resValue "string", "app_label", "Moonlight"
resValue "string", "app_label_root", "Moonlight (Root)"
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
@@ -114,10 +136,10 @@ android {
}
dependencies {
implementation 'org.bouncycastle:bcprov-jdk15on:1.62'
implementation 'org.bouncycastle:bcpkix-jdk15on:1.62'
implementation 'org.jcodec:jcodec:0.2.3'
implementation 'com.squareup.okhttp3:okhttp:3.12.3'
implementation 'com.squareup.okio:okio:1.17.4'
implementation 'org.jmdns:jmdns:3.5.5'
implementation 'org.bouncycastle:bcprov-jdk18on:1.77'
implementation 'org.bouncycastle:bcpkix-jdk18on:1.77'
implementation 'org.jcodec:jcodec:0.2.5'
implementation 'com.squareup.okhttp3:okhttp:4.12.0'
implementation 'org.jmdns:jmdns:3.5.9'
implementation 'com.github.cgutman:ShieldControllerExtensions:1.0.1'
}

View File

@@ -1,7 +0,0 @@
<?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

@@ -1,16 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.limelight">
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.VIBRATE" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.CHANGE_WIFI_MULTICAST_STATE" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="com.android.providers.tv.permission.READ_EPG_DATA"/>
<uses-permission android:name="com.android.providers.tv.permission.WRITE_EPG_DATA"/>
<!-- We don't need a MulticastLock on API level 34+ because we use NsdManager for mDNS -->
<uses-permission
android:name="android.permission.CHANGE_WIFI_MULTICAST_STATE"
android:maxSdkVersion="33" />
<uses-feature
android:name="android.hardware.touchscreen"
android:required="false" />
@@ -26,6 +29,12 @@
<uses-feature
android:name="android.software.leanback"
android:required="false" />
<uses-feature
android:name="android.hardware.sensor.accelerometer"
android:required="false" />
<uses-feature
android:name="android.hardware.sensor.gyroscope"
android:required="false" />
<!-- Disable legacy input emulation on ChromeOS -->
<uses-feature
@@ -35,36 +44,57 @@
<application
android:allowBackup="true"
android:fullBackupContent="@xml/backup_rules"
android:dataExtractionRules="@xml/backup_rules_s"
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:localeConfig="@xml/locales_config"
android:enableOnBackInvokedCallback="false"
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" />
<!-- Disable Game Mode downscaling since it can break our UI dialogs and doesn't benefit
performance much for us since we don't use GL/Vulkan for rendering anyway -->
<meta-data
android:name="com.android.graphics.intervention.wm.allowDownscale"
android:value="false"/>
<!-- Game Mode configuration -->
<meta-data
android:name="android.game_mode_config"
android:resource="@xml/game_mode_config" />
<!-- 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:exported="true"
android:resizeableActivity="true"
android:enableOnBackInvokedCallback="true"
android:configChanges="mcc|mnc|touchscreen|keyboard|keyboardHidden|navigation|screenLayout|fontScale|uiMode|orientation|screenSize|smallestScreenSize|layoutDirection">
<meta-data android:name="WindowManagerPreference:FreeformWindowSize" android:value="system-default" />
<meta-data android:name="WindowManagerPreference:FreeformWindowOrientation" android:value="landscape" />
<intent-filter>
<action android:name="android.intent.action.MAIN" />
@@ -80,50 +110,61 @@
android:noHistory="true"
android:exported="true"
android:resizeableActivity="true"
android:enableOnBackInvokedCallback="true"
android:configChanges="mcc|mnc|locale|touchscreen|keyboard|keyboardHidden|navigation|screenLayout|fontScale|uiMode|orientation|screenSize|smallestScreenSize|layoutDirection">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value="com.limelight.PcView" />
<meta-data android:name="WindowManagerPreference:FreeformWindowSize" android:value="system-default" />
<meta-data android:name="WindowManagerPreference:FreeformWindowOrientation" android:value="landscape" />
</activity>
<activity
android:name=".AppView"
android:resizeableActivity="true"
android:enableOnBackInvokedCallback="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" />
<meta-data android:name="WindowManagerPreference:FreeformWindowSize" android:value="system-default" />
<meta-data android:name="WindowManagerPreference:FreeformWindowOrientation" android:value="landscape" />
</activity>
<activity
android:name=".preferences.StreamSettings"
android:resizeableActivity="true"
android:enableOnBackInvokedCallback="true"
android:configChanges="mcc|mnc|touchscreen|keyboard|keyboardHidden|navigation|screenLayout|fontScale|uiMode|orientation|screenSize|smallestScreenSize|layoutDirection"
android:label="Streaming Settings">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value="com.limelight.PcView" />
<meta-data android:name="WindowManagerPreference:FreeformWindowSize" android:value="system-default" />
<meta-data android:name="WindowManagerPreference:FreeformWindowOrientation" android:value="landscape" />
</activity>
<activity
android:name=".preferences.AddComputerManually"
android:resizeableActivity="true"
android:windowSoftInputMode="stateVisible"
android:enableOnBackInvokedCallback="true"
android:configChanges="mcc|mnc|touchscreen|keyboard|keyboardHidden|navigation|screenLayout|fontScale|uiMode|orientation|screenSize|smallestScreenSize|layoutDirection"
android:label="Add Computer Manually">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value="com.limelight.PcView" />
<meta-data android:name="WindowManagerPreference:FreeformWindowSize" android:value="system-default" />
<meta-data android:name="WindowManagerPreference:FreeformWindowOrientation" android:value="landscape" />
</activity>
<!-- This will fall back to sensorLandscape at runtime on Android 4.2 and below -->
<activity
android:name=".Game"
android:configChanges="mcc|mnc|locale|touchscreen|keyboard|keyboardHidden|navigation|screenLayout|fontScale|uiMode|orientation|screenSize|smallestScreenSize|layoutDirection"
android:screenOrientation="userLandscape"
android:noHistory="true"
android:supportsPictureInPicture="true"
android:resizeableActivity="true"
android:enableOnBackInvokedCallback="false"
android:launchMode="singleTask"
android:excludeFromRecents="true"
android:theme="@style/StreamTheme">
android:theme="@style/StreamTheme"
android:preferMinimalPostProcessing="true">
<meta-data android:name="WindowManagerPreference:FreeformWindowSize" android:value="system-default" />
<meta-data android:name="WindowManagerPreference:FreeformWindowOrientation" android:value="landscape" />
<!-- Special metadata for NVIDIA Shield devices to prevent input buffering
and most importantly, opt out of mouse acceleration while streaming -->
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value="com.limelight.AppView" />
android:name="com.nvidia.immediateInput"
android:value="true" />
<meta-data
android:name="com.nvidia.rawCursorInput"
android:value="true" />
</activity>
<service
@@ -139,10 +180,10 @@
<activity
android:name=".HelpActivity"
android:resizeableActivity="true"
android:enableOnBackInvokedCallback="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" />
<meta-data android:name="WindowManagerPreference:FreeformWindowSize" android:value="system-default" />
<meta-data android:name="WindowManagerPreference:FreeformWindowOrientation" android:value="landscape" />
</activity>
</application>

View File

@@ -2,6 +2,7 @@ package com.limelight;
import java.io.IOException;
import java.io.StringReader;
import java.util.HashSet;
import java.util.List;
import com.limelight.computers.ComputerManagerListener;
@@ -26,6 +27,7 @@ import android.app.Service;
import android.content.ComponentName;
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;
@@ -59,17 +61,22 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
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_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() {
@@ -98,13 +105,16 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
try {
appGridAdapter = new AppGridAdapter(AppView.this,
PreferenceConfiguration.readPreferences(AppView.this),
computer, localBinder.getUniqueId());
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.
@@ -283,10 +293,21 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
setContentView(R.layout.activity_app_view);
// Allow floating expanded PiP overlays while browsing apps
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
setShouldDockBigOverlays(false);
}
UiHelper.notifyNewRootView(this);
showHiddenApps = getIntent().getBooleanExtra(SHOW_HIDDEN_APPS_EXTRA, false);
uuidString = getIntent().getStringExtra(UUID_EXTRA);
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);
@@ -298,6 +319,21 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
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
@@ -355,9 +391,12 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
@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);
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));
@@ -365,10 +404,17 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
}
else {
menu.add(Menu.NONE, START_WITH_QUIT, 1, getResources().getString(R.string.applist_menu_quit_and_start));
menu.add(Menu.NONE, CANCEL_ID, 2, getResources().getString(R.string.applist_menu_cancel));
}
}
menu.add(Menu.NONE, VIEW_DETAILS_ID, 3, getResources().getString(R.string.applist_menu_details));
// 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
@@ -379,7 +425,7 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
BitmapDrawable drawable = (BitmapDrawable)appImageView.getDrawable();
if (drawable != null && drawable.getBitmap() != null) {
// We have a bitmap loaded too
menu.add(Menu.NONE, CREATE_SHORTCUT_ID, 4, getResources().getString(R.string.applist_menu_scut));
menu.add(Menu.NONE, CREATE_SHORTCUT_ID, 5, getResources().getString(R.string.applist_menu_scut));
}
}
}
@@ -430,12 +476,20 @@ 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 VIEW_DETAILS_ID:
Dialog.displayDialog(AppView.this, getResources().getString(R.string.title_details),
getResources().getString(R.string.applist_details_id) + " " + app.app.getAppId(), false);
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:
@@ -517,6 +571,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;
}
}
@@ -559,9 +619,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
@@ -586,9 +645,10 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
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

@@ -2,9 +2,12 @@ package com.limelight;
import android.app.Activity;
import android.graphics.Bitmap;
import android.os.Build;
import android.os.Bundle;
import android.webkit.WebView;
import android.webkit.WebViewClient;
import android.window.OnBackInvokedCallback;
import android.window.OnBackInvokedDispatcher;
import com.limelight.utils.SpinnerDialog;
@@ -13,10 +16,26 @@ public class HelpActivity extends Activity {
private SpinnerDialog loadingDialog;
private WebView webView;
private boolean backCallbackRegistered;
private OnBackInvokedCallback onBackInvokedCallback;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
onBackInvokedCallback = new OnBackInvokedCallback() {
@Override
public void onBackInvoked() {
// We should always be able to go back because we unregister our callback
// when we can't go back. Nonetheless, we will still check anyway.
if (webView.canGoBack()) {
webView.goBack();
}
}
};
}
webView = new WebView(this);
setContentView(webView);
@@ -39,6 +58,8 @@ public class HelpActivity extends Activity {
getResources().getString(R.string.help_loading_title),
getResources().getString(R.string.help_loading_msg), false);
}
refreshBackDispatchState();
}
@Override
@@ -47,19 +68,41 @@ public class HelpActivity extends Activity {
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()));
refreshBackDispatchState();
}
});
webView.loadUrl(getIntent().getData().toString());
}
private void refreshBackDispatchState() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
if (webView.canGoBack() && !backCallbackRegistered) {
getOnBackInvokedDispatcher().registerOnBackInvokedCallback(
OnBackInvokedDispatcher.PRIORITY_DEFAULT, onBackInvokedCallback);
backCallbackRegistered = true;
}
else if (!webView.canGoBack() && backCallbackRegistered) {
getOnBackInvokedDispatcher().unregisterOnBackInvokedCallback(onBackInvokedCallback);
backCallbackRegistered = false;
}
}
}
@Override
protected void onDestroy() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
if (backCallbackRegistered) {
getOnBackInvokedDispatcher().unregisterOnBackInvokedCallback(onBackInvokedCallback);
}
}
super.onDestroy();
}
@Override
// NOTE: This will NOT be called on Android 13+ with android:enableOnBackInvokedCallback="true"
public void onBackPressed() {
// Back goes back through the WebView history
// until no more history remains

View File

@@ -5,21 +5,21 @@ import java.util.logging.FileHandler;
import java.util.logging.Logger;
public class LimeLog {
private static final Logger LOGGER = Logger.getLogger(LimeLog.class.getName());
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));
}
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

@@ -109,7 +109,6 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
}
}
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;
@@ -117,12 +116,20 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
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 final static int GAMESTREAM_EOL_ID = 11;
private void initializeViews() {
setContentView(R.layout.activity_pc_view);
UiHelper.notifyNewRootView(this);
// Allow floating expanded PiP overlays while browsing PCs
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
setShouldDockBigOverlays(false);
}
// Set default preferences if we've never been run
PreferenceManager.setDefaultValues(this, R.xml.preferences, false);
@@ -154,6 +161,13 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
}
});
// Amazon review didn't like the help button because the wiki was not entirely
// navigable via the Fire TV remote (though the relevant parts were). Let's hide
// it on Fire TV.
if (getPackageManager().hasSystemFeature("amazon.hardware.fire_tv")) {
helpButton.setVisibility(View.GONE);
}
getFragmentManager().beginTransaction()
.replace(R.id.pcFragmentContainer, new AdapterFragment())
.commitAllowingStateLoss();
@@ -246,6 +260,11 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
updateComputer(details);
}
});
// Add a launcher shortcut for this PC (off the main thread to prevent ANRs)
if (details.pairState == PairState.PAIRED) {
shortcutHelper.createAppViewShortcutForOnlineHost(details);
}
}
}
});
@@ -316,15 +335,36 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
AdapterContextMenuInfo info = (AdapterContextMenuInfo) menuInfo;
ComputerObject computer = (ComputerObject) pcGridAdapter.getItem(info.position);
// 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.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));
menu.add(Menu.NONE, GAMESTREAM_EOL_ID, 2, getResources().getString(R.string.pcview_menu_eol));
}
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));
if (computer.details.nvidiaServer) {
menu.add(Menu.NONE, GAMESTREAM_EOL_ID, 2, getResources().getString(R.string.pcview_menu_eol));
}
}
else {
if (computer.details.runningGameId != 0) {
@@ -332,13 +372,16 @@ 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));
if (computer.details.nvidiaServer) {
menu.add(Menu.NONE, GAMESTREAM_EOL_ID, 3, getResources().getString(R.string.pcview_menu_eol));
}
// 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, VIEW_DETAILS_ID, 5, getResources().getString(R.string.pcview_menu_details));
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
@@ -350,15 +393,10 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
}
private void doPair(final ComputerDetails computer) {
if (computer.state == ComputerDetails.State.OFFLINE ||
ServerHelper.getCurrentAddressFromComputer(computer) == null) {
if (computer.state == ComputerDetails.State.OFFLINE || computer.activeAddress == null) {
Toast.makeText(PcView.this, getResources().getString(R.string.pair_pc_offline), Toast.LENGTH_SHORT).show();
return;
}
if (computer.runningGameId != 0) {
Toast.makeText(PcView.this, getResources().getString(R.string.pair_pc_ingame), Toast.LENGTH_LONG).show();
return;
}
if (managerBinder == null) {
Toast.makeText(PcView.this, getResources().getString(R.string.error_manager_not_running), Toast.LENGTH_LONG).show();
return;
@@ -376,8 +414,7 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
stopComputerUpdates(true);
httpConn = new NvHTTP(ServerHelper.getCurrentAddressFromComputer(computer),
managerBinder.getUniqueId(),
computer.serverCert,
computer.httpsPort, managerBinder.getUniqueId(), computer.serverCert,
PlatformBinding.getCryptoProvider(PcView.this));
if (httpConn.getPairState() == PairState.PAIRED) {
// Don't display any toast, but open the app list
@@ -389,16 +426,22 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
// Spin the dialog off in a thread because it blocks
Dialog.displayDialog(PcView.this, getResources().getString(R.string.pair_pairing_title),
getResources().getString(R.string.pair_pairing_msg)+" "+pinStr, false);
getResources().getString(R.string.pair_pairing_msg)+" "+pinStr+"\n\n"+
getResources().getString(R.string.pair_pairing_help), false);
PairingManager pm = httpConn.getPairingManager();
PairState pairState = pm.pair(httpConn.getServerInfo(), pinStr);
PairState pairState = pm.pair(httpConn.getServerInfo(true), pinStr);
if (pairState == PairState.PIN_WRONG) {
message = getResources().getString(R.string.pair_incorrect_pin);
}
else if (pairState == PairState.FAILED) {
message = getResources().getString(R.string.pair_fail);
if (computer.runningGameId != 0) {
message = getResources().getString(R.string.pair_pc_ingame);
}
else {
message = getResources().getString(R.string.pair_fail);
}
}
else if (pairState == PairState.ALREADY_IN_PROGRESS) {
message = getResources().getString(R.string.pair_already_in_progress);
@@ -442,7 +485,7 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
if (toastSuccess) {
// Open the app list after a successful pairing attempt
doAppList(computer, true);
doAppList(computer, true, false);
}
else {
// Start polling again if we're still in the foreground
@@ -488,8 +531,7 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
}
private void doUnpair(final ComputerDetails computer) {
if (computer.state == ComputerDetails.State.OFFLINE ||
ServerHelper.getCurrentAddressFromComputer(computer) == null) {
if (computer.state == ComputerDetails.State.OFFLINE || computer.activeAddress == null) {
Toast.makeText(PcView.this, getResources().getString(R.string.error_pc_offline), Toast.LENGTH_SHORT).show();
return;
}
@@ -506,8 +548,7 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
String message;
try {
httpConn = new NvHTTP(ServerHelper.getCurrentAddressFromComputer(computer),
managerBinder.getUniqueId(),
computer.serverCert,
computer.httpsPort, managerBinder.getUniqueId(), computer.serverCert,
PlatformBinding.getCryptoProvider(PcView.this));
if (httpConn.getPairState() == PairingManager.PairState.PAIRED) {
httpConn.unpair();
@@ -541,7 +582,7 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
}).start();
}
private void doAppList(ComputerDetails computer, boolean newlyPaired) {
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;
@@ -555,6 +596,7 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
i.putExtra(AppView.NAME_EXTRA, computer.name);
i.putExtra(AppView.UUID_EXTRA, computer.uuid);
i.putExtra(AppView.NEW_PAIR_EXTRA, newlyPaired);
i.putExtra(AppView.SHOW_HIDDEN_APPS_EXTRA, showHiddenGames);
startActivity(i);
}
@@ -592,8 +634,8 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
}, null);
return true;
case APP_LIST_ID:
doAppList(computer.details, false);
case FULL_APP_LIST_ID:
doAppList(computer.details, false, true);
return true;
case RESUME_ID:
@@ -625,6 +667,14 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
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;
case GAMESTREAM_EOL_ID:
HelpLauncher.launchGameStreamEolFaq(PcView.this);
return true;
default:
return super.onContextItemSelected(item);
}
@@ -635,6 +685,12 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
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);
@@ -669,11 +725,6 @@ 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;
@@ -692,9 +743,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
@@ -713,7 +762,7 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
// Pair an unpaired machine by default
doPair(computer.details);
} else {
doAppList(computer.details, false);
doAppList(computer.details, false, false);
}
}
});
@@ -721,7 +770,7 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
registerForContextMenu(listView);
}
public class ComputerObject {
public static class ComputerObject {
public ComputerDetails details;
public ComputerObject(ComputerDetails details) {

View File

@@ -8,17 +8,26 @@ import android.content.ServiceConnection;
import android.os.Bundle;
import android.os.IBinder;
import com.limelight.computers.ComputerDatabaseManager;
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.NvHTTP;
import com.limelight.nvstream.http.PairingManager;
import com.limelight.nvstream.wol.WakeOnLanSender;
import com.limelight.utils.CacheHelper;
import com.limelight.utils.Dialog;
import com.limelight.utils.ServerHelper;
import com.limelight.utils.SpinnerDialog;
import com.limelight.utils.UiHelper;
import org.xmlpull.v1.XmlPullParserException;
import java.io.IOException;
import java.io.StringReader;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
public class ShortcutTrampoline extends Activity {
@@ -26,10 +35,12 @@ public class ShortcutTrampoline extends Activity {
private NvApp app;
private ArrayList<Intent> intentStack = new ArrayList<>();
private int wakeHostTries = 10;
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 =
@@ -79,6 +90,23 @@ public class ShortcutTrampoline extends Activity {
return;
}
// Try to wake the target PC if it's offline (up to some retry limit)
if (details.state == ComputerDetails.State.OFFLINE && details.macAddress != null && --wakeHostTries >= 0) {
try {
// Make a best effort attempt to wake the target PC
WakeOnLanSender.sendWolPacket(computer);
// If we sent at least one WoL packet, reset the computer state
// to force ComputerManager to poll it again.
managerBinder.invalidateStateForComputer(computer.uuid);
return;
} catch (IOException e) {
// If we got an exception, we couldn't send a single WoL packet,
// so fallthrough into the offline error path.
e.printStackTrace();
}
}
if (details.state != ComputerDetails.State.UNKNOWN) {
runOnUiThread(new Runnable() {
@Override
@@ -194,9 +222,9 @@ public class ShortcutTrampoline extends Activity {
}
};
protected boolean validateInput(String uuidString, String appIdString) {
// Validate UUID
if (uuidString == null) {
protected boolean validateInput(String uuidString, String appIdString, String nameString) {
// Validate PC UUID/Name
if (uuidString == null && nameString == null) {
Dialog.displayDialog(ShortcutTrampoline.this,
getResources().getString(R.string.conn_error_title),
getResources().getString(R.string.scut_invalid_uuid),
@@ -204,14 +232,25 @@ public class ShortcutTrampoline extends Activity {
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;
if (uuidString != null && !uuidString.isEmpty()) {
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;
}
} else {
// UUID is null, so fallback to Name
if (nameString == null || nameString.isEmpty()) {
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)
@@ -235,24 +274,93 @@ public class ShortcutTrampoline extends Activity {
super.onCreate(savedInstanceState);
UiHelper.notifyNewRootView(this);
ComputerDatabaseManager dbManager = new ComputerDatabaseManager(this);
ComputerDetails _computer = null;
String appIdString = getIntent().getStringExtra(Game.EXTRA_APP_ID);
// PC arguments, both are optional, but at least one must be provided
uuidString = getIntent().getStringExtra(AppView.UUID_EXTRA);
String nameString = getIntent().getStringExtra(AppView.NAME_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));
// App arguments, both are optional, but one must be provided in order to start an app
String appIdString = getIntent().getStringExtra(Game.EXTRA_APP_ID);
String appNameString = getIntent().getStringExtra(Game.EXTRA_APP_NAME);
if (!validateInput(uuidString, appIdString, nameString)) {
// Invalid input, so just return
return;
}
if (uuidString == null || uuidString.isEmpty()) {
// Use nameString to find the corresponding UUID
_computer = dbManager.getComputerByName(nameString);
if (_computer == null) {
Dialog.displayDialog(ShortcutTrampoline.this,
getResources().getString(R.string.conn_error_title),
getResources().getString(R.string.scut_pc_not_found),
true);
return;
}
// Bind to the computer manager service
bindService(new Intent(this, ComputerManagerService.class), serviceConnection,
Service.BIND_AUTO_CREATE);
uuidString = _computer.uuid;
blockingLoadSpinner = SpinnerDialog.displayDialog(this, getResources().getString(R.string.conn_establishing_title),
getResources().getString(R.string.applist_connect_msg), true);
// Set the AppView UUID intent, since it wasn't provided
setIntent(new Intent(getIntent()).putExtra(AppView.UUID_EXTRA, uuidString));
}
if (appIdString != null && !appIdString.isEmpty()) {
app = new NvApp(getIntent().getStringExtra(Game.EXTRA_APP_NAME),
Integer.parseInt(appIdString),
getIntent().getBooleanExtra(Game.EXTRA_APP_HDR, false));
}
else if (appNameString != null && !appNameString.isEmpty()) {
// Use appNameString to find the corresponding AppId
try {
int appId = -1;
String rawAppList = CacheHelper.readInputStreamToString(CacheHelper.openCacheFileForInput(getCacheDir(), "applist", uuidString));
if (rawAppList.isEmpty()) {
Dialog.displayDialog(ShortcutTrampoline.this,
getResources().getString(R.string.conn_error_title),
getResources().getString(R.string.scut_invalid_app_id),
true);
return;
}
List<NvApp> applist = NvHTTP.getAppListByReader(new StringReader(rawAppList));
for (NvApp _app : applist) {
if (_app.getAppName().equals(appNameString)) {
appId = _app.getAppId();
break;
}
}
if (appId < 0) {
Dialog.displayDialog(ShortcutTrampoline.this,
getResources().getString(R.string.conn_error_title),
getResources().getString(R.string.scut_invalid_app_id),
true);
return;
}
setIntent(new Intent(getIntent()).putExtra(Game.EXTRA_APP_ID, appId));
app = new NvApp(
appNameString,
appId,
getIntent().getBooleanExtra(Game.EXTRA_APP_HDR, false));
} catch (IOException | XmlPullParserException e) {
Dialog.displayDialog(ShortcutTrampoline.this,
getResources().getString(R.string.conn_error_title),
getResources().getString(R.string.scut_invalid_app_id),
true);
return;
}
}
// 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

View File

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

View File

@@ -1,9 +1,12 @@
package com.limelight.binding.audio;
import android.content.Context;
import android.content.Intent;
import android.media.AudioAttributes;
import android.media.AudioFormat;
import android.media.AudioManager;
import android.media.AudioTrack;
import android.media.audiofx.AudioEffect;
import android.os.Build;
import com.limelight.LimeLog;
@@ -12,77 +15,86 @@ import com.limelight.nvstream.jni.MoonBridge;
public class AndroidAudioRenderer implements AudioRenderer {
private final Context context;
private final boolean enableAudioFx;
private AudioTrack track;
private AudioTrack createAudioTrack(int channelConfig, int bufferSize, boolean lowLatency) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
return new AudioTrack(AudioManager.STREAM_MUSIC,
48000,
channelConfig,
AudioFormat.ENCODING_PCM_16BIT,
bufferSize,
AudioTrack.MODE_STREAM);
public AndroidAudioRenderer(Context context, boolean enableAudioFx) {
this.context = context;
this.enableAudioFx = enableAudioFx;
}
private AudioTrack createAudioTrack(int channelConfig, int sampleRate, int bufferSize, boolean lowLatency) {
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 {
AudioAttributes.Builder attributesBuilder = new AudioAttributes.Builder()
.setUsage(AudioAttributes.USAGE_GAME);
AudioFormat format = new AudioFormat.Builder()
.setEncoding(AudioFormat.ENCODING_PCM_16BIT)
.setSampleRate(48000)
.setChannelMask(channelConfig)
.build();
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);
}
return new AudioTrack(attributesBuilder.build(),
format,
bufferSize,
AudioTrack.MODE_STREAM,
AudioManager.AUDIO_SESSION_ID_GENERATE);
}
}
@Override
public int setup(int audioConfiguration) {
public int setup(MoonBridge.AudioConfiguration audioConfiguration, int sampleRate, int samplesPerFrame) {
int channelConfig;
int bytesPerFrame;
switch (audioConfiguration)
switch (audioConfiguration.channelCount)
{
case MoonBridge.AUDIO_CONFIGURATION_STEREO:
case 2:
channelConfig = AudioFormat.CHANNEL_OUT_STEREO;
bytesPerFrame = 2 * 240 * 2;
break;
case MoonBridge.AUDIO_CONFIGURATION_51_SURROUND:
case 4:
channelConfig = AudioFormat.CHANNEL_OUT_QUAD;
break;
case 6:
channelConfig = AudioFormat.CHANNEL_OUT_5POINT1;
bytesPerFrame = 6 * 240 * 2;
break;
case 8:
// 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
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.
@@ -122,7 +134,7 @@ public class AndroidAudioRenderer implements AudioRenderer {
case 1:
case 3:
// Try the larger buffer size
bufferSize = Math.max(AudioTrack.getMinBufferSize(48000,
bufferSize = Math.max(AudioTrack.getMinBufferSize(sampleRate,
channelConfig,
AudioFormat.ENCODING_PCM_16BIT),
bytesPerFrame * 2);
@@ -135,13 +147,19 @@ public class AndroidAudioRenderer implements AudioRenderer {
throw new IllegalStateException();
}
// Skip low latency options if hardware sample rate isn't 48000Hz
if (AudioTrack.getNativeOutputSampleRate(AudioManager.STREAM_MUSIC) != 48000 && lowLatency) {
// Skip low latency options if hardware sample rate doesn't match the content
if (AudioTrack.getNativeOutputSampleRate(AudioManager.STREAM_MUSIC) != sampleRate && lowLatency) {
continue;
}
// Skip low latency options when using audio effects, since low latency mode
// precludes the use of the audio effect pipeline (as of Android 13).
if (enableAudioFx && lowLatency) {
continue;
}
try {
track = createAudioTrack(channelConfig, bufferSize, lowLatency);
track = createAudioTrack(channelConfig, sampleRate, bufferSize, lowLatency);
track.play();
// Successfully created working AudioTrack. We're done here.
@@ -170,22 +188,39 @@ public class AndroidAudioRenderer implements AudioRenderer {
@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.getPendingAudioFrames() < 8) {
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 many pending audio frames: " + MoonBridge.getPendingAudioFrames());
LimeLog.info("Too much pending audio data: " + MoonBridge.getPendingAudioDuration() +" ms");
}
}
@Override
public void start() {}
public void start() {
if (enableAudioFx) {
// Open an audio effect control session to allow equalizers to apply audio effects
Intent i = new Intent(AudioEffect.ACTION_OPEN_AUDIO_EFFECT_CONTROL_SESSION);
i.putExtra(AudioEffect.EXTRA_AUDIO_SESSION, track.getAudioSessionId());
i.putExtra(AudioEffect.EXTRA_PACKAGE_NAME, context.getPackageName());
i.putExtra(AudioEffect.EXTRA_CONTENT_TYPE, AudioEffect.CONTENT_TYPE_GAME);
context.sendBroadcast(i);
}
}
@Override
public void stop() {}
public void stop() {
if (enableAudioFx) {
// Close our audio effect control session when we're stopping
Intent i = new Intent(AudioEffect.ACTION_CLOSE_AUDIO_EFFECT_CONTROL_SESSION);
i.putExtra(AudioEffect.EXTRA_AUDIO_SESSION, track.getAudioSessionId());
i.putExtra(AudioEffect.EXTRA_PACKAGE_NAME, context.getPackageName());
context.sendBroadcast(i);
}
}
@Override
public void cleanup() {

View File

@@ -12,12 +12,12 @@ import java.security.KeyFactory;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.Provider;
import java.security.SecureRandom;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.security.interfaces.RSAPrivateKey;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
import java.util.Calendar;
@@ -48,7 +48,7 @@ public class AndroidCryptoProvider implements LimelightCryptoProvider {
private final File keyFile;
private X509Certificate cert;
private RSAPrivateKey key;
private PrivateKey key;
private byte[] pemCertBytes;
private static final Object globalCryptoLock = new Object();
@@ -67,14 +67,12 @@ public class AndroidCryptoProvider implements LimelightCryptoProvider {
return null;
}
try {
FileInputStream fin = new FileInputStream(f);
try (final FileInputStream fin = new FileInputStream(f)) {
byte[] fileData = new byte[(int) f.length()];
if (fin.read(fileData) != f.length()) {
// Failed to read
fileData = null;
}
fin.close();
return fileData;
} catch (IOException e) {
return null;
@@ -96,15 +94,13 @@ public class AndroidCryptoProvider implements LimelightCryptoProvider {
cert = (X509Certificate) certFactory.generateCertificate(new ByteArrayInputStream(certBytes));
pemCertBytes = certBytes;
KeyFactory keyFactory = KeyFactory.getInstance("RSA", bcProvider);
key = (RSAPrivateKey) keyFactory.generatePrivate(new PKCS8EncodedKeySpec(keyBytes));
key = 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");
@@ -124,10 +120,8 @@ public class AndroidCryptoProvider implements LimelightCryptoProvider {
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA", bcProvider);
keyPairGenerator.initialize(2048);
keyPair = keyPairGenerator.generateKeyPair();
} catch (NoSuchAlgorithmException e1) {
// Should never happen
e1.printStackTrace();
return false;
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException(e);
}
Date now = new Date();
@@ -150,10 +144,8 @@ public class AndroidCryptoProvider implements LimelightCryptoProvider {
try {
ContentSigner sigGen = new JcaContentSignerBuilder("SHA256withRSA").setProvider(bcProvider).build(keyPair.getPrivate());
cert = new JcaX509CertificateConverter().setProvider(bcProvider).getCertificate(certBuilder.build(sigGen));
key = (RSAPrivateKey) keyPair.getPrivate();
key = keyPair.getPrivate();
} catch (Exception e) {
// Nothing should go wrong here
e.printStackTrace();
throw new RuntimeException(e);
}
@@ -166,32 +158,28 @@ public class AndroidCryptoProvider implements LimelightCryptoProvider {
}
private void saveCertKeyPair() {
try {
FileOutputStream certOut = new FileOutputStream(certFile);
FileOutputStream keyOut = new FileOutputStream(keyFile);
try (final FileOutputStream certOut = new FileOutputStream(certFile);
final FileOutputStream keyOut = new FileOutputStream(keyFile)
) {
// Write the certificate in OpenSSL PEM format (important for the server)
StringWriter strWriter = new StringWriter();
JcaPEMWriter pemWriter = new JcaPEMWriter(strWriter);
pemWriter.writeObject(cert);
pemWriter.close();
try (final JcaPEMWriter pemWriter = new JcaPEMWriter(strWriter)) {
pemWriter.writeObject(cert);
}
// Line endings MUST be UNIX for the PC to accept the cert properly
OutputStreamWriter certWriter = new OutputStreamWriter(certOut);
String pemStr = strWriter.getBuffer().toString();
for (int i = 0; i < pemStr.length(); i++) {
char c = pemStr.charAt(i);
if (c != '\r')
certWriter.append(c);
try (final OutputStreamWriter certWriter = new OutputStreamWriter(certOut)) {
String pemStr = strWriter.getBuffer().toString();
for (int i = 0; i < pemStr.length(); i++) {
char c = pemStr.charAt(i);
if (c != '\r')
certWriter.append(c);
}
}
certWriter.close();
// Write the private out in PKCS8 format
keyOut.write(key.getEncoded());
certOut.close();
keyOut.close();
LimeLog.info("Saved generated key pair to disk");
} catch (IOException e) {
// This isn't good because it means we'll have
@@ -227,7 +215,7 @@ public class AndroidCryptoProvider implements LimelightCryptoProvider {
}
}
public RSAPrivateKey getClientPrivateKey() {
public PrivateKey getClientPrivateKey() {
// Use a lock here to ensure only one guy will be generating or loading
// the certificate and key at a time
synchronized (globalCryptoLock) {

View File

@@ -1,297 +1,386 @@
package com.limelight.binding.input;
import android.annotation.TargetApi;
import android.hardware.input.InputManager;
import android.os.Build;
import android.util.SparseArray;
import android.view.InputDevice;
import android.view.KeyEvent;
import java.util.Arrays;
/**
* Class to translate a Android key code into the codes GFE is expecting
* @author Diego Waxemberg
* @author Cameron Gutman
*/
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 class KeyboardTranslator implements InputManager.InputDeviceListener {
/**
* 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_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 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;
private static class KeyboardMapping {
private final InputDevice device;
private final int[] deviceKeyCodeToQwertyKeyCode;
default:
return false;
}
}
@TargetApi(33)
public KeyboardMapping(InputDevice device) {
int maxKeyCode = KeyEvent.getMaxKeyCode();
/**
* 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;
this.device = device;
this.deviceKeyCodeToQwertyKeyCode = new int[maxKeyCode + 1];
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;
// Any unmatched keycodes are treated as unknown
Arrays.fill(deviceKeyCodeToQwertyKeyCode, KeyEvent.KEYCODE_UNKNOWN);
case KeyEvent.KEYCODE_CTRL_RIGHT:
translated = 0xA3;
break;
case KeyEvent.KEYCODE_DEL:
translated = VK_BACK_SPACE;
break;
case KeyEvent.KEYCODE_ENTER:
translated = 0x0d;
break;
for (int i = 0; i <= maxKeyCode; i++) {
int deviceKeyCode = device.getKeyCodeForKeyLocation(i);
if (deviceKeyCode != KeyEvent.KEYCODE_UNKNOWN) {
deviceKeyCodeToQwertyKeyCode[deviceKeyCode] = i;
}
}
}
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;
@TargetApi(33)
public int getDeviceKeyCodeForQwertyKeyCode(int qwertyKeyCode) {
return device.getKeyCodeForKeyLocation(qwertyKeyCode);
}
case KeyEvent.KEYCODE_META_LEFT:
translated = 0x5b;
break;
public int getQwertyKeyCodeForDeviceKeyCode(int deviceKeyCode) {
if (deviceKeyCode > KeyEvent.getMaxKeyCode()) {
return KeyEvent.KEYCODE_UNKNOWN;
}
case KeyEvent.KEYCODE_META_RIGHT:
translated = 0x5c;
break;
return deviceKeyCodeToQwertyKeyCode[deviceKeyCode];
}
}
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;
private final SparseArray<KeyboardMapping> keyboardMappings = new SparseArray<>();
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;
public KeyboardTranslator() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
for (int deviceId : InputDevice.getDeviceIds()) {
InputDevice device = InputDevice.getDevice(deviceId);
if (device != null && device.getKeyboardType() == InputDevice.KEYBOARD_TYPE_ALPHABETIC) {
keyboardMappings.set(deviceId, new KeyboardMapping(device));
}
}
}
}
case KeyEvent.KEYCODE_NUMPAD_DIVIDE:
translated = 0x6F;
break;
public boolean hasNormalizedMapping(int keycode, int deviceId) {
if (deviceId >= 0) {
KeyboardMapping mapping = keyboardMappings.get(deviceId);
if (mapping != null) {
// Try to map this device-specific keycode onto a QWERTY layout.
// GFE assumes incoming keycodes are from a QWERTY keyboard.
int qwertyKeyCode = mapping.getQwertyKeyCodeForDeviceKeyCode(keycode);
if (qwertyKeyCode != KeyEvent.KEYCODE_UNKNOWN) {
return true;
}
}
}
case KeyEvent.KEYCODE_NUMPAD_MULTIPLY:
translated = 0x6A;
break;
return false;
}
case KeyEvent.KEYCODE_NUMPAD_SUBTRACT:
translated = 0x6D;
break;
/**
* Translates the given keycode and returns the GFE keycode
* @param keycode the code to be translated
* @param deviceId InputDevice.getId() or -1 if unknown
* @return a GFE keycode for the given keycode
*/
public short translate(int keycode, int deviceId) {
int translated;
case KeyEvent.KEYCODE_NUMPAD_ADD:
translated = 0x6B;
break;
// If a device ID was provided, look up the keyboard mapping
if (deviceId >= 0) {
KeyboardMapping mapping = keyboardMappings.get(deviceId);
if (mapping != null) {
// Try to map this device-specific keycode onto a QWERTY layout.
// GFE assumes incoming keycodes are from a QWERTY keyboard.
int qwertyKeyCode = mapping.getQwertyKeyCodeForDeviceKeyCode(keycode);
if (qwertyKeyCode != KeyEvent.KEYCODE_UNKNOWN) {
keycode = qwertyKeyCode;
}
}
}
// 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_NUMPAD_DOT:
translated = 0x6E;
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_AT:
translated = 2 + VK_0;
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_POUND:
translated = 3 + VK_0;
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_STAR:
translated = 8 + VK_0;
break;
case KeyEvent.KEYCODE_META_LEFT:
translated = 0x5b;
break;
default:
System.out.println("No key for "+keycode);
return 0;
}
}
return (short) ((KEY_PREFIX << 8) | translated);
}
case KeyEvent.KEYCODE_META_RIGHT:
translated = 0x5c;
break;
case KeyEvent.KEYCODE_MENU:
translated = 0x5d;
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;
default:
return 0;
}
}
return (short) ((KEY_PREFIX << 8) | translated);
}
@Override
public void onInputDeviceAdded(int index) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
InputDevice device = InputDevice.getDevice(index);
if (device != null && device.getKeyboardType() == InputDevice.KEYBOARD_TYPE_ALPHABETIC) {
keyboardMappings.put(index, new KeyboardMapping(device));
}
}
}
@Override
public void onInputDeviceRemoved(int index) {
keyboardMappings.remove(index);
}
@Override
public void onInputDeviceChanged(int index) {
keyboardMappings.remove(index);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
InputDevice device = InputDevice.getDevice(index);
if (device != null && device.getKeyboardType() == InputDevice.KEYBOARD_TYPE_ALPHABETIC) {
keyboardMappings.set(index, new KeyboardMapping(device));
}
}
}
}

View File

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

View File

@@ -1,17 +1,26 @@
package com.limelight.binding.input.capture;
import android.annotation.TargetApi;
import android.app.Activity;
import android.hardware.input.InputManager;
import android.os.Build;
import android.os.Handler;
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 InputCaptureProvider {
public class AndroidNativePointerCaptureProvider extends AndroidPointerIconCaptureProvider implements InputManager.InputDeviceListener {
private final InputManager inputManager;
private final View targetView;
private View targetView;
public AndroidNativePointerCaptureProvider(View targetView) {
public AndroidNativePointerCaptureProvider(Activity activity, View targetView) {
super(activity, targetView);
this.inputManager = activity.getSystemService(InputManager.class);
this.targetView = targetView;
}
@@ -19,43 +28,143 @@ public class AndroidNativePointerCaptureProvider extends InputCaptureProvider {
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.O;
}
@Override
public void enableCapture() {
super.enableCapture();
targetView.requestPointerCapture();
// We only capture the pointer if we have a compatible InputDevice
// present. This is a workaround for an Android 12 regression causing
// incorrect mouse input when using the SPen.
// https://github.com/moonlight-stream/moonlight-android/issues/1030
private boolean hasCaptureCompatibleInputDevice() {
for (int id : InputDevice.getDeviceIds()) {
InputDevice device = InputDevice.getDevice(id);
if (device == null) {
continue;
}
// Skip touchscreens when considering compatible capture devices.
// Samsung devices on Android 12 will report a sec_touchpad device
// with SOURCE_TOUCHSCREEN, SOURCE_KEYBOARD, and SOURCE_MOUSE.
// Upon enabling pointer capture, that device will switch to
// SOURCE_KEYBOARD and SOURCE_TOUCHPAD.
// Only skip on non ChromeOS devices cause the ChromeOS pointer else
// gets disabled removing relative mouse capabilities
// on Chromebooks with touchscreens
if (device.supportsSource(InputDevice.SOURCE_TOUCHSCREEN) && !targetView.getContext().getPackageManager().hasSystemFeature("org.chromium.arc.device_management")) {
continue;
}
if (device.supportsSource(InputDevice.SOURCE_MOUSE) ||
device.supportsSource(InputDevice.SOURCE_MOUSE_RELATIVE) ||
device.supportsSource(InputDevice.SOURCE_TOUCHPAD)) {
return true;
}
}
return false;
}
@Override
public void disableCapture() {
super.disableCapture();
public void showCursor() {
super.showCursor();
// It is important to unregister the listener *before* releasing pointer capture,
// because releasing pointer capture can cause an onInputDeviceChanged() callback
// for devices with a touchpad (like a DS4 controller).
inputManager.unregisterInputDeviceListener(this);
targetView.releasePointerCapture();
}
@Override
public boolean isCapturingActive() {
return targetView.hasPointerCapture();
public void hideCursor() {
super.hideCursor();
// Listen for device events to enable/disable capture
inputManager.registerInputDeviceListener(this, null);
// Capture now if we have a capture-capable device
if (hasCaptureCompatibleInputDevice()) {
targetView.requestPointerCapture();
}
}
@Override
public void onWindowFocusChanged(boolean focusActive) {
// NB: We have to check cursor visibility here because Android pointer capture
// doesn't support capturing the cursor while it's visible. Enabling pointer
// capture implicitly hides the cursor.
if (!focusActive || !isCapturing || isCursorVisible) {
return;
}
// Recapture the pointer if focus was regained. On Android Q,
// we have to delay a bit before requesting capture because otherwise
// we'll hit the "requestPointerCapture called for a window that has no focus"
// error and it will not actually capture the cursor.
Handler h = new Handler();
h.postDelayed(new Runnable() {
@Override
public void run() {
if (hasCaptureCompatibleInputDevice()) {
targetView.requestPointerCapture();
}
}
}, 500);
}
@Override
public boolean eventHasRelativeMouseAxes(MotionEvent event) {
return event.getSource() == InputDevice.SOURCE_MOUSE_RELATIVE;
// 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()
int eventSource = event.getSource();
return (eventSource == InputDevice.SOURCE_MOUSE_RELATIVE && event.getToolType(0) == MotionEvent.TOOL_TYPE_MOUSE) ||
(eventSource == InputDevice.SOURCE_TOUCHPAD && targetView.hasPointerCapture());
}
@Override
public float getRelativeAxisX(MotionEvent event) {
float x = event.getX();
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.getHistoricalX(i);
x += event.getHistoricalAxisValue(axis, i);
}
return x;
}
@Override
public float getRelativeAxisY(MotionEvent event) {
float y = event.getY();
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.getHistoricalY(i);
y += event.getHistoricalAxisValue(axis, i);
}
return y;
}
@Override
public void onInputDeviceAdded(int deviceId) {
// Check if we've added a capture-compatible device
if (!targetView.hasPointerCapture() && hasCaptureCompatibleInputDevice()) {
targetView.requestPointerCapture();
}
}
@Override
public void onInputDeviceRemoved(int deviceId) {
// Check if the capture-compatible device was removed
if (targetView.hasPointerCapture() && !hasCaptureCompatibleInputDevice()) {
targetView.releasePointerCapture();
}
}
@Override
public void onInputDeviceChanged(int deviceId) {
// Emulating a remove+add should be sufficient for our purposes.
//
// Note: This callback must be handled carefully because it can happen as a result of
// calling requestPointerCapture(). This can cause trackpad devices to gain SOURCE_MOUSE_RELATIVE
// and re-enter this callback.
onInputDeviceRemoved(deviceId);
onInputDeviceAdded(deviceId);
}
}

View File

@@ -4,58 +4,32 @@ import android.annotation.TargetApi;
import android.app.Activity;
import android.content.Context;
import android.os.Build;
import android.view.MotionEvent;
import android.view.PointerIcon;
import android.view.View;
import android.view.ViewGroup;
@TargetApi(Build.VERSION_CODES.N)
public class AndroidPointerIconCaptureProvider extends InputCaptureProvider {
private ViewGroup rootViewGroup;
private Context context;
private final View targetView;
private final Context context;
public AndroidPointerIconCaptureProvider(Activity activity) {
public AndroidPointerIconCaptureProvider(Activity activity, View targetView) {
this.context = activity;
this.rootViewGroup = (ViewGroup) activity.getWindow().getDecorView();
this.targetView = targetView;
}
public static boolean isCaptureProviderSupported() {
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.N;
}
private void setPointerIconOnAllViews(PointerIcon icon) {
for (int i = 0; i < rootViewGroup.getChildCount(); i++) {
View view = rootViewGroup.getChildAt(i);
view.setPointerIcon(icon);
}
rootViewGroup.setPointerIcon(icon);
@Override
public void hideCursor() {
super.hideCursor();
targetView.setPointerIcon(PointerIcon.getSystemIcon(context, PointerIcon.TYPE_NULL));
}
@Override
public void enableCapture() {
super.enableCapture();
setPointerIconOnAllViews(PointerIcon.getSystemIcon(context, PointerIcon.TYPE_NULL));
}
@Override
public void disableCapture() {
super.disableCapture();
setPointerIconOnAllViews(null);
}
@Override
public boolean eventHasRelativeMouseAxes(MotionEvent event) {
return event.getAxisValue(MotionEvent.AXIS_RELATIVE_X) != 0 ||
event.getAxisValue(MotionEvent.AXIS_RELATIVE_Y) != 0;
}
@Override
public float getRelativeAxisX(MotionEvent event) {
return event.getAxisValue(MotionEvent.AXIS_RELATIVE_X);
}
@Override
public float getRelativeAxisY(MotionEvent event) {
return event.getAxisValue(MotionEvent.AXIS_RELATIVE_Y);
public void showCursor() {
super.showCursor();
targetView.setPointerIcon(null);
}
}

View File

@@ -2,8 +2,8 @@ package com.limelight.binding.input.capture;
import android.app.Activity;
import com.limelight.BuildConfig;
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;
@@ -12,11 +12,11 @@ public class InputCaptureManager {
public static InputCaptureProvider getInputCaptureProvider(Activity activity, EvdevListener rootListener) {
if (AndroidNativePointerCaptureProvider.isCaptureProviderSupported()) {
LimeLog.info("Using Android O+ native mouse capture");
return new AndroidNativePointerCaptureProvider(activity.findViewById(R.id.surfaceView));
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()) {
else if (!BuildConfig.ROOT_BUILD && ShieldCaptureProvider.isCaptureProviderSupported()) {
LimeLog.info("Using NVIDIA mouse capture extension");
return new ShieldCaptureProvider(activity);
}
@@ -28,7 +28,7 @@ public class InputCaptureManager {
// 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);
return new AndroidPointerIconCaptureProvider(activity, activity.findViewById(R.id.surfaceView));
}
else {
LimeLog.info("Mouse capture not available");

View File

@@ -4,12 +4,15 @@ import android.view.MotionEvent;
public abstract class InputCaptureProvider {
protected boolean isCapturing;
protected boolean isCursorVisible;
public void enableCapture() {
isCapturing = true;
hideCursor();
}
public void disableCapture() {
isCapturing = false;
showCursor();
}
public void destroy() {}
@@ -22,6 +25,14 @@ public abstract class InputCaptureProvider {
return isCapturing;
}
public void showCursor() {
isCursorVisible = true;
}
public void hideCursor() {
isCursorVisible = false;
}
public boolean eventHasRelativeMouseAxes(MotionEvent event) {
return false;
}
@@ -33,4 +44,6 @@ public abstract class InputCaptureProvider {
public float getRelativeAxisY(MotionEvent event) {
return 0;
}
public void onWindowFocusChanged(boolean focusActive) {}
}

View File

@@ -22,7 +22,7 @@ public class ShieldCaptureProvider extends InputCaptureProvider {
private static int AXIS_RELATIVE_X;
private static int AXIS_RELATIVE_Y;
private Context context;
private final Context context;
static {
try {
@@ -62,21 +62,23 @@ public class ShieldCaptureProvider extends InputCaptureProvider {
}
@Override
public void enableCapture() {
super.enableCapture();
public void hideCursor() {
super.hideCursor();
setCursorVisibility(false);
}
@Override
public void disableCapture() {
super.disableCapture();
public void showCursor() {
super.showCursor();
setCursorVisibility(true);
}
@Override
public boolean eventHasRelativeMouseAxes(MotionEvent event) {
return event.getAxisValue(AXIS_RELATIVE_X) != 0 ||
event.getAxisValue(AXIS_RELATIVE_Y) != 0;
// 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

View File

@@ -3,18 +3,42 @@ 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 int buttonFlags, supportedButtonFlags;
protected float leftTrigger, rightTrigger;
protected float rightStickX, rightStickY;
protected float leftStickX, leftStickY;
protected short capabilities;
protected byte type;
public int getControllerId() {
return deviceId;
}
public int getVendorId() {
return vendorId;
}
public int getProductId() {
return productId;
}
public int getSupportedButtonFlags() {
return supportedButtonFlags;
}
public short getCapabilities() {
return capabilities;
}
public byte getType() {
return type;
}
protected void setButtonFlag(int buttonFlag, int data) {
if (data != 0) {
buttonFlags |= buttonFlag;
@@ -32,13 +56,17 @@ public abstract class AbstractController {
public abstract boolean start();
public abstract void stop();
public AbstractController(int deviceId, UsbDriverListener listener) {
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);
public abstract void rumbleTriggers(short leftTrigger, short rightTrigger);
protected void notifyDeviceRemoved() {
listener.deviceRemoved(this);
}

View File

@@ -5,9 +5,11 @@ import android.hardware.usb.UsbDevice;
import android.hardware.usb.UsbDeviceConnection;
import android.hardware.usb.UsbEndpoint;
import android.hardware.usb.UsbInterface;
import android.os.SystemClock;
import com.limelight.LimeLog;
import com.limelight.binding.video.MediaCodecHelper;
import com.limelight.nvstream.input.ControllerPacket;
import com.limelight.nvstream.jni.MoonBridge;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
@@ -22,9 +24,17 @@ public abstract class AbstractXboxController extends AbstractController {
protected UsbEndpoint inEndpt, outEndpt;
public AbstractXboxController(UsbDevice device, UsbDeviceConnection connection, int deviceId, UsbDriverListener listener) {
super(deviceId, listener);
super(deviceId, listener, device.getVendorId(), device.getProductId());
this.device = device;
this.connection = connection;
this.type = MoonBridge.LI_CTYPE_XBOX;
this.capabilities = MoonBridge.LI_CCAP_ANALOG_TRIGGERS | MoonBridge.LI_CCAP_RUMBLE;
this.buttonFlags =
ControllerPacket.A_FLAG | ControllerPacket.B_FLAG | ControllerPacket.X_FLAG | ControllerPacket.Y_FLAG |
ControllerPacket.UP_FLAG | ControllerPacket.DOWN_FLAG | ControllerPacket.LEFT_FLAG | ControllerPacket.RIGHT_FLAG |
ControllerPacket.LB_FLAG | ControllerPacket.RB_FLAG |
ControllerPacket.LS_CLK_FLAG | ControllerPacket.RS_CLK_FLAG |
ControllerPacket.BACK_FLAG | ControllerPacket.PLAY_FLAG | ControllerPacket.SPECIAL_BUTTON_FLAG;
}
private Thread createInputThread() {
@@ -37,7 +47,9 @@ public abstract class AbstractXboxController extends AbstractController {
// 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) {}
} catch (InterruptedException e) {
return;
}
// Report that we're added _before_ reporting input
notifyDeviceAdded();
@@ -56,7 +68,7 @@ public abstract class AbstractXboxController extends AbstractController {
do {
// Read the next input state packet
long lastMillis = MediaCodecHelper.getMonotonicMillis();
long lastMillis = SystemClock.uptimeMillis();
res = connection.bulkTransfer(inEndpt, buffer, buffer.length, 3000);
// If we get a zero length response, treat it as an error
@@ -64,7 +76,7 @@ public abstract class AbstractXboxController extends AbstractController {
res = -1;
}
if (res == -1 && MediaCodecHelper.getMonotonicMillis() - lastMillis < 1000) {
if (res == -1 && SystemClock.uptimeMillis() - lastMillis < 1000) {
LimeLog.warning("Detected device I/O error");
AbstractXboxController.this.stop();
break;

View File

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

View File

@@ -1,5 +1,6 @@
package com.limelight.binding.input.driver;
import android.annotation.SuppressLint;
import android.app.PendingIntent;
import android.app.Service;
import android.content.BroadcastReceiver;
@@ -20,6 +21,7 @@ import com.limelight.LimeLog;
import com.limelight.R;
import com.limelight.preferences.PreferenceConfiguration;
import java.io.File;
import java.util.ArrayList;
public class UsbDriverService extends Service implements UsbDriverListener {
@@ -29,6 +31,7 @@ public class UsbDriverService extends Service implements UsbDriverListener {
private UsbManager usbManager;
private PreferenceConfiguration prefConfig;
private boolean started;
private final UsbEventReceiver receiver = new UsbEventReceiver();
private final UsbDriverBinder binder = new UsbDriverBinder();
@@ -36,10 +39,12 @@ public class UsbDriverService extends Service implements UsbDriverListener {
private final ArrayList<AbstractController> controllers = new ArrayList<>();
private UsbDriverListener listener;
private UsbDriverStateListener stateListener;
private int nextDeviceId;
@Override
public void reportControllerState(int controllerId, short buttonFlags, float leftStickX, float leftStickY, float rightStickX, float rightStickY, float leftTrigger, float rightTrigger) {
public void reportControllerState(int controllerId, int 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);
@@ -93,6 +98,11 @@ public class UsbDriverService extends Service implements UsbDriverListener {
else if (action.equals(ACTION_USB_PERMISSION)) {
UsbDevice device = intent.getParcelableExtra(UsbManager.EXTRA_DEVICE);
// Permission dialog is now closed
if (stateListener != null) {
stateListener.onUsbPermissionPromptCompleted();
}
// 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);
@@ -112,6 +122,18 @@ public class UsbDriverService extends Service implements UsbDriverListener {
}
}
}
public void setStateListener(UsbDriverStateListener stateListener) {
UsbDriverService.this.stateListener = stateListener;
}
public void start() {
UsbDriverService.this.start();
}
public void stop() {
UsbDriverService.this.stop();
}
}
private void handleUsbDeviceState(UsbDevice device) {
@@ -121,15 +143,34 @@ public class UsbDriverService extends Service implements UsbDriverListener {
if (!usbManager.hasPermission(device)) {
// Let's ask for permission
try {
// Tell the state listener that we're about to display a permission dialog
if (stateListener != null) {
stateListener.onUsbPermissionPromptStarting();
}
int intentFlags = 0;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
// This PendingIntent must be mutable to allow the framework to populate EXTRA_DEVICE and EXTRA_PERMISSION_GRANTED.
intentFlags |= PendingIntent.FLAG_MUTABLE;
}
// 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));
// Use an explicit intent to activate our unexported broadcast receiver, as required on Android 14+
Intent i = new Intent(ACTION_USB_PERMISSION);
i.setPackage(getPackageName());
usbManager.requestPermission(device, PendingIntent.getBroadcast(UsbDriverService.this, 0, i, intentFlags));
} catch (SecurityException e) {
Toast.makeText(this, this.getText(R.string.error_usb_prohibited), Toast.LENGTH_LONG).show();
if (stateListener != null) {
stateListener.onUsbPermissionPromptCompleted();
}
}
return;
}
@@ -150,6 +191,9 @@ public class UsbDriverService extends Service implements UsbDriverListener {
else if (Xbox360Controller.canClaimDevice(device)) {
controller = new Xbox360Controller(device, connection, nextDeviceId++, this);
}
else if (Xbox360WirelessDongle.canClaimDevice(device)) {
controller = new Xbox360WirelessDongle(device, connection, nextDeviceId++, this);
}
else {
// Unreachable
return;
@@ -166,48 +210,95 @@ public class UsbDriverService extends Service implements UsbDriverListener {
}
}
private static boolean isRecognizedInputDevice(UsbDevice device) {
// On KitKat and later, we can determine if this VID and PID combo
// matches an existing input device and defer to the built-in controller
// support in that case. Prior to KitKat, we'll always return true to be safe.
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
for (int id : InputDevice.getDeviceIds()) {
InputDevice inputDev = InputDevice.getDevice(id);
if (inputDev == null) {
// Device was removed while looping
continue;
}
if (inputDev.getVendorId() == device.getVendorId() &&
inputDev.getProductId() == device.getProductId()) {
return true;
}
public static boolean isRecognizedInputDevice(UsbDevice device) {
// Determine if this VID and PID combo matches an existing input device
// and defer to the built-in controller support in that case.
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;
}
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) {
// We always bind to XB1 controllers but only bind to XB360 controllers
// if we know the kernel isn't already driving this device.
return XboxOneController.canClaimDevice(device) ||
((!isRecognizedInputDevice(device) || claimAllAvailable) && Xbox360Controller.canClaimDevice(device));
public static boolean kernelSupportsXbox360W() {
// Check if this kernel is 4.2+ to see if the xpad driver sets Xbox 360 wireless LEDs
// https://github.com/torvalds/linux/commit/75b7f05d2798ee3a1cc5bbdd54acd0e318a80396
String kernelVersion = System.getProperty("os.version");
if (kernelVersion != null) {
if (kernelVersion.startsWith("2.") || kernelVersion.startsWith("3.") ||
kernelVersion.startsWith("4.0.") || kernelVersion.startsWith("4.1.")) {
// Even if LED devices are present, the driver won't set the initial LED state.
return false;
}
}
// We know we have a kernel that should set Xbox 360 wireless LEDs, but we still don't
// know if CONFIG_JOYSTICK_XPAD_LEDS was enabled during the kernel build. Unfortunately
// it's not possible to detect this reliably due to Android's app sandboxing. Reading
// /proc/config.gz and enumerating /sys/class/leds are both blocked by SELinux on any
// relatively modern device. We will assume that CONFIG_JOYSTICK_XPAD_LEDS=y on these
// kernels and users can override by using the settings option to claim all devices.
return true;
}
@Override
public void onCreate() {
this.usbManager = (UsbManager) getSystemService(Context.USB_SERVICE);
this.prefConfig = PreferenceConfiguration.readPreferences(this);
public static boolean shouldClaimDevice(UsbDevice device, boolean claimAllAvailable) {
return ((!kernelSupportsXboxOne() || !isRecognizedInputDevice(device) || claimAllAvailable) && XboxOneController.canClaimDevice(device)) ||
((!isRecognizedInputDevice(device) || claimAllAvailable) && Xbox360Controller.canClaimDevice(device)) ||
// We must not call isRecognizedInputDevice() because wireless controllers don't share the same product ID as the dongle
((!kernelSupportsXbox360W() || claimAllAvailable) && Xbox360WirelessDongle.canClaimDevice(device));
}
@SuppressLint("UnspecifiedRegisterReceiverFlag")
private void start() {
if (started || usbManager == null) {
return;
}
started = true;
// 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);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
registerReceiver(receiver, filter, RECEIVER_NOT_EXPORTED);
}
else {
registerReceiver(receiver, filter);
}
// Enumerate existing devices
for (UsbDevice dev : usbManager.getDeviceList().values()) {
@@ -218,14 +309,16 @@ public class UsbDriverService extends Service implements UsbDriverListener {
}
}
@Override
public void onDestroy() {
private void stop() {
if (!started) {
return;
}
started = false;
// Stop the attachment receiver
unregisterReceiver(receiver);
// Remove listeners
listener = null;
// Stop all controllers
while (controllers.size() > 0) {
// Stop and remove the controller
@@ -233,8 +326,28 @@ public class UsbDriverService extends Service implements UsbDriverListener {
}
}
@Override
public void onCreate() {
this.usbManager = (UsbManager) getSystemService(Context.USB_SERVICE);
this.prefConfig = PreferenceConfiguration.readPreferences(this);
}
@Override
public void onDestroy() {
stop();
// Remove listeners
listener = null;
stateListener = null;
}
@Override
public IBinder onBind(Intent intent) {
return binder;
}
public interface UsbDriverStateListener {
void onUsbPermissionPromptStarting();
void onUsbPermissionPromptCompleted();
}
}

View File

@@ -26,6 +26,7 @@ public class Xbox360Controller extends AbstractXboxController {
0x0f0d, // Hori
0x1038, // SteelSeries
0x11c9, // Nacon
0x1209, // Ardwiino
0x12ab, // Unknown
0x1430, // RedOctane
0x146b, // BigBen
@@ -33,8 +34,12 @@ public class Xbox360Controller extends AbstractXboxController {
0x15e4, // Numark
0x162e, // Joytech
0x1689, // Razer Onza
0x1949, // Lab126 (Amazon Luna)
0x1bad, // Harmonix
0x20d6, // PowerA
0x24c6, // PowerA
0x2f24, // GameSir
0x2dc8, // 8BitDo
};
public static boolean canClaimDevice(UsbDevice device) {
@@ -66,8 +71,8 @@ public class Xbox360Controller extends AbstractXboxController {
@Override
protected boolean handleRead(ByteBuffer buffer) {
if (buffer.limit() < 14) {
LimeLog.severe("Read too small: "+buffer.limit());
if (buffer.remaining() < 14) {
LimeLog.severe("Read too small: "+buffer.remaining());
return false;
}
@@ -152,4 +157,9 @@ public class Xbox360Controller extends AbstractXboxController {
LimeLog.warning("Rumble transfer failed: "+res);
}
}
@Override
public void rumbleTriggers(short leftTrigger, short rightTrigger) {
// Trigger motors not present on Xbox 360 controllers
}
}

View File

@@ -0,0 +1,148 @@
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 android.os.Build;
import android.view.InputDevice;
import com.limelight.LimeLog;
import java.nio.ByteBuffer;
public class Xbox360WirelessDongle extends AbstractController {
private UsbDevice device;
private UsbDeviceConnection connection;
private static final int XB360W_IFACE_SUBCLASS = 93;
private static final int XB360W_IFACE_PROTOCOL = 129; // Wireless only
private static final int[] SUPPORTED_VENDORS = {
0x045e, // Microsoft
};
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() == XB360W_IFACE_SUBCLASS &&
device.getInterface(0).getInterfaceProtocol() == XB360W_IFACE_PROTOCOL) {
return true;
}
}
return false;
}
public Xbox360WirelessDongle(UsbDevice device, UsbDeviceConnection connection, int deviceId, UsbDriverListener listener) {
super(deviceId, listener, device.getVendorId(), device.getProductId());
this.device = device;
this.connection = connection;
}
private void sendLedCommandToEndpoint(UsbEndpoint endpoint, int controllerIndex) {
byte[] commandBuffer = {
0x00,
0x00,
0x08,
(byte) (0x40 + (2 + (controllerIndex % 4))),
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00};
int res = connection.bulkTransfer(endpoint, commandBuffer, commandBuffer.length, 3000);
if (res != commandBuffer.length) {
LimeLog.warning("LED set transfer failed: "+res);
}
}
private void sendLedCommandToInterface(UsbInterface iface, int controllerIndex) {
// Claim this interface to kick xpad off it (temporarily)
if (!connection.claimInterface(iface, true)) {
LimeLog.warning("Failed to claim interface: "+iface.getId());
return;
}
// Find the out endpoint for this interface
for (int i = 0; i < iface.getEndpointCount(); i++) {
UsbEndpoint endpt = iface.getEndpoint(i);
if (endpt.getDirection() == UsbConstants.USB_DIR_OUT) {
// Send the LED command
sendLedCommandToEndpoint(endpt, controllerIndex);
break;
}
}
// Release the interface to allow xpad to take over again
connection.releaseInterface(iface);
}
@Override
public boolean start() {
int controllerIndex = 0;
// On Android, there is a controller number associated with input devices.
// We can use this to approximate the likely controller number. This won't
// be completely accurate because there's no guarantee the order of interfaces
// matches the order that devices were enumerated by xpad, but it's probably
// better than nothing.
for (int id : InputDevice.getDeviceIds()) {
InputDevice inputDev = InputDevice.getDevice(id);
if (inputDev == null) {
// Device was removed while looping
continue;
}
// Newer xpad versions use a special product ID (0x02a1) for controllers
// rather than copying the product ID of the dongle itself.
if (inputDev.getVendorId() == device.getVendorId() &&
(inputDev.getProductId() == device.getProductId() ||
inputDev.getProductId() == 0x02a1) &&
inputDev.getControllerNumber() > 0) {
controllerIndex = inputDev.getControllerNumber() - 1;
break;
}
}
// Send LED commands on the out endpoint of each interface. There is one interface
// corresponding to each possible attached controller.
for (int i = 0; i < device.getInterfaceCount(); i++) {
UsbInterface iface = device.getInterface(i);
// Skip the non-input interfaces
if (iface.getInterfaceClass() != UsbConstants.USB_CLASS_VENDOR_SPEC ||
iface.getInterfaceSubclass() != XB360W_IFACE_SUBCLASS ||
iface.getInterfaceProtocol() != XB360W_IFACE_PROTOCOL) {
continue;
}
sendLedCommandToInterface(iface, controllerIndex++);
}
// "Fail" to give control back to the kernel driver
return false;
}
@Override
public void stop() {
// Nothing to do
}
@Override
public void rumble(short lowFreqMotor, short highFreqMotor) {
// Unreachable.
}
@Override
public void rumbleTriggers(short leftTrigger, short rightTrigger) {
// Unreachable.
}
}

View File

@@ -6,6 +6,7 @@ import android.hardware.usb.UsbDeviceConnection;
import com.limelight.LimeLog;
import com.limelight.nvstream.input.ControllerPacket;
import com.limelight.nvstream.jni.MoonBridge;
import java.nio.ByteBuffer;
import java.util.Arrays;
@@ -21,10 +22,13 @@ public class XboxOneController extends AbstractXboxController {
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};
@@ -38,6 +42,8 @@ public class XboxOneController extends AbstractXboxController {
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),
@@ -49,9 +55,14 @@ public class XboxOneController extends AbstractXboxController {
};
private byte seqNum = 0;
private short lowFreqMotor = 0;
private short highFreqMotor = 0;
private short leftTriggerMotor = 0;
private short rightTriggerMotor = 0;
public XboxOneController(UsbDevice device, UsbDeviceConnection connection, int deviceId, UsbDriverListener listener) {
super(device, connection, deviceId, listener);
capabilities |= MoonBridge.LI_CCAP_TRIGGER_RUMBLE;
}
private void processButtons(ByteBuffer buffer) {
@@ -98,11 +109,21 @@ public class XboxOneController extends AbstractXboxController {
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) {
@@ -161,12 +182,14 @@ public class XboxOneController extends AbstractXboxController {
return true;
}
@Override
public void rumble(short lowFreqMotor, short highFreqMotor) {
private void sendRumblePacket() {
byte[] data = {
0x09, 0x00, seqNum++, 0x09, 0x00,
0x0F, 0x00, 0x00,
(byte)(lowFreqMotor >> 9), (byte)(highFreqMotor >> 9),
0x0F,
(byte)(leftTriggerMotor >> 9),
(byte)(rightTriggerMotor >> 9),
(byte)(lowFreqMotor >> 9),
(byte)(highFreqMotor >> 9),
(byte)0xFF, 0x00, (byte)0xFF
};
int res = connection.bulkTransfer(outEndpt, data, data.length, 100);
@@ -175,6 +198,20 @@ public class XboxOneController extends AbstractXboxController {
}
}
@Override
public void rumble(short lowFreqMotor, short highFreqMotor) {
this.lowFreqMotor = lowFreqMotor;
this.highFreqMotor = highFreqMotor;
sendRumblePacket();
}
@Override
public void rumbleTriggers(short leftTrigger, short rightTrigger) {
this.leftTriggerMotor = leftTrigger;
this.rightTriggerMotor = rightTrigger;
sendRumblePacket();
}
private static class InitPacket {
final int vendorId;
final int productId;

View File

@@ -3,12 +3,12 @@ package com.limelight.binding.input.evdev;
import android.app.Activity;
import com.limelight.LimelightBuildProps;
import com.limelight.BuildConfig;
import com.limelight.binding.input.capture.InputCaptureProvider;
public class EvdevCaptureProviderShim {
public static boolean isCaptureProviderSupported() {
return LimelightBuildProps.ROOT_BUILD;
return BuildConfig.ROOT_BUILD;
}
// We need to construct our capture provider using reflection because it isn't included in non-root builds

View File

@@ -9,6 +9,7 @@ public interface EvdevListener {
void mouseMove(int deltaX, int deltaY);
void mouseButtonEvent(int buttonId, boolean down);
void mouseScroll(byte amount);
void mouseVScroll(byte amount);
void mouseHScroll(byte amount);
void keyboardEvent(boolean buttonDown, short keyCode);
}

View File

@@ -0,0 +1,249 @@
package com.limelight.binding.input.touch;
import android.os.Handler;
import android.os.Looper;
import android.view.View;
import com.limelight.nvstream.NvConnection;
import com.limelight.nvstream.input.MouseButtonPacket;
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 final Runnable longPressRunnable = new Runnable() {
@Override
public void run() {
// 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);
}
};
private final Runnable tapDownRunnable = new Runnable() {
@Override
public void run() {
// Start our tap
tapConfirmed();
}
};
private final NvConnection conn;
private final int actionIndex;
private final View targetView;
private final Handler handler;
private final Runnable leftButtonUpRunnable = new Runnable() {
@Override
public void run() {
conn.sendMouseButtonUp(MouseButtonPacket.BUTTON_LEFT);
}
};
private static final int SCROLL_SPEED_FACTOR = 3;
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;
this.handler = new Handler(Looper.getMainLooper());
}
@Override
public int getActionIndex()
{
return actionIndex;
}
@Override
public boolean touchDownEvent(int eventX, int eventY, long eventTime, boolean isNewFinger)
{
if (!isNewFinger) {
// We don't handle finger transitions for absolute mode
return true;
}
lastTouchLocationX = lastTouchDownX = eventX;
lastTouchLocationY = lastTouchDownY = eventY;
lastTouchDownTime = eventTime;
cancelled = confirmedTap = confirmedLongPress = false;
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, long eventTime)
{
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();
// Release the left mouse button in 100ms to allow for apps that use polling
// to detect mouse button presses.
handler.removeCallbacks(leftButtonUpRunnable);
handler.postDelayed(leftButtonUpRunnable, 100);
}
}
lastTouchLocationX = lastTouchUpX = eventX;
lastTouchLocationY = lastTouchUpY = eventY;
lastTouchUpTime = eventTime;
}
private void startLongPressTimer() {
cancelLongPressTimer();
handler.postDelayed(longPressRunnable, LONG_PRESS_TIME_THRESHOLD);
}
private void cancelLongPressTimer() {
handler.removeCallbacks(longPressRunnable);
}
private void startTapDownTimer() {
cancelTapDownTimer();
handler.postDelayed(tapDownRunnable, TOUCH_DOWN_DEAD_ZONE_TIME_THRESHOLD);
}
private void cancelTapDownTimer() {
handler.removeCallbacks(tapDownRunnable);
}
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, long eventTime)
{
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) {
conn.sendMouseHighResScroll((short)((eventY - lastTouchLocationY) * SCROLL_SPEED_FACTOR));
}
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,331 @@
package com.limelight.binding.input.touch;
import android.os.Handler;
import android.os.Looper;
import android.view.View;
import com.limelight.nvstream.NvConnection;
import com.limelight.nvstream.input.MouseButtonPacket;
import com.limelight.preferences.PreferenceConfiguration;
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 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 final PreferenceConfiguration prefConfig;
private final Handler handler;
private final Runnable dragTimerRunnable = new Runnable() {
@Override
public void run() {
// Check if someone already set move
if (confirmedMove) {
return;
}
// The drag should only be processed for the primary finger
if (actionIndex != maxPointerCountInGesture - 1) {
return;
}
// We haven't been cancelled before the timer expired so begin dragging
confirmedDrag = true;
conn.sendMouseButtonDown(getMouseButtonIndex());
}
};
// Indexed by MouseButtonPacket.BUTTON_XXX - 1
private final Runnable[] buttonUpRunnables = new Runnable[] {
new Runnable() {
@Override
public void run() {
conn.sendMouseButtonUp(MouseButtonPacket.BUTTON_LEFT);
}
},
new Runnable() {
@Override
public void run() {
conn.sendMouseButtonUp(MouseButtonPacket.BUTTON_MIDDLE);
}
},
new Runnable() {
@Override
public void run() {
conn.sendMouseButtonUp(MouseButtonPacket.BUTTON_RIGHT);
}
},
new Runnable() {
@Override
public void run() {
conn.sendMouseButtonUp(MouseButtonPacket.BUTTON_X1);
}
},
new Runnable() {
@Override
public void run() {
conn.sendMouseButtonUp(MouseButtonPacket.BUTTON_X2);
}
}
};
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_FACTOR = 5;
public RelativeTouchContext(NvConnection conn, int actionIndex,
int referenceWidth, int referenceHeight,
View view, PreferenceConfiguration prefConfig)
{
this.conn = conn;
this.actionIndex = actionIndex;
this.referenceWidth = referenceWidth;
this.referenceHeight = referenceHeight;
this.targetView = view;
this.prefConfig = prefConfig;
this.handler = new Handler(Looper.getMainLooper());
}
@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(long eventTime)
{
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 = eventTime - 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, long eventTime, 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 = eventTime;
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, long eventTime)
{
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(eventTime))
{
// Lower the mouse button
conn.sendMouseButtonDown(buttonIndex);
// Release the mouse button in 100ms to allow for apps that use polling
// to detect mouse button presses.
Runnable buttonUpRunnable = buttonUpRunnables[buttonIndex - 1];
handler.removeCallbacks(buttonUpRunnable);
handler.postDelayed(buttonUpRunnable, 100);
}
}
private void startDragTimer() {
cancelDragTimer();
handler.postDelayed(dragTimerRunnable, DRAG_TIME_THRESHOLD);
}
private void cancelDragTimer() {
handler.removeCallbacks(dragTimerRunnable);
}
private 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, long eventTime)
{
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) {
conn.sendMouseHighResScroll((short)(deltaY * SCROLL_SPEED_FACTOR));
}
} else {
if (prefConfig.absoluteMouseMode) {
conn.sendMouseMoveAsMousePosition(
(short) deltaX,
(short) deltaY,
(short) targetView.getWidth(),
(short) targetView.getHeight());
}
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, long eventTime, boolean isNewFinger);
boolean touchMoveEvent(int eventX, int eventY, long eventTime);
void touchUpEvent(int eventX, int eventY, long eventTime);
void cancelTouch();
boolean isCancelled();
}

View File

@@ -210,7 +210,7 @@ public class AnalogStick extends VirtualControllerElement {
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
// calculate new radius sizes depending
radius_complete = getPercent(getCorrectWidth() / 2, 100);
radius_complete = getPercent(getCorrectWidth() / 2, 100) - 2 * getDefaultStrokeWidth();
radius_dead_zone = getPercent(getCorrectWidth() / 2, 30);
radius_analog_stick = getPercent(getCorrectWidth() / 2, 20);
@@ -253,7 +253,7 @@ public class AnalogStick extends VirtualControllerElement {
}
}
private void updatePosition() {
private void updatePosition(long eventTime) {
// get 100% way
float complete = radius_complete - radius_analog_stick;
@@ -270,7 +270,7 @@ public class AnalogStick extends VirtualControllerElement {
// We also release the deadzone if the user keeps the stick pressed for a bit to allow
// them to make precise movements.
stick_state = (stick_state == STICK_STATE.MOVED_ACTIVE ||
System.currentTimeMillis() - timeLastClick > timeoutDeadzone ||
eventTime - timeLastClick > timeoutDeadzone ||
movement_radius > radius_dead_zone) ?
STICK_STATE.MOVED_ACTIVE : STICK_STATE.MOVED_IN_DEAD_ZONE;
@@ -293,25 +293,24 @@ public class AnalogStick extends VirtualControllerElement {
movement_radius = getMovementRadius(relative_x, relative_y);
movement_angle = getAngle(relative_x, relative_y);
// chop radius if out of outer circle and already pressed
// 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)) {
// not pressed already, so ignore event from outer circle
if (!isPressed()) {
return false;
}
movement_radius = radius_complete - radius_analog_stick;
}
// handle event depending on action
switch (event.getActionMasked()) {
// down event (touch event)
case MotionEvent.ACTION_DOWN:
case MotionEvent.ACTION_POINTER_DOWN: {
case MotionEvent.ACTION_DOWN: {
// set to dead zoned, will be corrected in update position if necessary
stick_state = STICK_STATE.MOVED_IN_DEAD_ZONE;
// check for double click
if (lastClickState == CLICK_STATE.SINGLE &&
timeLastClick + timeoutDoubleClick > System.currentTimeMillis()) {
event.getEventTime() - timeLastClick <= timeoutDoubleClick) {
click_state = CLICK_STATE.DOUBLE;
notifyOnDoubleClick();
} else {
@@ -319,14 +318,14 @@ public class AnalogStick extends VirtualControllerElement {
notifyOnClick();
}
// reset last click timestamp
timeLastClick = System.currentTimeMillis();
timeLastClick = event.getEventTime();
// set item pressed and update
setPressed(true);
break;
}
// up event (revoke touch)
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_POINTER_UP: {
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP: {
setPressed(false);
break;
}
@@ -334,7 +333,7 @@ public class AnalogStick extends VirtualControllerElement {
if (isPressed()) {
// when is pressed calculate new positions (will trigger movement if necessary)
updatePosition();
updatePosition(event.getEventTime());
} else {
stick_state = STICK_STATE.NO_MOVEMENT;
notifyOnRevoke();

View File

@@ -8,13 +8,12 @@ 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.
@@ -42,24 +41,19 @@ public class DigitalButton extends VirtualControllerElement {
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 Runnable longClickRunnable = new Runnable() {
@Override
public void run() {
onLongClickCallback();
}
};
private final Paint paint = new Paint();
private final RectF rect = new RectF();
private int layer;
private DigitalButton movingButton = null;
@@ -144,14 +138,18 @@ public class DigitalButton extends VirtualControllerElement {
// set transparent background
canvas.drawColor(Color.TRANSPARENT);
paint.setTextSize(getPercent(getWidth(), 30));
paint.setTextSize(getPercent(getWidth(), 25));
paint.setTextAlign(Paint.Align.CENTER);
paint.setStrokeWidth(getDefaultStrokeWidth());
paint.setColor(isPressed() ? pressedColor : getDefaultColor());
paint.setStyle(Paint.Style.STROKE);
canvas.drawRect(paint.getStrokeWidth(), paint.getStrokeWidth(),
getWidth() - paint.getStrokeWidth(), getHeight() - paint.getStrokeWidth(), paint);
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);
@@ -171,9 +169,8 @@ public class DigitalButton extends VirtualControllerElement {
listener.onClick();
}
timerLongClick = new Timer();
longClickTimerTask = new TimerLongClickTimerTask();
timerLongClick.schedule(longClickTimerTask, timerLongClickTimeout);
virtualController.getHandler().removeCallbacks(longClickRunnable);
virtualController.getHandler().postDelayed(longClickRunnable, timerLongClickTimeout);
}
private void onLongClickCallback() {
@@ -192,12 +189,7 @@ public class DigitalButton extends VirtualControllerElement {
}
// We may be called for a release without a prior click
if (timerLongClick != null) {
timerLongClick.cancel();
}
if (longClickTimerTask != null) {
longClickTimerTask.cancel();
}
virtualController.getHandler().removeCallbacks(longClickRunnable);
}
@Override
@@ -208,8 +200,7 @@ public class DigitalButton extends VirtualControllerElement {
int action = event.getActionMasked();
switch (action) {
case MotionEvent.ACTION_DOWN:
case MotionEvent.ACTION_POINTER_DOWN: {
case MotionEvent.ACTION_DOWN: {
movingButton = null;
setPressed(true);
onClickCallback();
@@ -224,8 +215,7 @@ public class DigitalButton extends VirtualControllerElement {
return true;
}
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_POINTER_UP: {
case MotionEvent.ACTION_UP: {
setPressed(false);
onReleaseCallback();

View File

@@ -162,7 +162,6 @@ public class DigitalPad extends VirtualControllerElement {
// 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;
@@ -184,8 +183,7 @@ public class DigitalPad extends VirtualControllerElement {
return true;
}
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_POINTER_UP: {
case MotionEvent.ACTION_UP: {
direction = 0;
newDirectionCallback(direction);
invalidate();

View File

@@ -5,24 +5,23 @@
package com.limelight.binding.input.virtual_controller;
import android.content.Context;
import android.os.Handler;
import android.os.Looper;
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.LimeLog;
import com.limelight.R;
import com.limelight.binding.input.ControllerHandler;
import com.limelight.nvstream.NvConnection;
import java.util.ArrayList;
import java.util.List;
import java.util.Timer;
import java.util.TimerTask;
public class VirtualController {
public class ControllerInputContext {
public static class ControllerInputContext {
public short inputMap = 0x0000;
public byte leftTrigger = 0x00;
public byte rightTrigger = 0x00;
@@ -34,18 +33,24 @@ public class VirtualController {
public enum ControllerMode {
Active,
Configuration
MoveButtons,
ResizeButtons
}
private static final boolean _PRINT_DEBUG_INFORMATION = false;
private ControllerHandler controllerHandler;
private Context context = null;
private final ControllerHandler controllerHandler;
private final Context context;
private final Handler handler;
private final Runnable delayedRetransmitRunnable = new Runnable() {
@Override
public void run() {
sendControllerInputContextInternal();
}
};
private FrameLayout frame_layout = null;
private RelativeLayout relative_layout = null;
private Timer retransmitTimer;
ControllerMode currentMode = ControllerMode.Active;
ControllerInputContext inputContext = new ControllerInputContext();
@@ -58,10 +63,7 @@ public class VirtualController {
this.controllerHandler = controllerHandler;
this.frame_layout = layout;
this.context = context;
relative_layout = new RelativeLayout(context);
frame_layout.addView(relative_layout);
this.handler = new Handler(Looper.getMainLooper());
buttonConfigure = new Button(context);
buttonConfigure.setAlpha(0.25f);
@@ -72,60 +74,72 @@ public class VirtualController {
public void onClick(View v) {
String message;
if (currentMode == ControllerMode.Configuration) {
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";
} else {
currentMode = ControllerMode.Configuration;
message = "Entering configuration mode";
}
Toast.makeText(context, message, Toast.LENGTH_SHORT).show();
relative_layout.invalidate();
buttonConfigure.invalidate();
for (VirtualControllerElement element : elements) {
element.invalidate();
}
}
});
}
Handler getHandler() {
return handler;
}
public void hide() {
retransmitTimer.cancel();
relative_layout.setVisibility(View.INVISIBLE);
for (VirtualControllerElement element : elements) {
element.setVisibility(View.INVISIBLE);
}
buttonConfigure.setVisibility(View.INVISIBLE);
}
public void show() {
relative_layout.setVisibility(View.VISIBLE);
for (VirtualControllerElement element : elements) {
element.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);
buttonConfigure.setVisibility(View.VISIBLE);
}
public void removeElements() {
for (VirtualControllerElement element : elements) {
relative_layout.removeView(element);
frame_layout.removeView(element);
}
elements.clear();
frame_layout.removeView(buttonConfigure);
}
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);
FrameLayout.LayoutParams layoutParams = new FrameLayout.LayoutParams(width, height);
layoutParams.setMargins(x, y, 0, 0);
relative_layout.addView(element, layoutParams);
frame_layout.addView(element, layoutParams);
}
public List<VirtualControllerElement> getElements() {
@@ -134,23 +148,20 @@ public class VirtualController {
private static final void _DBG(String text) {
if (_PRINT_DEBUG_INFORMATION) {
System.out.println("VirtualController: " + text);
LimeLog.info("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);
FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(buttonSize, buttonSize);
params.leftMargin = 15;
params.topMargin = 15;
relative_layout.addView(buttonConfigure, params);
frame_layout.addView(buttonConfigure, params);
// Start with the default layout
VirtualControllerConfigurationLoader.createDefaultLayout(this, context);
@@ -167,7 +178,7 @@ public class VirtualController {
return inputContext;
}
void sendControllerInputContext() {
private void sendControllerInputContextInternal() {
_DBG("INPUT_MAP + " + inputContext.inputMap);
_DBG("LEFT_TRIGGER " + inputContext.leftTrigger);
_DBG("RIGHT_TRIGGER " + inputContext.rightTrigger);
@@ -186,4 +197,19 @@ public class VirtualController {
);
}
}
void sendControllerInputContext() {
// Cancel retransmissions of prior gamepad inputs
handler.removeCallbacks(delayedRetransmitRunnable);
sendControllerInputContextInternal();
// 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 retransmit
// the gamepad state a few times unless another input event happens before then.
handler.postDelayed(delayedRetransmitRunnable, 25);
handler.postDelayed(delayedRetransmitRunnable, 50);
handler.postDelayed(delayedRetransmitRunnable, 75);
}
}

View File

@@ -24,6 +24,11 @@ public class VirtualControllerConfigurationLoader {
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) {
@@ -35,27 +40,31 @@ public class VirtualControllerConfigurationLoader {
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) {
if ((direction & DigitalPad.DIGITAL_PAD_DIRECTION_LEFT) != 0) {
inputContext.inputMap |= ControllerPacket.LEFT_FLAG;
}
if ((direction & DigitalPad.DIGITAL_PAD_DIRECTION_RIGHT) > 0) {
else {
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) {
else {
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) {
else {
inputContext.inputMap &= ~ControllerPacket.UP_FLAG;
}
if ((direction & DigitalPad.DIGITAL_PAD_DIRECTION_DOWN) != 0) {
inputContext.inputMap |= ControllerPacket.DOWN_FLAG;
}
else {
inputContext.inputMap &= ~ControllerPacket.DOWN_FLAG;
}
controller.sendControllerInputContext();
}
});
@@ -145,149 +154,200 @@ public class VirtualControllerConfigurationLoader {
return new RightAnalogStick(controller, context);
}
private static final int BUTTON_BASE_X = 65;
private static final int BUTTON_BASE_Y = 5;
private static final int BUTTON_WIDTH = getPercent(30, 33);
private static final int BUTTON_HEIGHT = getPercent(40, 33);
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;
// Make the Guide Menu be in the center of START and BACK menu
private static final int GUIDE_X = START_X-BACK_X;
private static final int GUIDE_Y = START_BACK_Y;
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),
getPercent(5, screen.widthPixels),
getPercent(BUTTON_BASE_Y, screen.heightPixels),
getPercent(30, screen.widthPixels),
getPercent(40, screen.heightPixels)
screenScale(DPAD_BASE_X, height),
screenScale(DPAD_BASE_Y, height),
screenScale(DPAD_SIZE, height),
screenScale(DPAD_SIZE, height)
);
controller.addElement(createDigitalButton(
VirtualControllerElement.EID_A,
ControllerPacket.A_FLAG, 0, 1, "A", -1, controller, context),
getPercent(BUTTON_BASE_X, screen.widthPixels) + getPercent(BUTTON_WIDTH, screen.widthPixels),
getPercent(BUTTON_BASE_Y, screen.heightPixels) + 2 * getPercent(BUTTON_HEIGHT, screen.heightPixels),
getPercent(BUTTON_WIDTH, screen.widthPixels),
getPercent(BUTTON_HEIGHT, screen.heightPixels)
!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,
ControllerPacket.B_FLAG, 0, 1, "B", -1, controller, context),
getPercent(BUTTON_BASE_X, screen.widthPixels) + 2 * getPercent(BUTTON_WIDTH, screen.widthPixels),
getPercent(BUTTON_BASE_Y, screen.heightPixels) + getPercent(BUTTON_HEIGHT, screen.heightPixels),
getPercent(BUTTON_WIDTH, screen.widthPixels),
getPercent(BUTTON_HEIGHT, screen.heightPixels)
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,
ControllerPacket.X_FLAG, 0, 1, "X", -1, controller, context),
getPercent(BUTTON_BASE_X, screen.widthPixels),
getPercent(BUTTON_BASE_Y, screen.heightPixels) + getPercent(BUTTON_HEIGHT, screen.heightPixels),
getPercent(BUTTON_WIDTH, screen.widthPixels),
getPercent(BUTTON_HEIGHT, screen.heightPixels)
!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,
ControllerPacket.Y_FLAG, 0, 1, "Y", -1, controller, context),
getPercent(BUTTON_BASE_X, screen.widthPixels) + getPercent(BUTTON_WIDTH, screen.widthPixels),
getPercent(BUTTON_BASE_Y, screen.heightPixels),
getPercent(BUTTON_WIDTH, screen.widthPixels),
getPercent(BUTTON_HEIGHT, screen.heightPixels)
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(
0, "LT", -1, controller, context),
getPercent(BUTTON_BASE_X, screen.widthPixels),
getPercent(BUTTON_BASE_Y, screen.heightPixels),
getPercent(BUTTON_WIDTH, screen.widthPixels),
getPercent(BUTTON_HEIGHT, screen.heightPixels)
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(
0, "RT", -1, controller, context),
getPercent(BUTTON_BASE_X, screen.widthPixels) + 2 * getPercent(BUTTON_WIDTH, screen.widthPixels),
getPercent(BUTTON_BASE_Y, screen.heightPixels),
getPercent(BUTTON_WIDTH, screen.widthPixels),
getPercent(BUTTON_HEIGHT, screen.heightPixels)
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),
getPercent(BUTTON_BASE_X, screen.widthPixels),
getPercent(BUTTON_BASE_Y, screen.heightPixels) + 2 * getPercent(BUTTON_HEIGHT, screen.heightPixels),
getPercent(BUTTON_WIDTH, screen.widthPixels),
getPercent(BUTTON_HEIGHT, screen.heightPixels)
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),
getPercent(BUTTON_BASE_X, screen.widthPixels) + 2 * getPercent(BUTTON_WIDTH, screen.widthPixels),
getPercent(BUTTON_BASE_Y, screen.heightPixels) + 2 * getPercent(BUTTON_HEIGHT, screen.heightPixels),
getPercent(BUTTON_WIDTH, screen.widthPixels),
getPercent(BUTTON_HEIGHT, screen.heightPixels)
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),
getPercent(5, screen.widthPixels),
getPercent(50, screen.heightPixels),
getPercent(40, screen.widthPixels),
getPercent(50, screen.heightPixels)
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),
getPercent(55, screen.widthPixels),
getPercent(50, screen.heightPixels),
getPercent(40, screen.widthPixels),
getPercent(50, screen.heightPixels)
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),
getPercent(40, screen.widthPixels),
getPercent(90, screen.heightPixels),
getPercent(10, screen.widthPixels),
getPercent(10, screen.heightPixels)
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),
getPercent(40, screen.widthPixels) + getPercent(10, screen.widthPixels),
getPercent(90, screen.heightPixels),
getPercent(10, screen.widthPixels),
getPercent(10, screen.heightPixels)
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),
getPercent(2, screen.widthPixels),
getPercent(80, screen.heightPixels),
getPercent(BUTTON_WIDTH, screen.widthPixels),
getPercent(BUTTON_HEIGHT, screen.heightPixels)
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),
getPercent(89, screen.widthPixels),
getPercent(80, screen.heightPixels),
getPercent(BUTTON_WIDTH, screen.widthPixels),
getPercent(BUTTON_HEIGHT, screen.heightPixels)
screenScale(TRIGGER_R_BASE_X + TRIGGER_DISTANCE, height) + rightDisplacement,
screenScale(L3_R3_BASE_Y, height),
screenScale(TRIGGER_WIDTH, height),
screenScale(TRIGGER_HEIGHT, height)
);
}
if(config.showGuideButton){
controller.addElement(createDigitalButton(VirtualControllerElement.EID_GDB,
ControllerPacket.SPECIAL_BUTTON_FLAG, 0, 1, "GUIDE", -1, controller, context),
screenScale(GUIDE_X, height)+ rightDisplacement,
screenScale(GUIDE_Y, height),
screenScale(START_BACK_WIDTH, height),
screenScale(START_BACK_HEIGHT, height)
);
}
controller.setOpacity(config.oscOpacity);
}
public static void saveProfile(final VirtualController controller,
@@ -306,7 +366,7 @@ public class VirtualControllerConfigurationLoader {
prefEditor.apply();
}
public static void loadFromPreferences(final VirtualController controller, final Context context) {
public static void loadFromPreferences(final VirtualController controller, final Context context) {
SharedPreferences pref = context.getSharedPreferences(OSC_PREFERENCE, Activity.MODE_PRIVATE);
for (VirtualControllerElement element : controller.getElements()) {
@@ -324,5 +384,5 @@ public class VirtualControllerConfigurationLoader {
}
}
}
}
}
}
}

View File

@@ -12,7 +12,7 @@ import android.graphics.Paint;
import android.util.DisplayMetrics;
import android.view.MotionEvent;
import android.view.View;
import android.widget.RelativeLayout;
import android.widget.FrameLayout;
import org.json.JSONException;
import org.json.JSONObject;
@@ -35,6 +35,7 @@ public abstract class VirtualControllerElement extends View {
public static final int EID_RS = 13;
public static final int EID_LSB = 14;
public static final int EID_RSB = 15;
public static final int EID_GDB = 16;
protected VirtualController virtualController;
protected final int elementId;
@@ -43,7 +44,8 @@ public abstract class VirtualControllerElement extends View {
private int normalColor = 0xF0888888;
protected int pressedColor = 0xF00000FF;
private int configNormalColor = 0xF0FF0000;
private int configMoveColor = 0xF0FF0000;
private int configResizeColor = 0xF0FF00FF;
private int configSelectedColor = 0xF000FF00;
protected int startSize_x;
@@ -71,7 +73,7 @@ public abstract class VirtualControllerElement extends View {
int newPos_x = (int) getX() + x - pressed_x;
int newPos_y = (int) getY() + y - pressed_y;
RelativeLayout.LayoutParams layoutParams = (RelativeLayout.LayoutParams) getLayoutParams();
FrameLayout.LayoutParams layoutParams = (FrameLayout.LayoutParams) getLayoutParams();
layoutParams.leftMargin = newPos_x > 0 ? newPos_x : 0;
layoutParams.topMargin = newPos_y > 0 ? newPos_y : 0;
@@ -82,7 +84,7 @@ public abstract class VirtualControllerElement extends View {
}
protected void resizeElement(int pressed_x, int pressed_y, int width, int height) {
RelativeLayout.LayoutParams layoutParams = (RelativeLayout.LayoutParams) getLayoutParams();
FrameLayout.LayoutParams layoutParams = (FrameLayout.LayoutParams) getLayoutParams();
int newHeight = height + (startSize_y - pressed_y);
int newWidth = width + (startSize_x - pressed_x);
@@ -112,34 +114,34 @@ public abstract class VirtualControllerElement extends View {
/*
protected void actionShowNormalColorChooser() {
AmbilWarnaDialog colorDialog = new AmbilWarnaDialog(getContext(), normalColor, true, new AmbilWarnaDialog.OnAmbilWarnaListener() {
@Override
public void onCancel(AmbilWarnaDialog dialog)
{}
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();
}
@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) {
}
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();
}
@Override
public void onOk(AmbilWarnaDialog dialog, int color) {
pressedColor = color;
invalidate();
}
});
colorDialog.show();
}
*/
protected void actionEnableMove() {
@@ -156,8 +158,12 @@ public abstract class VirtualControllerElement extends View {
}
protected int getDefaultColor() {
return (virtualController.getControllerMode() == VirtualController.ControllerMode.Configuration) ?
configNormalColor : normalColor;
if (virtualController.getControllerMode() == VirtualController.ControllerMode.MoveButtons)
return configMoveColor;
else if (virtualController.getControllerMode() == VirtualController.ControllerMode.ResizeButtons)
return configResizeColor;
else
return normalColor;
}
protected int getDefaultStrokeWidth() {
@@ -195,11 +201,11 @@ public abstract class VirtualControllerElement extends View {
break;
}
/*
case 2: { // set default color
case 2: { // set default color
actionShowNormalColorChooser();
break;
}
case 3: { // set pressed color
case 3: { // set pressed color
actionShowPressedColorChooser();
break;
}
@@ -218,19 +224,30 @@ public abstract class VirtualControllerElement extends View {
@Override
public boolean onTouchEvent(MotionEvent event) {
// Ignore secondary touches on controls
//
// NB: We can get an additional pointer down if the user touches a non-StreamView area
// while also touching an OSC control, even if that pointer down doesn't correspond to
// an area of the OSC control.
if (event.getActionIndex() != 0) {
return true;
}
if (virtualController.getControllerMode() == VirtualController.ControllerMode.Active) {
return onElementTouchEvent(event);
}
switch (event.getActionMasked()) {
case MotionEvent.ACTION_DOWN:
case MotionEvent.ACTION_POINTER_DOWN: {
case MotionEvent.ACTION_DOWN: {
position_pressed_x = event.getX();
position_pressed_y = event.getY();
startSize_x = getWidth();
startSize_y = getHeight();
actionEnableMove();
if (virtualController.getControllerMode() == VirtualController.ControllerMode.MoveButtons)
actionEnableMove();
else if (virtualController.getControllerMode() == VirtualController.ControllerMode.ResizeButtons)
actionEnableResize();
return true;
}
@@ -259,8 +276,7 @@ public abstract class VirtualControllerElement extends View {
return true;
}
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_POINTER_UP: {
case MotionEvent.ACTION_UP: {
actionCancel();
return true;
}
@@ -287,6 +303,15 @@ public abstract class VirtualControllerElement extends View {
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;
}
@@ -299,7 +324,7 @@ public abstract class VirtualControllerElement extends View {
public JSONObject getConfiguration() throws JSONException {
JSONObject configuration = new JSONObject();
RelativeLayout.LayoutParams layoutParams = (RelativeLayout.LayoutParams) getLayoutParams();
FrameLayout.LayoutParams layoutParams = (FrameLayout.LayoutParams) getLayoutParams();
configuration.put("LEFT", layoutParams.leftMargin);
configuration.put("TOP", layoutParams.topMargin);
@@ -310,7 +335,7 @@ public abstract class VirtualControllerElement extends View {
}
public void loadConfiguration(JSONObject configuration) throws JSONException {
RelativeLayout.LayoutParams layoutParams = (RelativeLayout.LayoutParams) getLayoutParams();
FrameLayout.LayoutParams layoutParams = (FrameLayout.LayoutParams) getLayoutParams();
layoutParams.leftMargin = configuration.getInt("LEFT");
layoutParams.topMargin = configuration.getInt("TOP");

View File

@@ -1,5 +1,7 @@
package com.limelight.binding.video;
import android.os.SystemClock;
class VideoStats {
long decoderTimeMs;
@@ -9,6 +11,10 @@ class VideoStats {
int totalFramesRendered;
int frameLossEvents;
int framesLost;
char minHostProcessingLatency;
char maxHostProcessingLatency;
int totalHostProcessingLatency;
int framesWithHostProcessingLatency;
long measurementStartTimestamp;
void add(VideoStats other) {
@@ -20,11 +26,20 @@ class VideoStats {
this.frameLossEvents += other.frameLossEvents;
this.framesLost += other.framesLost;
if (this.minHostProcessingLatency == 0) {
this.minHostProcessingLatency = other.minHostProcessingLatency;
} else {
this.minHostProcessingLatency = (char) Math.min(this.minHostProcessingLatency, other.minHostProcessingLatency);
}
this.maxHostProcessingLatency = (char) Math.max(this.maxHostProcessingLatency, other.maxHostProcessingLatency);
this.totalHostProcessingLatency += other.totalHostProcessingLatency;
this.framesWithHostProcessingLatency += other.framesWithHostProcessingLatency;
if (this.measurementStartTimestamp == 0) {
this.measurementStartTimestamp = other.measurementStartTimestamp;
}
assert other.measurementStartTimestamp <= this.measurementStartTimestamp;
assert other.measurementStartTimestamp >= this.measurementStartTimestamp;
}
void copy(VideoStats other) {
@@ -35,6 +50,10 @@ class VideoStats {
this.totalFramesRendered = other.totalFramesRendered;
this.frameLossEvents = other.frameLossEvents;
this.framesLost = other.framesLost;
this.minHostProcessingLatency = other.minHostProcessingLatency;
this.maxHostProcessingLatency = other.maxHostProcessingLatency;
this.totalHostProcessingLatency = other.totalHostProcessingLatency;
this.framesWithHostProcessingLatency = other.framesWithHostProcessingLatency;
this.measurementStartTimestamp = other.measurementStartTimestamp;
}
@@ -46,11 +65,15 @@ class VideoStats {
this.totalFramesRendered = 0;
this.frameLossEvents = 0;
this.framesLost = 0;
this.minHostProcessingLatency = 0;
this.maxHostProcessingLatency = 0;
this.totalHostProcessingLatency = 0;
this.framesWithHostProcessingLatency = 0;
this.measurementStartTimestamp = 0;
}
VideoStatsFps getFps() {
float elapsed = (System.currentTimeMillis() - this.measurementStartTimestamp) / (float) 1000;
float elapsed = (SystemClock.uptimeMillis() - this.measurementStartTimestamp) / (float) 1000;
VideoStatsFps fps = new VideoStatsFps();
if (elapsed > 0) {

View File

@@ -9,7 +9,9 @@ import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
import com.limelight.LimeLog;
import com.limelight.nvstream.http.ComputerDetails;
import com.limelight.nvstream.http.NvHTTP;
import android.content.ContentValues;
import android.content.Context;
@@ -17,17 +19,28 @@ import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteException;
import org.json.JSONException;
import org.json.JSONObject;
public class ComputerDatabaseManager {
private static final String COMPUTER_DB_NAME = "computers3.db";
private static final String COMPUTER_DB_NAME = "computers4.db";
private static final String COMPUTER_TABLE_NAME = "Computers";
private static final String COMPUTER_UUID_COLUMN_NAME = "UUID";
private static final String COMPUTER_NAME_COLUMN_NAME = "ComputerName";
private static final String ADDRESSES_COLUMN_NAME = "Addresses";
private interface AddressFields {
String LOCAL = "local";
String REMOTE = "remote";
String MANUAL = "manual";
String IPv6 = "ipv6";
String ADDRESS = "address";
String PORT = "port";
}
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;
public ComputerDatabaseManager(Context c) {
@@ -62,24 +75,54 @@ public class ComputerDatabaseManager {
for (ComputerDetails computer : oldComputers) {
updateComputer(computer);
}
oldComputers = LegacyDatabaseReader3.migrateAllComputers(c);
for (ComputerDetails computer : oldComputers) {
updateComputer(computer);
}
}
public void deleteComputer(ComputerDetails details) {
computerDb.delete(COMPUTER_TABLE_NAME, COMPUTER_UUID_COLUMN_NAME+"=?", new String[]{details.uuid});
}
public static JSONObject tupleToJson(ComputerDetails.AddressTuple tuple) throws JSONException {
if (tuple == null) {
return null;
}
JSONObject json = new JSONObject();
json.put(AddressFields.ADDRESS, tuple.address);
json.put(AddressFields.PORT, tuple.port);
return json;
}
public static ComputerDetails.AddressTuple tupleFromJson(JSONObject json, String name) throws JSONException {
if (!json.has(name)) {
return null;
}
JSONObject address = json.getJSONObject(name);
return new ComputerDetails.AddressTuple(
address.getString(AddressFields.ADDRESS), address.getInt(AddressFields.PORT));
}
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);
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 : "");
try {
JSONObject addresses = new JSONObject();
addresses.put(AddressFields.LOCAL, tupleToJson(details.localAddress));
addresses.put(AddressFields.REMOTE, tupleToJson(details.remoteAddress));
addresses.put(AddressFields.MANUAL, tupleToJson(details.manualAddress));
addresses.put(AddressFields.IPv6, tupleToJson(details.ipv6Address));
values.put(ADDRESSES_COLUMN_NAME, addresses.toString());
} catch (JSONException e) {
throw new RuntimeException(e);
}
values.put(ADDRESSES_COLUMN_NAME, addresses.toString());
values.put(MAC_ADDRESS_COLUMN_NAME, details.macAddress);
try {
if (details.serverCert != null) {
@@ -95,26 +138,28 @@ public class ComputerDatabaseManager {
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);
try {
JSONObject addresses = new JSONObject(c.getString(2));
details.localAddress = tupleFromJson(addresses, AddressFields.LOCAL);
details.remoteAddress = tupleFromJson(addresses, AddressFields.REMOTE);
details.manualAddress = tupleFromJson(addresses, AddressFields.MANUAL);
details.ipv6Address = tupleFromJson(addresses, AddressFields.IPv6);
} catch (JSONException e) {
throw new RuntimeException(e);
}
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]);
// External port is persisted in the remote address field
if (details.remoteAddress != null) {
details.externalPort = details.remoteAddress.port;
}
else {
details.externalPort = NvHTTP.DEFAULT_HTTP_PORT;
}
details.macAddress = c.getString(3);
@@ -136,28 +181,55 @@ public class ComputerDatabaseManager {
}
public List<ComputerDetails> getAllComputers() {
Cursor c = computerDb.rawQuery("SELECT * FROM "+COMPUTER_TABLE_NAME, null);
LinkedList<ComputerDetails> computerList = new LinkedList<>();
while (c.moveToNext()) {
computerList.add(getComputerFromCursor(c));
try (final Cursor c = computerDb.rawQuery("SELECT * FROM "+COMPUTER_TABLE_NAME, null)) {
LinkedList<ComputerDetails> computerList = new LinkedList<>();
while (c.moveToNext()) {
computerList.add(getComputerFromCursor(c));
}
return computerList;
}
c.close();
return computerList;
}
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;
/**
* Get a computer by name
* NOTE: It is perfectly valid for multiple computers to have the same name,
* this function will only return the first one it finds.
* Consider using getComputerByUUID instead.
* @param name The name of the computer
* @see ComputerDatabaseManager#getComputerByUUID(String) for alternative.
* @return The computer details, or null if no computer with that name exists
*/
public ComputerDetails getComputerByName(String name) {
try (final Cursor c = computerDb.query(
COMPUTER_TABLE_NAME, null, COMPUTER_NAME_COLUMN_NAME+"=?",
new String[]{ name }, null, null, null)
) {
if (!c.moveToFirst()) {
// No matching computer
return null;
}
return getComputerFromCursor(c);
}
}
ComputerDetails details = getComputerFromCursor(c);
c.close();
/**
* Get a computer by UUID
* @param uuid The UUID of the computer
* @see ComputerDatabaseManager#getComputerByName(String) for alternative.
* @return The computer details, or null if no computer with that UUID exists
*/
public ComputerDetails getComputerByUUID(String uuid) {
try (final Cursor c = computerDb.query(
COMPUTER_TABLE_NAME, null, COMPUTER_UUID_COLUMN_NAME+"=?",
new String[]{ uuid }, null, null, null)
) {
if (!c.moveToFirst()) {
// No matching computer
return null;
}
return details;
return getComputerFromCursor(c);
}
}
}

View File

@@ -4,12 +4,14 @@ import java.io.IOException;
import java.io.OutputStream;
import java.io.StringReader;
import java.net.Inet4Address;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
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;
@@ -22,14 +24,21 @@ 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;
@@ -38,8 +47,7 @@ public class ComputerManagerService extends Service {
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 = 1000;
private static final int OFFLINE_POLL_TRIES = 5;
private static final int OFFLINE_POLL_TRIES = 3;
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;
@@ -54,6 +62,9 @@ public class ComputerManagerService extends Service {
private ComputerManagerListener listener = null;
private final AtomicInteger activePolls = new AtomicInteger(0);
private boolean pollingActive = false;
private final Lock defaultNetworkLock = new ReentrantLock();
private ConnectivityManager.NetworkCallback networkCallback;
private DiscoveryService.DiscoveryBinder discoveryBinder;
private final ServiceConnection discoveryServiceConnection = new ServiceConnection() {
@@ -125,6 +136,18 @@ public class ComputerManagerService extends Service {
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.address);
if (addr.isSiteLocalAddress()) {
populateExternalAddress(details);
}
}
} catch (UnknownHostException ignored) {}
dbManager.updateComputer(details);
}
}
@@ -153,7 +176,7 @@ public class ComputerManagerService extends Service {
LimeLog.warning(tuple.computer.name + " is offline (try " + offlineCount + ")");
offlineCount++;
} else {
tuple.lastSuccessfulPollMs = System.currentTimeMillis();
tuple.lastSuccessfulPollMs = SystemClock.elapsedRealtime();
offlineCount = 0;
}
}
@@ -184,7 +207,7 @@ public class ComputerManagerService extends Service {
synchronized (pollingTuples) {
for (PollingTuple tuple : pollingTuples) {
// Enforce the poll data TTL
if (System.currentTimeMillis() - tuple.lastSuccessfulPollMs > POLL_DATA_TTL_MS) {
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;
}
@@ -208,7 +231,13 @@ public class ComputerManagerService extends Service {
// Wait for the bind notification
discoveryServiceConnection.wait(1000);
}
} catch (InterruptedException ignored) {
} catch (InterruptedException e) {
e.printStackTrace();
// InterruptedException clears the thread's interrupt status. Since we can't
// handle that here, we will re-interrupt the thread to set the interrupt
// status back to true.
Thread.currentThread().interrupt();
}
}
}
@@ -217,11 +246,18 @@ public class ComputerManagerService extends Service {
while (activePolls.get() != 0) {
try {
Thread.sleep(250);
} catch (InterruptedException ignored) {}
} catch (InterruptedException e) {
e.printStackTrace();
// InterruptedException clears the thread's interrupt status. Since we can't
// handle that here, we will re-interrupt the thread to set the interrupt
// status back to true.
Thread.currentThread().interrupt();
}
}
}
public boolean addComputerBlocking(ComputerDetails fakeDetails) {
public boolean addComputerBlocking(ComputerDetails fakeDetails) throws InterruptedException {
return ComputerManagerService.this.addComputerBlocking(fakeDetails);
}
@@ -294,6 +330,67 @@ 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();
// 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) {
String stunResolvedAddress = NvConnection.findExternalAddressForMdns("stun.moonlight-stream.org", 3478);
if (stunResolvedAddress != null) {
// We don't know for sure what the external port is, so we will have to guess.
// When we contact the PC (if we haven't already), it will update the port.
details.remoteAddress = new ComputerDetails.AddressTuple(stunResolvedAddress, details.guessExternalPort());
}
}
// Unbind from the network
if (boundToNetwork) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
connMgr.bindProcessToNetwork(null);
} else {
ConnectivityManager.setProcessDefaultNetwork(null);
}
}
// Unlock the network state
if (activeNetworkIsVpn) {
defaultNetworkLock.unlock();
}
}
}
private MdnsDiscoveryListener createDiscoveryListener() {
return new MdnsDiscoveryListener() {
@Override
@@ -302,28 +399,32 @@ public class ComputerManagerService extends Service {
// Populate the computer template with mDNS info
if (computer.getLocalAddress() != null) {
details.localAddress = computer.getLocalAddress().getHostAddress();
details.localAddress = new ComputerDetails.AddressTuple(computer.getLocalAddress().getHostAddress(), computer.getPort());
// Since we're on the same network, we can use STUN to find
// our WAN address, which is also very likely the WAN address
// of the PC. We can use this later to connect remotely.
if (computer.getLocalAddress() instanceof Inet4Address) {
details.remoteAddress = NvConnection.findExternalAddressForMdns("stun.moonlight-stream.org", 3478);
populateExternalAddress(details);
}
}
if (computer.getIpv6Address() != null) {
details.ipv6Address = computer.getIpv6Address().getHostAddress();
details.ipv6Address = new ComputerDetails.AddressTuple(computer.getIpv6Address().getHostAddress(), computer.getPort());
}
// Kick off a serverinfo poll on this machine
if (!addComputerBlocking(details)) {
LimeLog.warning("Auto-discovered PC failed to respond: "+details);
}
}
try {
// Kick off a blocking serverinfo poll on this machine
if (!addComputerBlocking(details)) {
LimeLog.warning("Auto-discovered PC failed to respond: "+details);
}
} catch (InterruptedException e) {
e.printStackTrace();
@Override
public void notifyComputerRemoved(MdnsComputer computer) {
// Nothing to do here
// InterruptedException clears the thread's interrupt status. Since we can't
// handle that here, we will re-interrupt the thread to set the interrupt
// status back to true.
Thread.currentThread().interrupt();
}
}
@Override
@@ -365,28 +466,25 @@ public class ComputerManagerService extends Service {
}
}
public boolean addComputerBlocking(ComputerDetails fakeDetails) {
public boolean addComputerBlocking(ComputerDetails fakeDetails) throws InterruptedException {
// Block while we try to fill the details
try {
// 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;
}
// 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;
// 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);
}
// If the machine is reachable, it was successful
@@ -443,17 +541,21 @@ public class ComputerManagerService extends Service {
}
}
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;
}
private ComputerDetails tryPollIp(ComputerDetails details, ComputerDetails.AddressTuple address) {
try {
NvHTTP http = new NvHTTP(address, idManager.getUniqueId(), details.serverCert,
// If the current address's port number matches the active address's port number, we can also assume
// the HTTPS port will also match. This assumption is currently safe because Sunshine sets all ports
// as offsets from the base HTTP port and doesn't allow custom HttpsPort responses for WAN vs LAN.
boolean portMatchesActiveAddress = details.state == ComputerDetails.State.ONLINE &&
details.activeAddress != null && address.port == details.activeAddress.port;
NvHTTP http = new NvHTTP(address, portMatchesActiveAddress ? details.httpsPort : 0, idManager.getUniqueId(), details.serverCert,
PlatformBinding.getCryptoProvider(ComputerManagerService.this));
ComputerDetails newDetails = http.getComputerDetails();
// If this PC is currently online at this address, extend the timeouts to allow more time for the PC to respond.
boolean isLikelyOnline = details.state == ComputerDetails.State.ONLINE && address.equals(details.activeAddress);
ComputerDetails newDetails = http.getComputerDetails(isLikelyOnline);
// Check if this is the PC we expected
if (newDetails.uuid == null) {
@@ -467,146 +569,140 @@ public class ComputerManagerService extends Service {
return null;
}
// Set the new active address
newDetails.activeAddress = address;
return newDetails;
} catch (XmlPullParserException | IOException e) {
} catch (XmlPullParserException e) {
e.printStackTrace();
return null;
} catch (IOException e) {
return null;
}
}
// Just try to establish a TCP connection to speculatively detect a running
// GFE server
private boolean fastPollIp(String address) {
if (address == null) {
// Don't bother if our address is null
return false;
private static class ParallelPollTuple {
public ComputerDetails.AddressTuple address;
public ComputerDetails existingDetails;
public boolean complete;
public Thread pollingThread;
public ComputerDetails returnedDetails;
public ParallelPollTuple(ComputerDetails.AddressTuple address, ComputerDetails existingDetails) {
this.address = address;
this.existingDetails = existingDetails;
}
Socket s = new Socket();
try {
s.connect(new InetSocketAddress(address, NvHTTP.HTTPS_PORT), FAST_POLL_TIMEOUT);
s.close();
return true;
} catch (IOException e) {
return false;
public void interrupt() {
if (pollingThread != null) {
pollingThread.interrupt();
}
}
}
private void startFastPollThread(final String address, final boolean[] info) {
Thread t = new Thread() {
private void startParallelPollThread(ParallelPollTuple tuple, HashSet<ComputerDetails.AddressTuple> uniqueAddresses) {
// Don't bother starting a polling thread for an address that doesn't exist
// or if the address has already been polled with an earlier tuple
if (tuple.address == null || !uniqueAddresses.add(tuple.address)) {
tuple.complete = true;
tuple.returnedDetails = null;
return;
}
tuple.pollingThread = new Thread() {
@Override
public void run() {
boolean pollRes = fastPollIp(address);
ComputerDetails details = tryPollIp(tuple.existingDetails, tuple.address);
synchronized (info) {
info[0] = true; // Done
info[1] = pollRes; // Polling result
synchronized (tuple) {
tuple.complete = true; // Done
tuple.returnedDetails = details; // Polling result
info.notify();
tuple.notify();
}
}
};
t.setName("Fast Poll - "+address);
t.start();
tuple.pollingThread.setName("Parallel Poll - "+tuple.address+" - "+tuple.existingDetails.name);
tuple.pollingThread.start();
}
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];
private ComputerDetails parallelPollPc(ComputerDetails details) throws InterruptedException {
ParallelPollTuple localInfo = new ParallelPollTuple(details.localAddress, details);
ParallelPollTuple manualInfo = new ParallelPollTuple(details.manualAddress, details);
ParallelPollTuple remoteInfo = new ParallelPollTuple(details.remoteAddress, details);
ParallelPollTuple ipv6Info = new ParallelPollTuple(details.ipv6Address, details);
startFastPollThread(localAddress, localInfo);
startFastPollThread(remoteAddress, remoteInfo);
startFastPollThread(manualAddress, manualInfo);
startFastPollThread(ipv6Address, ipv6Info);
// These must be started in order of precedence for the deduplication algorithm
// to result in the correct behavior.
HashSet<ComputerDetails.AddressTuple> uniqueAddresses = new HashSet<>();
startParallelPollThread(localInfo, uniqueAddresses);
startParallelPollThread(manualInfo, uniqueAddresses);
startParallelPollThread(remoteInfo, uniqueAddresses);
startParallelPollThread(ipv6Info, uniqueAddresses);
// Check local first
synchronized (localInfo) {
while (!localInfo[0]) {
localInfo.wait(500);
try {
// Check local first
synchronized (localInfo) {
while (!localInfo.complete) {
localInfo.wait();
}
if (localInfo.returnedDetails != null) {
localInfo.returnedDetails.activeAddress = localInfo.address;
return localInfo.returnedDetails;
}
}
if (localInfo[1]) {
return localAddress;
}
}
// Now manual
synchronized (manualInfo) {
while (!manualInfo.complete) {
manualInfo.wait();
}
// Now manual
synchronized (manualInfo) {
while (!manualInfo[0]) {
manualInfo.wait(500);
if (manualInfo.returnedDetails != null) {
manualInfo.returnedDetails.activeAddress = manualInfo.address;
return manualInfo.returnedDetails;
}
}
if (manualInfo[1]) {
return manualAddress;
}
}
// Now remote IPv4
synchronized (remoteInfo) {
while (!remoteInfo.complete) {
remoteInfo.wait();
}
// Now remote IPv4
synchronized (remoteInfo) {
while (!remoteInfo[0]) {
remoteInfo.wait(500);
if (remoteInfo.returnedDetails != null) {
remoteInfo.returnedDetails.activeAddress = remoteInfo.address;
return remoteInfo.returnedDetails;
}
}
if (remoteInfo[1]) {
return remoteAddress;
}
}
// Now global IPv6
synchronized (ipv6Info) {
while (!ipv6Info.complete) {
ipv6Info.wait();
}
// Now global IPv6
synchronized (ipv6Info) {
while (!ipv6Info[0]) {
ipv6Info.wait(500);
}
if (ipv6Info[1]) {
return ipv6Address;
if (ipv6Info.returnedDetails != null) {
ipv6Info.returnedDetails.activeAddress = ipv6Info.address;
return ipv6Info.returnedDetails;
}
}
} finally {
// Stop any further polling if we've found a working address or we've been
// interrupted by an attempt to stop polling.
localInfo.interrupt();
manualInfo.interrupt();
remoteInfo.interrupt();
ipv6Info.interrupt();
}
return null;
}
private boolean pollComputer(ComputerDetails details) throws InterruptedException {
ComputerDetails polledDetails;
// Do a TCP-level connection to the HTTP server to see if it's listening.
// Do not write this address to details.activeAddress because:
// a) it's only a candidate and may be wrong (multiple PCs behind a single router)
// b) if it's null, it will be unexpectedly nulling the activeAddress of a possibly online PC
LimeLog.info("Starting fast poll for "+details.name+" ("+details.localAddress +", "+details.remoteAddress +", "+details.manualAddress+", "+details.ipv6Address+")");
String candidateAddress = fastPollPc(details.localAddress, details.remoteAddress, details.manualAddress, details.ipv6Address);
LimeLog.info("Fast poll for "+details.name+" returned candidate address: "+candidateAddress);
// If no connection could be established to either IP address, there's nothing we can do
if (candidateAddress == null) {
return false;
}
// 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;
}
}
}
// Poll all addresses in parallel to speed up the process
LimeLog.info("Starting parallel poll for "+details.name+" ("+details.localAddress +", "+details.remoteAddress +", "+details.manualAddress+", "+details.ipv6Address+")");
ComputerDetails polledDetails = parallelPollPc(details);
LimeLog.info("Parallel poll for "+details.name+" returned address: "+details.activeAddress);
if (polledDetails != null) {
details.update(polledDetails);
@@ -641,10 +737,49 @@ public class ComputerManagerService extends Service {
}
releaseLocalDatabaseReference();
// Monitor for network changes to invalidate our PC state
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
networkCallback = new ConnectivityManager.NetworkCallback() {
@Override
public void onAvailable(Network network) {
LimeLog.info("Resetting PC state for new available network");
synchronized (pollingTuples) {
for (PollingTuple tuple : pollingTuples) {
tuple.computer.state = ComputerDetails.State.UNKNOWN;
if (listener != null) {
listener.notifyComputerUpdated(tuple.computer);
}
}
}
}
@Override
public void onLost(Network network) {
LimeLog.info("Offlining PCs due to network loss");
synchronized (pollingTuples) {
for (PollingTuple tuple : pollingTuples) {
tuple.computer.state = ComputerDetails.State.OFFLINE;
if (listener != null) {
listener.notifyComputerUpdated(tuple.computer);
}
}
}
}
};
ConnectivityManager connMgr = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE);
connMgr.registerDefaultNetworkCallback(networkCallback);
}
}
@Override
public void onDestroy() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
ConnectivityManager connMgr = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE);
connMgr.unregisterNetworkCallback(networkCallback);
}
if (discoveryBinder != null) {
// Unbind from the discovery service
unbindService(discoveryServiceConnection);
@@ -732,7 +867,7 @@ public class ComputerManagerService extends Service {
PollingTuple tuple = getPollingTuple(computer);
try {
NvHTTP http = new NvHTTP(ServerHelper.getCurrentAddressFromComputer(computer), idManager.getUniqueId(),
NvHTTP http = new NvHTTP(ServerHelper.getCurrentAddressFromComputer(computer), computer.httpsPort, idManager.getUniqueId(),
computer.serverCert, PlatformBinding.getCryptoProvider(ComputerManagerService.this));
String appList;
@@ -760,18 +895,12 @@ public class ComputerManagerService extends Service {
if (!appList.isEmpty() &&
(!list.isEmpty() || emptyAppListResponses >= EMPTY_LIST_THRESHOLD)) {
// Open the cache file
OutputStream cacheOut = null;
try {
cacheOut = CacheHelper.openCacheFileForOutput(getCacheDir(), "applist", computer.uuid);
try (final OutputStream cacheOut = CacheHelper.openCacheFileForOutput(
getCacheDir(), "applist", computer.uuid)
) {
CacheHelper.writeStringToOutputStream(cacheOut, appList);
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (cacheOut != null) {
cacheOut.close();
}
} catch (IOException ignored) {}
}
// Reset empty count if it wasn't empty this time

View File

@@ -33,12 +33,11 @@ public class IdentityManager {
private static String loadUniqueId(Context c) {
// 2 Hex digits per byte
char[] uid = new char[UID_SIZE_IN_BYTES * 2];
InputStreamReader reader = null;
LimeLog.info("Reading UID from disk");
try {
reader = new InputStreamReader(c.openFileInput(UNIQUE_ID_FILE_NAME));
if (reader.read(uid) != UID_SIZE_IN_BYTES * 2)
{
try (final InputStreamReader reader =
new InputStreamReader(c.openFileInput(UNIQUE_ID_FILE_NAME))
) {
if (reader.read(uid) != UID_SIZE_IN_BYTES * 2) {
LimeLog.severe("UID file data is truncated");
return null;
}
@@ -50,12 +49,6 @@ public class IdentityManager {
LimeLog.severe("Error while reading UID file");
e.printStackTrace();
return null;
} finally {
if (reader != null) {
try {
reader.close();
} catch (IOException ignored) {}
}
}
}
@@ -64,20 +57,14 @@ public class IdentityManager {
LimeLog.info("Generating new UID");
String uidStr = String.format((Locale)null, "%016x", new Random().nextLong());
OutputStreamWriter writer = null;
try {
writer = new OutputStreamWriter(c.openFileOutput(UNIQUE_ID_FILE_NAME, 0));
try (final OutputStreamWriter writer =
new OutputStreamWriter(c.openFileOutput(UNIQUE_ID_FILE_NAME, 0))
) {
writer.write(uidStr);
LimeLog.info("UID written to disk");
} catch (IOException e) {
LimeLog.severe("Error while writing UID file");
e.printStackTrace();
} finally {
if (writer != null) {
try {
writer.close();
} catch (IOException ignored) {}
}
}
// We can return a UID even if I/O fails

View File

@@ -7,6 +7,7 @@ import android.database.sqlite.SQLiteException;
import com.limelight.LimeLog;
import com.limelight.nvstream.http.ComputerDetails;
import com.limelight.nvstream.http.NvHTTP;
import java.net.InetAddress;
import java.net.UnknownHostException;
@@ -30,26 +31,26 @@ public class LegacyDatabaseReader {
// 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();
details.localAddress = new ComputerDetails.AddressTuple(InetAddress.getByAddress(c.getBlob(2)).getHostAddress(), NvHTTP.DEFAULT_HTTP_PORT);
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());
details.localAddress = new ComputerDetails.AddressTuple(c.getString(2).substring(ADDRESS_PREFIX.length()), NvHTTP.DEFAULT_HTTP_PORT);
} else {
LimeLog.severe("DB: Corrupted local address for " + details.name);
}
}
try {
details.remoteAddress = InetAddress.getByAddress(c.getBlob(3)).getHostAddress();
details.remoteAddress = new ComputerDetails.AddressTuple(InetAddress.getByAddress(c.getBlob(3)).getHostAddress(), NvHTTP.DEFAULT_HTTP_PORT);
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());
details.remoteAddress = new ComputerDetails.AddressTuple(c.getString(3).substring(ADDRESS_PREFIX.length()), NvHTTP.DEFAULT_HTTP_PORT);
} else {
LimeLog.severe("DB: Corrupted remote address for " + details.name);
}
@@ -68,37 +69,34 @@ public class LegacyDatabaseReader {
}
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);
try (final 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;
// If a critical field is corrupt or missing, skip the database entry
if (details.uuid == null) {
continue;
}
computerList.add(details);
}
computerList.add(details);
return computerList;
}
c.close();
return computerList;
}
public static List<ComputerDetails> migrateAllComputers(Context c) {
SQLiteDatabase computerDb = null;
try {
try (final SQLiteDatabase computerDb = SQLiteDatabase.openDatabase(
c.getDatabasePath(COMPUTER_DB_NAME).getPath(),
null, SQLiteDatabase.OPEN_READONLY)
) {
// 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

@@ -6,6 +6,7 @@ import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteException;
import com.limelight.nvstream.http.ComputerDetails;
import com.limelight.nvstream.http.NvHTTP;
import java.io.ByteArrayInputStream;
import java.security.cert.CertificateException;
@@ -23,9 +24,9 @@ public class LegacyDatabaseReader2 {
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.localAddress = new ComputerDetails.AddressTuple(c.getString(2), NvHTTP.DEFAULT_HTTP_PORT);
details.remoteAddress = new ComputerDetails.AddressTuple(c.getString(3), NvHTTP.DEFAULT_HTTP_PORT);
details.manualAddress = new ComputerDetails.AddressTuple(c.getString(4), NvHTTP.DEFAULT_HTTP_PORT);
details.macAddress = c.getString(5);
// This column wasn't always present in the old schema
@@ -49,37 +50,34 @@ public class LegacyDatabaseReader2 {
}
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);
try (final 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;
// If a critical field is corrupt or missing, skip the database entry
if (details.uuid == null) {
continue;
}
computerList.add(details);
}
computerList.add(details);
return computerList;
}
c.close();
return computerList;
}
public static List<ComputerDetails> migrateAllComputers(Context c) {
SQLiteDatabase computerDb = null;
try {
try (final SQLiteDatabase computerDb = SQLiteDatabase.openDatabase(
c.getDatabasePath(COMPUTER_DB_NAME).getPath(),
null, SQLiteDatabase.OPEN_READONLY)
) {
// 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,123 @@
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 com.limelight.nvstream.http.NvHTTP;
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 LegacyDatabaseReader3 {
private static final String COMPUTER_DB_NAME = "computers3.db";
private static final String COMPUTER_TABLE_NAME = "Computers";
private static final char ADDRESS_DELIMITER = ';';
private static final char PORT_DELIMITER = '_';
private static String readNonEmptyString(String input) {
if (input.isEmpty()) {
return null;
}
return input;
}
private static ComputerDetails.AddressTuple splitAddressToTuple(String input) {
if (input == null) {
return null;
}
String[] parts = input.split(""+PORT_DELIMITER, -1);
if (parts.length == 1) {
return new ComputerDetails.AddressTuple(parts[0], NvHTTP.DEFAULT_HTTP_PORT);
}
else {
return new ComputerDetails.AddressTuple(parts[0], Integer.parseInt(parts[1]));
}
}
private static String splitTupleToAddress(ComputerDetails.AddressTuple tuple) {
return tuple.address+PORT_DELIMITER+tuple.port;
}
private static 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 = splitAddressToTuple(readNonEmptyString(addresses[0]));
details.remoteAddress = splitAddressToTuple(readNonEmptyString(addresses[1]));
details.manualAddress = splitAddressToTuple(readNonEmptyString(addresses[2]));
details.ipv6Address = splitAddressToTuple(readNonEmptyString(addresses[3]));
// External port is persisted in the remote address field
if (details.remoteAddress != null) {
details.externalPort = details.remoteAddress.port;
}
else {
details.externalPort = NvHTTP.DEFAULT_HTTP_PORT;
}
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 static List<ComputerDetails> getAllComputers(SQLiteDatabase computerDb) {
try (final 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);
}
return computerList;
}
}
public static List<ComputerDetails> migrateAllComputers(Context c) {
try (final SQLiteDatabase computerDb = SQLiteDatabase.openDatabase(
c.getDatabasePath(COMPUTER_DB_NAME).getPath(),
null, SQLiteDatabase.OPEN_READONLY)
) {
// Open the existing database
return getAllComputers(computerDb);
} catch (SQLiteException e) {
return new LinkedList<ComputerDetails>();
} finally {
// Close and delete the old DB
c.deleteDatabase(COMPUTER_DB_NAME);
}
}
}

View File

@@ -3,22 +3,21 @@ package com.limelight.discovery;
import java.util.List;
import com.limelight.nvstream.mdns.MdnsComputer;
import com.limelight.nvstream.mdns.JmDNSDiscoveryAgent;
import com.limelight.nvstream.mdns.MdnsDiscoveryAgent;
import com.limelight.nvstream.mdns.MdnsDiscoveryListener;
import com.limelight.nvstream.mdns.NsdManagerDiscoveryAgent;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.net.wifi.WifiManager;
import android.net.wifi.WifiManager.MulticastLock;
import android.os.Binder;
import android.os.Build;
import android.os.IBinder;
public class DiscoveryService extends Service {
private MdnsDiscoveryAgent discoveryAgent;
private MdnsDiscoveryListener boundListener;
private MulticastLock multicastLock;
public class DiscoveryBinder extends Binder {
public void setListener(MdnsDiscoveryListener listener) {
@@ -26,13 +25,11 @@ public class DiscoveryService extends Service {
}
public void startDiscovery(int queryIntervalMs) {
multicastLock.acquire();
discoveryAgent.startDiscovery(queryIntervalMs);
}
public void stopDiscovery() {
discoveryAgent.stopDiscovery();
multicastLock.release();
}
public List<MdnsComputer> getComputerSet() {
@@ -42,11 +39,7 @@ public class DiscoveryService extends Service {
@Override
public void onCreate() {
WifiManager wifiMgr = (WifiManager) getApplicationContext().getSystemService(Context.WIFI_SERVICE);
multicastLock = wifiMgr.createMulticastLock("Limelight mDNS");
multicastLock.setReferenceCounted(false);
discoveryAgent = new MdnsDiscoveryAgent(new MdnsDiscoveryListener() {
MdnsDiscoveryListener listener = new MdnsDiscoveryListener() {
@Override
public void notifyComputerAdded(MdnsComputer computer) {
if (boundListener != null) {
@@ -54,20 +47,28 @@ public class DiscoveryService extends Service {
}
}
@Override
public void notifyComputerRemoved(MdnsComputer computer) {
if (boundListener != null) {
boundListener.notifyComputerRemoved(computer);
}
}
@Override
public void notifyDiscoveryFailure(Exception e) {
if (boundListener != null) {
boundListener.notifyDiscoveryFailure(e);
}
}
});
};
// Prior to Android 14, NsdManager doesn't provide all the capabilities needed for parity
// with jmDNS (specifically handling multiple addresses for a single service). There are
// also documented reliability bugs early in the Android 4.x series shortly after it was
// introduced. The benefit of using NsdManager over jmDNS is that it works correctly in
// environments where mDNS proxying is required, like ChromeOS, WSA, and the emulator.
//
// As such, we use the jmDNS-based MdnsDiscoveryAgent prior to Android 14 and NsdManager
// on Android 14 and above.
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
discoveryAgent = new JmDNSDiscoveryAgent(getApplicationContext(), listener);
}
else {
discoveryAgent = new NsdManagerDiscoveryAgent(getApplicationContext(), listener);
}
}
private final DiscoveryBinder binder = new DiscoveryBinder();
@@ -81,7 +82,6 @@ public class DiscoveryService extends Service {
public boolean onUnbind(Intent intent) {
// Stop any discovery session
discoveryAgent.stopDiscovery();
multicastLock.release();
// Unbind the listener
boundListener = null;

View File

@@ -1,6 +1,8 @@
package com.limelight.grid;
import android.content.Context;
import android.graphics.BitmapFactory;
import android.view.View;
import android.widget.ImageView;
import android.widget.ProgressBar;
import android.widget.TextView;
@@ -15,8 +17,12 @@ import com.limelight.grid.assets.NetworkAssetLoader;
import com.limelight.nvstream.http.ComputerDetails;
import com.limelight.preferences.PreferenceConfiguration;
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> {
@@ -26,23 +32,49 @@ public class AppGridAdapter extends GenericGridAdapter<AppView.AppObject> {
private final ComputerDetails computer;
private final String uniqueId;
private final boolean showHiddenApps;
private CachedAppAssetLoader loader;
private Set<Integer> hiddenAppIds = new HashSet<>();
private ArrayList<AppView.AppObject> allApps = new ArrayList<>();
public AppGridAdapter(Context context, PreferenceConfiguration prefs, ComputerDetails computer, String uniqueId) {
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);
}
private static int getLayoutIdForPreferences(PreferenceConfiguration prefs) {
if (prefs.listMode) {
return R.layout.simple_row;
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 if (prefs.smallIconMode) {
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 {
@@ -76,7 +108,8 @@ public class AppGridAdapter extends GenericGridAdapter<AppView.AppObject> {
this.loader = new CachedAppAssetLoader(computer, scalingDivisor,
new NetworkAssetLoader(context, uniqueId),
new MemoryAssetLoader(),
new DiskAssetLoader(context));
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));
@@ -88,8 +121,8 @@ 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().toLowerCase().compareTo(rhs.app.getAppName().toLowerCase());
@@ -98,43 +131,54 @@ public class AppGridAdapter extends GenericGridAdapter<AppView.AppObject> {
}
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);
}
@Override
public boolean populateImageView(ImageView imgView, ProgressBar prgView, AppView.AppObject obj) {
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, prgView);
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.isRunning) {
// Show the play button overlay
overlayView.setImageResource(R.drawable.ic_play);
return true;
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

@@ -10,7 +10,6 @@ import android.widget.ProgressBar;
import android.widget.TextView;
import com.limelight.R;
import com.limelight.preferences.PreferenceConfiguration;
import java.util.ArrayList;
@@ -55,9 +54,7 @@ public abstract class GenericGridAdapter<T> extends BaseAdapter {
return i;
}
public abstract boolean populateImageView(ImageView imgView, ProgressBar prgView, T obj);
public abstract boolean populateTextView(TextView txtView, T obj);
public abstract boolean populateOverlayView(ImageView overlayView, T obj);
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) {
@@ -70,22 +67,7 @@ public abstract class GenericGridAdapter<T> extends BaseAdapter {
TextView txtView = convertView.findViewById(R.id.grid_text);
ProgressBar prgView = convertView.findViewById(R.id.grid_spinner);
if (imgView != null) {
if (!populateImageView(imgView, prgView, itemList.get(i))) {
imgView.setImageBitmap(null);
}
}
if (!populateTextView(txtView, itemList.get(i))) {
txtView.setText(itemList.get(i).toString());
}
if (overlayView != null) {
if (!populateOverlayView(overlayView, itemList.get(i))) {
overlayView.setVisibility(View.INVISIBLE);
}
else {
overlayView.setVisibility(View.VISIBLE);
}
}
populateView(convertView, imgView, prgView, txtView, overlayView, itemList.get(i));
return convertView;
}

View File

@@ -22,15 +22,7 @@ public class PcGridAdapter extends GenericGridAdapter<PcView.ComputerObject> {
}
private static int getLayoutIdForPreferences(PreferenceConfiguration prefs) {
if (prefs.listMode) {
return R.layout.simple_row;
}
else if (prefs.smallIconMode) {
return R.layout.pc_grid_item_small;
}
else {
return R.layout.pc_grid_item;
}
return R.layout.pc_grid_item;
}
public void updateLayoutWithPreferences(Context context, PreferenceConfiguration prefs) {
@@ -57,7 +49,8 @@ public class PcGridAdapter extends GenericGridAdapter<PcView.ComputerObject> {
}
@Override
public boolean populateImageView(ImageView imgView, ProgressBar prgView, PcView.ComputerObject obj) {
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);
}
@@ -72,12 +65,7 @@ public class PcGridAdapter extends GenericGridAdapter<PcView.ComputerObject> {
prgView.setVisibility(View.INVISIBLE);
}
imgView.setImageResource(R.drawable.ic_computer);
return true;
}
@Override
public boolean populateTextView(TextView txtView, PcView.ComputerObject obj) {
txtView.setText(obj.details.name);
if (obj.details.state == ComputerDetails.State.ONLINE) {
txtView.setAlpha(1.0f);
}
@@ -85,16 +73,10 @@ public class PcGridAdapter extends GenericGridAdapter<PcView.ComputerObject> {
txtView.setAlpha(0.4f);
}
// Return false to use the computer's toString method
return false;
}
@Override
public boolean populateOverlayView(ImageView overlayView, PcView.ComputerObject obj) {
if (obj.details.state == ComputerDetails.State.OFFLINE) {
overlayView.setImageResource(R.drawable.ic_pc_offline);
overlayView.setAlpha(0.4f);
return true;
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
@@ -102,8 +84,10 @@ public class PcGridAdapter extends GenericGridAdapter<PcView.ComputerObject> {
obj.details.pairState == PairingManager.PairState.NOT_PAIRED) {
overlayView.setImageResource(R.drawable.ic_lock);
overlayView.setAlpha(1.0f);
return true;
overlayView.setVisibility(View.VISIBLE);
}
else {
overlayView.setVisibility(View.GONE);
}
return false;
}
}

View File

@@ -6,9 +6,12 @@ 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.ProgressBar;
import android.widget.TextView;
import com.limelight.R;
import com.limelight.nvstream.http.ComputerDetails;
import com.limelight.nvstream.http.NvApp;
@@ -52,15 +55,17 @@ 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) {
DiskAssetLoader diskLoader, Bitmap noAppImageBitmap) {
this.computer = computer;
this.scalingDivider = scalingDivider;
this.networkLoader = networkLoader;
this.memoryLoader = memoryLoader;
this.diskLoader = diskLoader;
this.noAppImageBitmap = noAppImageBitmap;
this.placeholderBitmap = Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888);
}
@@ -87,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
@@ -108,7 +113,7 @@ public class CachedAppAssetLoader {
// If there's a task associated with this load, we should return the bitmap
if (task != null) {
// If the cached bitmap is valid, return it. Otherwise, we'll try the load again
Bitmap bmp = diskLoader.loadBitmapFromCache(tuple, (int) scalingDivider);
ScaledBitmap bmp = diskLoader.loadBitmapFromCache(tuple, (int) scalingDivider);
if (bmp != null) {
return bmp;
}
@@ -123,6 +128,13 @@ public class CachedAppAssetLoader {
try {
Thread.sleep((int) (1000 + (Math.random() * 500)));
} catch (InterruptedException e) {
e.printStackTrace();
// InterruptedException clears the thread's interrupt status. Since we can't
// handle that here, we will re-interrupt the thread to set the interrupt
// status back to true.
Thread.currentThread().interrupt();
return null;
}
}
@@ -130,29 +142,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<ProgressBar> progressViewRef;
private final WeakReference<TextView> textViewRef;
private final boolean diskOnly;
private LoaderTuple tuple;
public LoaderTask(ImageView imageView, ProgressBar prgView, boolean diskOnly) {
public LoaderTask(ImageView imageView, TextView textView, boolean diskOnly) {
this.imageViewRef = new WeakReference<>(imageView);
this.progressViewRef = new WeakReference<>(prgView);
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 views are gone
if (isCancelled() || imageViewRef.get() == null || progressViewRef.get() == null) {
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
@@ -181,44 +193,61 @@ public class CachedAppAssetLoader {
// If the current loader task for this view isn't us, do nothing
final ImageView imageView = imageViewRef.get();
final ProgressBar prgView = progressViewRef.get();
final TextView textView = textViewRef.get();
if (getLoaderTask(imageView) == this) {
// Now display the progress bar since we have to hit the network
if (prgView != null) {
prgView.setVisibility(View.VISIBLE);
}
// Set off another loader task on the network executor
LoaderTask task = new LoaderTask(imageView, prgView, false);
AsyncDrawable asyncDrawable = new AsyncDrawable(imageView.getResources(), placeholderBitmap, task);
imageView.setVisibility(View.VISIBLE);
// 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 ProgressBar prgView = progressViewRef.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);
// Hide the progress bar
if (prgView != null) {
prgView.setVisibility(View.INVISIBLE);
}
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) {}
// Show the view
imageView.setVisibility(View.VISIBLE);
@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);
}
}
}
}
}
@@ -296,7 +325,13 @@ public class CachedAppAssetLoader {
});
}
public boolean populateImageView(NvApp app, ImageView imgView, ProgressBar prgView) {
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,
@@ -306,22 +341,26 @@ public class CachedAppAssetLoader {
return true;
}
// Hide the progress bar always on initial load
prgView.setVisibility(View.INVISIBLE);
// 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
imgView.setVisibility(View.VISIBLE);
imgView.setImageBitmap(bmp);
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(imgView, prgView, true);
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);
@@ -330,7 +369,7 @@ public class CachedAppAssetLoader {
return false;
}
public class LoaderTuple {
public static class LoaderTuple {
public final ComputerDetails computer;
public final NvApp app;

View File

@@ -28,14 +28,8 @@ public class DiskAssetLoader {
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;
}
this.isLowRamDevice =
((ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE)).isLowRamDevice();
}
public boolean checkCacheExists(CachedAppAssetLoader.LoaderTuple tuple) {
@@ -64,7 +58,7 @@ public class DiskAssetLoader {
return inSampleSize;
}
public Bitmap loadBitmapFromCache(CachedAppAssetLoader.LoaderTuple tuple, int sampleSize) {
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
@@ -110,27 +104,33 @@ public class DiskAssetLoader {
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 {
bmp = ImageDecoder.decodeBitmap(ImageDecoder.createSource(file), new ImageDecoder.OnHeaderDecodedListener() {
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 bmp;
return null;
}
public File getFile(String computerUuid, int appId) {
@@ -148,21 +148,15 @@ public class DiskAssetLoader {
}
public void populateCacheWithStream(CachedAppAssetLoader.LoaderTuple tuple, InputStream input) {
OutputStream out = null;
boolean success = false;
try {
out = CacheHelper.openCacheFileForOutput(cacheDir, "boxart", tuple.computer.uuid, tuple.app.getAppId() + ".png");
try (final OutputStream 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 {
if (out != null) {
try {
out.close();
} catch (IOException ignored) {}
}
if (!success) {
LimeLog.warning("Unable to populate cache with tuple: "+tuple);
CacheHelper.deleteCacheFile(cacheDir, "boxart", tuple.computer.uuid, 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+"-"+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

@@ -22,8 +22,9 @@ public class NetworkAssetLoader {
public InputStream getBitmapStream(CachedAppAssetLoader.LoaderTuple tuple) {
InputStream in = null;
try {
NvHTTP http = new NvHTTP(ServerHelper.getCurrentAddressFromComputer(tuple.computer), uniqueId,
tuple.computer.serverCert, PlatformBinding.getCryptoProvider(context));
NvHTTP http = new NvHTTP(ServerHelper.getCurrentAddressFromComputer(tuple.computer),
tuple.computer.httpsPort, uniqueId, tuple.computer.serverCert,
PlatformBinding.getCryptoProvider(context));
in = http.getBoxArt(tuple.app);
} catch (IOException ignored) {}

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

@@ -1,24 +1,34 @@
package com.limelight.nvstream;
import com.limelight.nvstream.http.ComputerDetails;
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 int negotiatedFps;
public boolean negotiatedHdr;
public ComputerDetails.AddressTuple serverAddress;
public int httpsPort;
public boolean isNvidiaServerSoftware;
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 serverCodecModeSupport;
// This is the sessionUrl0 tag from /resume and /launch
public String rtspSessionUrl;
public int negotiatedWidth, negotiatedHeight;
public boolean negotiatedHdr;
public int negotiatedRemoteStreaming;
public int negotiatedPacketSize;
public int videoCapabilities;
}

View File

@@ -1,12 +1,26 @@
package com.limelight.nvstream;
import android.app.ActivityManager;
import android.content.Context;
import android.net.ConnectivityManager;
import android.net.IpPrefix;
import android.net.LinkProperties;
import android.net.Network;
import android.net.NetworkCapabilities;
import android.net.NetworkInfo;
import android.net.RouteInfo;
import android.os.Build;
import java.io.IOException;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.nio.ByteBuffer;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.security.cert.X509Certificate;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.Semaphore;
import javax.crypto.KeyGenerator;
@@ -17,7 +31,8 @@ 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.ComputerDetails;
import com.limelight.nvstream.http.HostHttpResponseException;
import com.limelight.nvstream.http.LimelightCryptoProvider;
import com.limelight.nvstream.http.NvApp;
import com.limelight.nvstream.http.NvHTTP;
@@ -26,320 +41,551 @@ 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;
// Context parameters
private LimelightCryptoProvider cryptoProvider;
private String uniqueId;
private ConnectionContext context;
private static Semaphore connectionAllowed = new Semaphore(1);
private final boolean isMonkey;
private final Context appContext;
this.context = new ConnectionContext();
this.context.streamConfig = config;
this.context.serverCert = serverCert;
try {
// This is unique per connection
this.context.riKey = generateRiAesKey();
} catch (NoSuchAlgorithmException e) {
// Should never happen
e.printStackTrace();
}
this.context.riKeyId = generateRiKeyId();
this.isMonkey = ActivityManager.isUserAMonkey();
}
private static SecretKey generateRiAesKey() throws NoSuchAlgorithmException {
KeyGenerator keyGen = KeyGenerator.getInstance("AES");
// RI keys are 128 bits
keyGen.init(128);
return keyGen.generateKey();
}
private static int generateRiKeyId() {
return new SecureRandom().nextInt();
}
public NvConnection(Context appContext, ComputerDetails.AddressTuple host, int httpsPort, String uniqueId, StreamConfiguration config, LimelightCryptoProvider cryptoProvider, X509Certificate serverCert)
{
this.appContext = appContext;
this.cryptoProvider = cryptoProvider;
this.uniqueId = uniqueId;
public void stop() {
// Interrupt any pending connection. This is thread-safe.
MoonBridge.interruptConnection();
this.context = new ConnectionContext();
this.context.serverAddress = host;
this.context.httpsPort = httpsPort;
this.context.streamConfig = config;
this.context.serverCert = serverCert;
// 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();
}
// This is unique per connection
this.context.riKey = generateRiAesKey();
this.context.riKeyId = generateRiKeyId();
// 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);
this.isMonkey = ActivityManager.isUserAMonkey();
}
private static SecretKey generateRiAesKey() {
try {
KeyGenerator keyGen = KeyGenerator.getInstance("AES");
String serverInfo = h.getServerInfo();
context.serverAppVersion = h.getServerVersion(serverInfo);
if (context.serverAppVersion == null) {
context.connListener.displayMessage("Server version malformed");
return false;
}
// RI keys are 128 bits
keyGen.init(128);
// 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;
}
return keyGen.generateKey();
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
throw new RuntimeException(e);
}
}
private static int generateRiKeyId() {
return new SecureRandom().nextInt();
}
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.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;
context.negotiatedFps = context.streamConfig.getRefreshRate();
}
else {
// Take what the client wanted
context.negotiatedWidth = context.streamConfig.getWidth();
context.negotiatedHeight = context.streamConfig.getHeight();
context.negotiatedFps = context.streamConfig.getRefreshRate();
}
//
// 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);
}
}
public void stop() {
// Interrupt any pending connection. This is thread-safe.
MoonBridge.interruptConnection();
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;
}
}
// 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();
}
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;
}
// Now a pending connection can be processed
connectionAllowed.release();
}
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();
private InetAddress resolveServerAddress() throws IOException {
// Try to find an address that works for this host
InetAddress[] addrs = InetAddress.getAllByName(context.serverAddress.address);
for (InetAddress addr : addrs) {
try (Socket s = new Socket()) {
s.setSoLinger(true, 0);
s.connect(new InetSocketAddress(addr, context.serverAddress.port), 1000);
return addr;
} catch (IOException e) {
e.printStackTrace();
}
}
String appName = context.streamConfig.getApp().getAppName();
// If we made it here, we didn't manage to find a working address. If DNS returned any
// address, we'll use the first available address and hope for the best.
if (addrs.length > 0) {
return addrs[0];
}
else {
throw new IOException("No addresses found for "+context.serverAddress);
}
}
context.serverAddress = host;
context.connListener.stageStarting(appName);
private int detectServerConnectionType() {
ConnectivityManager connMgr = (ConnectivityManager) appContext.getSystemService(Context.CONNECTIVITY_SERVICE);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
Network activeNetwork = connMgr.getActiveNetwork();
if (activeNetwork != null) {
NetworkCapabilities netCaps = connMgr.getNetworkCapabilities(activeNetwork);
if (netCaps != null) {
if (netCaps.hasTransport(NetworkCapabilities.TRANSPORT_VPN) ||
!netCaps.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN)) {
// VPNs are treated as remote connections
return StreamConfiguration.STREAM_CFG_REMOTE;
}
else if (netCaps.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR)) {
// Cellular is always treated as remote to avoid any possible
// issues with 464XLAT or similar technologies.
return StreamConfiguration.STREAM_CFG_REMOTE;
}
}
try {
if (!startApp()) {
context.connListener.stageFailed(appName, 0);
return;
}
context.connListener.stageComplete(appName);
} catch (XmlPullParserException | IOException e) {
e.printStackTrace();
context.connListener.displayMessage(e.getMessage());
context.connListener.stageFailed(appName, 0);
return;
}
// Check if the server address is on-link
LinkProperties linkProperties = connMgr.getLinkProperties(activeNetwork);
if (linkProperties != null) {
InetAddress serverAddress;
try {
serverAddress = resolveServerAddress();
} catch (IOException e) {
e.printStackTrace();
ByteBuffer ib = ByteBuffer.allocate(16);
ib.putInt(context.riKeyId);
// We can't decide without being able to resolve the server address
return StreamConfiguration.STREAM_CFG_AUTO;
}
// 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);
return;
}
// If the address is in the NAT64 prefix, always treat it as remote
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
IpPrefix nat64Prefix = linkProperties.getNat64Prefix();
if (nat64Prefix != null && nat64Prefix.contains(serverAddress)) {
return StreamConfiguration.STREAM_CFG_REMOTE;
}
}
// 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.negotiatedFps, context.streamConfig.getBitrate(),
context.streamConfig.getMaxPacketSize(),
context.streamConfig.getRemote(), context.streamConfig.getAudioConfiguration(),
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 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);
}
}
for (RouteInfo route : linkProperties.getRoutes()) {
// Skip non-unicast routes (which are all we get prior to Android 13)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && route.getType() != RouteInfo.RTN_UNICAST) {
continue;
}
public static String findExternalAddressForMdns(String stunHostname, int stunPort) {
return MoonBridge.findExternalAddressIP4(stunHostname, stunPort);
}
// Find the first route that matches this address
if (route.matches(serverAddress)) {
// If there's no gateway, this is an on-link destination
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
// We want to use hasGateway() because getGateway() doesn't adhere
// to documented behavior of returning null for on-link addresses.
if (!route.hasGateway()) {
return StreamConfiguration.STREAM_CFG_LOCAL;
}
}
else {
// getGateway() is documented to return null for on-link destinations,
// but it actually returns the unspecified address (0.0.0.0 or ::).
InetAddress gateway = route.getGateway();
if (gateway == null || gateway.isAnyLocalAddress()) {
return StreamConfiguration.STREAM_CFG_LOCAL;
}
}
// We _should_ stop after the first matching route, but for some reason
// Android doesn't always report IPv6 routes in descending order of
// specificity and metric. To handle that case, we enumerate all matching
// routes, assuming that an on-link route will always be preferred.
}
}
}
}
}
else {
NetworkInfo activeNetworkInfo = connMgr.getActiveNetworkInfo();
if (activeNetworkInfo != null) {
switch (activeNetworkInfo.getType()) {
case ConnectivityManager.TYPE_VPN:
case ConnectivityManager.TYPE_MOBILE:
case ConnectivityManager.TYPE_MOBILE_DUN:
case ConnectivityManager.TYPE_MOBILE_HIPRI:
case ConnectivityManager.TYPE_MOBILE_MMS:
case ConnectivityManager.TYPE_MOBILE_SUPL:
case ConnectivityManager.TYPE_WIMAX:
// VPNs and cellular connections are always remote connections
return StreamConfiguration.STREAM_CFG_REMOTE;
}
}
}
// If we can't determine the connection type, let moonlight-common-c decide.
return StreamConfiguration.STREAM_CFG_AUTO;
}
private boolean startApp() throws XmlPullParserException, IOException
{
NvHTTP h = new NvHTTP(context.serverAddress, context.httpsPort, uniqueId, context.serverCert, cryptoProvider);
String serverInfo = h.getServerInfo(true);
context.serverAppVersion = h.getServerVersion(serverInfo);
if (context.serverAppVersion == null) {
context.connListener.displayMessage("Server version malformed");
return false;
}
ComputerDetails details = h.getComputerDetails(serverInfo);
context.isNvidiaServerSoftware = details.nvidiaServer;
// 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.serverCodecModeSupport = (int)h.getServerCodecModeSupport(serverInfo);
context.negotiatedHdr = (context.streamConfig.getSupportedVideoFormats() & MoonBridge.VIDEO_FORMAT_MASK_10BIT) != 0;
if ((context.serverCodecModeSupport & 0x20200) == 0 && context.negotiatedHdr) {
context.connListener.displayTransientMessage("Your PC 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.isNvidiaServerSoftware) {
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.getSupportedVideoFormats() & ~MoonBridge.VIDEO_FORMAT_MASK_H264) == 0) {
context.connListener.displayMessage("Your streaming device must support HEVC or AV1 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();
}
// We will perform some connection type detection if the caller asked for it
if (context.streamConfig.getRemote() == StreamConfiguration.STREAM_CFG_AUTO) {
context.negotiatedRemoteStreaming = detectServerConnectionType();
context.negotiatedPacketSize =
context.negotiatedRemoteStreaming == StreamConfiguration.STREAM_CFG_REMOTE ?
1024 : context.streamConfig.getMaxPacketSize();
}
else {
context.negotiatedRemoteStreaming = context.streamConfig.getRemote();
context.negotiatedPacketSize = context.streamConfig.getMaxPacketSize();
}
//
// 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.launchApp(context, "resume", app.getAppId(), context.negotiatedHdr)) {
context.connListener.displayMessage("Failed to resume existing session");
return false;
}
} else {
return quitAndLaunch(h, context);
}
} catch (HostHttpResponseException 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 (HostHttpResponseException 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, "launch", 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.connListener.stageStarting(appName);
try {
if (!startApp()) {
context.connListener.stageFailed(appName, 0, 0);
return;
}
context.connListener.stageComplete(appName);
} catch (HostHttpResponseException 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.address,
context.serverAppVersion, context.serverGfeVersion, context.rtspSessionUrl,
context.serverCodecModeSupport,
context.negotiatedWidth, context.negotiatedHeight,
context.streamConfig.getRefreshRate(), context.streamConfig.getBitrate(),
context.negotiatedPacketSize, context.negotiatedRemoteStreaming,
context.streamConfig.getAudioConfiguration().toInt(),
context.streamConfig.getSupportedVideoFormats(),
context.streamConfig.getClientRefreshRateX100(),
context.riKey.getEncoded(), ib.array(),
context.videoCapabilities,
context.streamConfig.getColorSpace(),
context.streamConfig.getColorRange());
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();
return;
}
}
}
}).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 sendMouseMoveAsMousePosition(short deltaX, short deltaY, short referenceWidth, short referenceHeight)
{
if (!isMonkey) {
MoonBridge.sendMouseMoveAsMousePosition(deltaX, deltaY, 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 int 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 sendKeyboardInput(final short keyMap, final byte keyDirection, final byte modifier, final byte flags) {
if (!isMonkey) {
MoonBridge.sendKeyboardInput(keyMap, keyDirection, modifier, flags);
}
}
public void sendMouseScroll(final byte scrollClicks) {
if (!isMonkey) {
MoonBridge.sendMouseHighResScroll((short)(scrollClicks * 120)); // WHEEL_DELTA
}
}
public void sendMouseHScroll(final byte scrollClicks) {
if (!isMonkey) {
MoonBridge.sendMouseHighResHScroll((short)(scrollClicks * 120)); // WHEEL_DELTA
}
}
public void sendMouseHighResScroll(final short scrollAmount) {
if (!isMonkey) {
MoonBridge.sendMouseHighResScroll(scrollAmount);
}
}
public void sendMouseHighResHScroll(final short scrollAmount) {
if (!isMonkey) {
MoonBridge.sendMouseHighResHScroll(scrollAmount);
}
}
public int sendTouchEvent(byte eventType, int pointerId, float x, float y, float pressureOrDistance,
float contactAreaMajor, float contactAreaMinor, short rotation) {
if (!isMonkey) {
return MoonBridge.sendTouchEvent(eventType, pointerId, x, y, pressureOrDistance,
contactAreaMajor, contactAreaMinor, rotation);
}
else {
return MoonBridge.LI_ERR_UNSUPPORTED;
}
}
public int sendPenEvent(byte eventType, byte toolType, byte penButtons, float x, float y,
float pressureOrDistance, float contactAreaMajor, float contactAreaMinor,
short rotation, byte tilt) {
if (!isMonkey) {
return MoonBridge.sendPenEvent(eventType, toolType, penButtons, x, y, pressureOrDistance,
contactAreaMajor, contactAreaMinor, rotation, tilt);
}
else {
return MoonBridge.LI_ERR_UNSUPPORTED;
}
}
public int sendControllerArrivalEvent(byte controllerNumber, short activeGamepadMask, byte type,
int supportedButtonFlags, short capabilities) {
return MoonBridge.sendControllerArrivalEvent(controllerNumber, activeGamepadMask, type, supportedButtonFlags, capabilities);
}
public int sendControllerTouchEvent(byte controllerNumber, byte eventType, int pointerId,
float x, float y, float pressure) {
if (!isMonkey) {
return MoonBridge.sendControllerTouchEvent(controllerNumber, eventType, pointerId, x, y, pressure);
}
else {
return MoonBridge.LI_ERR_UNSUPPORTED;
}
}
public int sendControllerMotionEvent(byte controllerNumber, byte motionType,
float x, float y, float z) {
if (!isMonkey) {
return MoonBridge.sendControllerMotionEvent(controllerNumber, motionType, x, y, z);
}
else {
return MoonBridge.LI_ERR_UNSUPPORTED;
}
}
public void sendControllerBatteryEvent(byte controllerNumber, byte batteryState, byte batteryPercentage) {
MoonBridge.sendControllerBatteryEvent(controllerNumber, batteryState, batteryPercentage);
}
public void sendUtf8Text(final String text) {
if (!isMonkey) {
MoonBridge.sendUtf8Text(text);
}
}
public static String findExternalAddressForMdns(String stunHostname, int stunPort) {
return MoonBridge.findExternalAddressIP4(stunHostname, stunPort);
}
}

View File

@@ -1,16 +1,23 @@
package com.limelight.nvstream;
public interface NvConnectionListener {
void stageStarting(String stage);
void stageComplete(String stage);
void stageFailed(String stage, long errorCode);
void connectionStarted();
void connectionTerminated(long errorCode);
void connectionStatusUpdate(int connectionStatus);
void displayMessage(String message);
void displayTransientMessage(String message);
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);
void rumble(short controllerNumber, short lowFreqMotor, short highFreqMotor);
void rumbleTriggers(short controllerNumber, short leftTrigger, short rightTrigger);
void setHdrMode(boolean enabled, byte[] hdrMetadata);
void setMotionEventState(short controllerNumber, byte motionType, short reportRateHz);
void setControllerLED(short controllerNumber, byte r, byte g, byte b);
}

View File

@@ -4,230 +4,221 @@ 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 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;
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 int supportedVideoFormats;
private int attachedGamepadMask;
private int encryptionFlags;
private int colorRange;
private int colorSpace;
private boolean persistGamepadsAfterDisconnect;
private static final int CHANNEL_COUNT_STEREO = 2;
private static final int CHANNEL_COUNT_5_1 = 6;
private static final int CHANNEL_MASK_STEREO = 0x3;
private static final int CHANNEL_MASK_5_1 = 0xFC;
private NvApp app;
private int width, height;
private int refreshRate;
private int clientRefreshRateX100;
private int bitrate;
private boolean sops;
private boolean enableAdaptiveResolution;
private boolean playLocalAudio;
private int maxPacketSize;
private int remote;
private int audioChannelMask;
private int audioChannelCount;
private int 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 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 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 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 setAttachedGamepadMask(int attachedGamepadMask) {
config.attachedGamepadMask = attachedGamepadMask;
return this;
}
public StreamConfiguration.Builder setEnableHdr(boolean enableHdr) {
config.enableHdr = enableHdr;
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 setAttachedGamepadMask(int attachedGamepadMask) {
config.attachedGamepadMask = attachedGamepadMask;
return this;
}
public StreamConfiguration.Builder setPersistGamepadsAfterDisconnect(boolean value) {
config.persistGamepadsAfterDisconnect = value;
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 setClientRefreshRateX100(int refreshRateX100) {
config.clientRefreshRateX100 = refreshRateX100;
return this;
}
public StreamConfiguration.Builder setAudioConfiguration(int audioConfig) {
if (audioConfig == MoonBridge.AUDIO_CONFIGURATION_STEREO) {
config.audioChannelCount = CHANNEL_COUNT_STEREO;
config.audioChannelMask = CHANNEL_MASK_STEREO;
}
else if (audioConfig == MoonBridge.AUDIO_CONFIGURATION_51_SURROUND) {
config.audioChannelCount = CHANNEL_COUNT_5_1;
config.audioChannelMask = CHANNEL_MASK_5_1;
}
else {
throw new IllegalArgumentException("Invalid audio configuration");
}
public StreamConfiguration.Builder setAudioConfiguration(MoonBridge.AudioConfiguration audioConfig) {
config.audioConfiguration = audioConfig;
return this;
}
public StreamConfiguration.Builder setSupportedVideoFormats(int supportedVideoFormats) {
config.supportedVideoFormats = supportedVideoFormats;
return this;
}
config.audioConfiguration = audioConfig;
public StreamConfiguration.Builder setColorRange(int colorRange) {
config.colorRange = colorRange;
return this;
}
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.bitrate = 10000;
this.maxPacketSize = 1024;
this.remote = STREAM_CFG_AUTO;
this.sops = true;
this.enableAdaptiveResolution = false;
this.audioChannelCount = CHANNEL_COUNT_STEREO;
this.audioChannelMask = CHANNEL_MASK_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 getBitrate() {
return bitrate;
}
public int getMaxPacketSize() {
return maxPacketSize;
}
public StreamConfiguration.Builder setColorSpace(int colorSpace) {
config.colorSpace = colorSpace;
return this;
}
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 int getAudioChannelCount() {
return audioChannelCount;
}
public int getAudioChannelMask() {
return audioChannelMask;
}
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.supportedVideoFormats = MoonBridge.VIDEO_FORMAT_H264;
this.attachedGamepadMask = 0;
}
public int getWidth() {
return width;
}
public int getHeight() {
return height;
}
public int getRefreshRate() {
return refreshRate;
}
public int getAudioConfiguration() {
return audioConfiguration;
}
public boolean getHevcSupported() {
return supportsHevc;
}
public int getLaunchRefreshRate() {
return launchRefreshRate;
}
public int getBitrate() {
return bitrate;
}
public int getMaxPacketSize() {
return maxPacketSize;
}
public int getHevcBitratePercentageMultiplier() {
return hevcBitratePercentageMultiplier;
}
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 boolean getEnableHdr() {
return enableHdr;
}
public MoonBridge.AudioConfiguration getAudioConfiguration() {
return audioConfiguration;
}
public int getSupportedVideoFormats() {
return supportedVideoFormats;
}
public int getAttachedGamepadMask() {
return attachedGamepadMask;
}
public int getAttachedGamepadMask() {
return attachedGamepadMask;
}
public int getClientRefreshRateX100() {
return clientRefreshRateX100;
}
public boolean getPersistGamepadsAfterDisconnect() {
return persistGamepadsAfterDisconnect;
}
public int getClientRefreshRateX100() {
return clientRefreshRateX100;
}
public int getColorRange() {
return colorRange;
}
public int getColorSpace() {
return colorSpace;
}
}

View File

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

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

View File

@@ -1,18 +1,21 @@
package com.limelight.nvstream.av.video;
public abstract class VideoDecoderRenderer {
public abstract int setup(int format, int width, int height, int redrawRate);
public abstract int setup(int format, int width, int height, int redrawRate);
public abstract void start();
public abstract void start();
public abstract void stop();
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);
public abstract void cleanup();
// 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, int frameType, char frameHostProcessingLatency,
long receiveTimeMs, long enqueueTimeMs);
public abstract void cleanup();
public abstract int getCapabilities();
public abstract int getCapabilities();
public abstract void setHdrMode(boolean enabled, byte[] hdrMetadata);
}

View File

@@ -1,85 +1,168 @@
package com.limelight.nvstream.http;
import java.security.cert.X509Certificate;
import java.util.Objects;
public class ComputerDetails {
public enum State {
ONLINE, OFFLINE, UNKNOWN
}
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;
public static class AddressTuple {
public String address;
public int port;
// Transient attributes
public State state;
public String activeAddress;
public PairingManager.PairState pairState;
public int runningGameId;
public String rawAppList;
public AddressTuple(String address, int port) {
if (address == null) {
throw new IllegalArgumentException("Address cannot be null");
}
if (port <= 0) {
throw new IllegalArgumentException("Invalid port");
}
public ComputerDetails() {
// Use defaults
state = State.UNKNOWN;
}
// If this was an escaped IPv6 address, remove the brackets
if (address.startsWith("[") && address.endsWith("]")) {
address = address.substring(1, address.length() - 1);
}
public ComputerDetails(ComputerDetails details) {
// Copy details from the other computer
update(details);
}
this.address = address;
this.port = port;
}
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 int hashCode() {
return Objects.hash(address, port);
}
@Override
public String toString() {
StringBuilder str = new StringBuilder();
str.append("State: ").append(state).append("\n");
str.append("Active Address: ").append(activeAddress).append("\n");
str.append("Name: ").append(name).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();
}
@Override
public boolean equals(Object obj) {
if (!(obj instanceof AddressTuple)) {
return false;
}
AddressTuple that = (AddressTuple) obj;
return address.equals(that.address) && port == that.port;
}
public String toString() {
if (address.contains(":")) {
// IPv6
return "[" + address + "]:" + port;
}
else {
// IPv4 and hostnames
return address + ":" + port;
}
}
}
// Persistent attributes
public String uuid;
public String name;
public AddressTuple localAddress;
public AddressTuple remoteAddress;
public AddressTuple manualAddress;
public AddressTuple ipv6Address;
public String macAddress;
public X509Certificate serverCert;
// Transient attributes
public State state;
public AddressTuple activeAddress;
public int httpsPort;
public int externalPort;
public PairingManager.PairState pairState;
public int runningGameId;
public String rawAppList;
public boolean nvidiaServer;
public ComputerDetails() {
// Use defaults
state = State.UNKNOWN;
}
public ComputerDetails(ComputerDetails details) {
// Copy details from the other computer
update(details);
}
public int guessExternalPort() {
if (externalPort != 0) {
return externalPort;
}
else if (remoteAddress != null) {
return remoteAddress.port;
}
else if (activeAddress != null) {
return activeAddress.port;
}
else if (ipv6Address != null) {
return ipv6Address.port;
}
else if (localAddress != null) {
return localAddress.port;
}
else {
return NvHTTP.DEFAULT_HTTP_PORT;
}
}
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.address.startsWith("127.")) {
this.localAddress = details.localAddress;
}
if (details.remoteAddress != null) {
this.remoteAddress = details.remoteAddress;
}
else if (this.remoteAddress != null && details.externalPort != 0) {
// If we have a remote address already (perhaps via STUN) but our updated details
// don't have a new one (because GFE doesn't send one), propagate the external
// port to the current remote address. We may have tried to guess it previously.
this.remoteAddress.port = details.externalPort;
}
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.externalPort = details.externalPort;
this.httpsPort = details.httpsPort;
this.pairState = details.pairState;
this.runningGameId = details.runningGameId;
this.nvidiaServer = details.nvidiaServer;
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");
str.append("HTTPS Port: ").append(httpsPort).append("\n");
return str.toString();
}
}

View File

@@ -1,28 +0,0 @@
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,28 @@
package com.limelight.nvstream.http;
import java.io.IOException;
public class HostHttpResponseException extends IOException {
private static final long serialVersionUID = 1543508830807804222L;
private int errorCode;
private String errorMsg;
public HostHttpResponseException(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 "Host PC returned error: "+errorMsg+" (Error code: "+errorCode+")";
}
}

View File

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

View File

@@ -3,59 +3,68 @@ 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;
}
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 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;
}
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 Supported: ").append(hdrSupported ? "Yes" : "Unknown").append("\n");
str.append("ID: ").append(appId).append("\n");
return str.toString();
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,8 +1,8 @@
package com.limelight.nvstream.http;
import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import org.bouncycastle.crypto.BlockCipher;
import org.bouncycastle.crypto.engines.AESLightEngine;
import org.bouncycastle.crypto.params.KeyParameter;
import org.xmlpull.v1.XmlPullParserException;
@@ -14,327 +14,321 @@ import java.security.*;
import java.security.cert.*;
import java.util.Arrays;
import java.util.Locale;
import java.util.Random;
public class PairingManager {
private NvHTTP http;
private PrivateKey pk;
private X509Certificate cert;
private SecretKey aesKey;
private byte[] pemCertBytes;
private NvHTTP http;
private PrivateKey pk;
private X509Certificate cert;
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();
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);
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);
}
try {
CertificateFactory cf = CertificateFactory.getInstance("X.509");
return (X509Certificate)cf.generateCertificate(new ByteArrayInputStream(certBytes));
} catch (CertificateException e) {
e.printStackTrace();
return null;
}
}
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];
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
{
// Plaincert may be null if another client is already trying to pair
String certText = NvHTTP.getXmlString(text, "plaincert", false);
if (certText != null) {
byte[] certBytes = hexToBytes(certText);
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);
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;
}
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() {
Random r = new Random();
return String.format((Locale)null, "%d%d%d%d",
r.nextInt(10), r.nextInt(10),
r.nextInt(10), r.nextInt(10));
}
private static Signature getSha256SignatureInstanceForKey(Key key) throws NoSuchAlgorithmException {
switch (key.getAlgorithm()) {
case "RSA":
return Signature.getInstance("SHA256withRSA");
case "EC":
return Signature.getInstance("SHA256withECDSA");
default:
throw new NoSuchAlgorithmException("Unhandled key algorithm: " + key.getAlgorithm());
}
}
private static boolean verifySignature(byte[] data, byte[] signature, Certificate cert) {
try {
Signature sig = PairingManager.getSha256SignatureInstanceForKey(cert.getPublicKey());
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 = PairingManager.getSha256SignatureInstanceForKey(key);
sig.initSign(key);
sig.update(data);
return sig.sign();
} catch (NoSuchAlgorithmException | SignatureException | InvalidKeyException e) {
e.printStackTrace();
throw new RuntimeException(e);
}
}
public X509Certificate getPairedCert() {
return serverCert;
}
public PairState pair(String serverInfo, String pin) throws IOException, XmlPullParserException {
PairingHashAlgorithm hashAlgo;
private static byte[] performBlockCipher(BlockCipher blockCipher, byte[] input) {
int blockSize = blockCipher.getBlockSize();
int blockRoundedSize = (input.length + (blockSize - 1)) & ~(blockSize - 1);
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);
byte[] blockRoundedInputData = Arrays.copyOf(input, blockRoundedSize);
byte[] blockRoundedOutputData = new byte[blockRoundedSize];
// 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;
}
for (int offset = 0; offset < blockRoundedSize; offset += blockSize) {
blockCipher.processBlock(blockRoundedInputData, offset, blockRoundedOutputData, offset);
}
// 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.
return PairState.ALREADY_IN_PROGRESS;
}
return blockRoundedOutputData;
}
private static byte[] decryptAes(byte[] encryptedData, byte[] aesKey) {
BlockCipher aesEngine = new AESLightEngine();
aesEngine.init(false, new KeyParameter(aesKey));
return performBlockCipher(aesEngine, encryptedData);
}
private static byte[] encryptAes(byte[] plaintextData, byte[] aesKey) {
BlockCipher aesEngine = new AESLightEngine();
aesEngine.init(true, new KeyParameter(aesKey));
return performBlockCipher(aesEngine, plaintextData);
}
private static byte[] generateAesKey(PairingHashAlgorithm hashAlgo, byte[] keyData) {
return Arrays.copyOf(hashAlgo.hashData(keyData), 16);
}
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));
}
// 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);
public X509Certificate getPairedCert() {
return serverCert;
}
public PairState pair(String serverInfo, String pin) throws IOException, XmlPullParserException {
PairingHashAlgorithm hashAlgo;
// 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;
}
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);
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) {
// Shouldn't ever happen
e.printStackTrace();
return null;
}
}
}
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) {
// Shouldn't ever happen
e.printStackTrace();
return null;
}
}
}
// Combine the salt and pin, then create an AES key from them
byte[] aesKey = generateAesKey(hashAlgo, saltPin(salt, pin));
// 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.executePairingCommand("phrase=getservercert&salt="+
bytesToHex(salt)+"&clientcert="+bytesToHex(pemCertBytes),
false);
if (!NvHTTP.getXmlString(getCert, "paired", true).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.unpair();
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.executePairingCommand("clientchallenge="+bytesToHex(encryptedChallenge), true);
if (!NvHTTP.getXmlString(challengeResp, "paired", true).equals("1")) {
http.unpair();
return PairState.FAILED;
}
// Decode the server's response and subsequent challenge
byte[] encServerChallengeResponse = hexToBytes(NvHTTP.getXmlString(challengeResp, "challengeresponse", true));
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.executePairingCommand("serverchallengeresp="+bytesToHex(challengeRespEncrypted), true);
if (!NvHTTP.getXmlString(secretResp, "paired", true).equals("1")) {
http.unpair();
return PairState.FAILED;
}
// Get the server's signed secret
byte[] serverSecretResp = hexToBytes(NvHTTP.getXmlString(secretResp, "pairingsecret", true));
byte[] serverSecret = Arrays.copyOfRange(serverSecretResp, 0, 16);
byte[] serverSignature = Arrays.copyOfRange(serverSecretResp, 16, serverSecretResp.length);
// Ensure the authenticity of the data
if (!verifySignature(serverSecret, serverSignature, serverCert)) {
// Cancel the pairing process
http.unpair();
// 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.unpair();
// 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.executePairingCommand("clientpairingsecret="+bytesToHex(clientPairingSecret), true);
if (!NvHTTP.getXmlString(clientSecretResp, "paired", true).equals("1")) {
http.unpair();
return PairState.FAILED;
}
// Do the initial challenge (seems necessary for us to show as paired)
String pairChallenge = http.executePairingChallenge();
if (!NvHTTP.getXmlString(pairChallenge, "paired", true).equals("1")) {
http.unpair();
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);
}
}
}
}

View File

@@ -1,19 +1,27 @@
package com.limelight.nvstream.input;
public class ControllerPacket {
public static final short A_FLAG = 0x1000;
public static final short B_FLAG = 0x2000;
public static final short X_FLAG = 0x4000;
public static final short Y_FLAG = (short)0x8000;
public static final short UP_FLAG = 0x0001;
public static final short DOWN_FLAG = 0x0002;
public static final short LEFT_FLAG = 0x0004;
public static final short RIGHT_FLAG = 0x0008;
public static final short LB_FLAG = 0x0100;
public static final short RB_FLAG = 0x0200;
public static final short PLAY_FLAG = 0x0010;
public static final short BACK_FLAG = 0x0020;
public static final short LS_CLK_FLAG = 0x0040;
public static final short RS_CLK_FLAG = 0x0080;
public static final short SPECIAL_BUTTON_FLAG = 0x0400;
public static final int A_FLAG = 0x1000;
public static final int B_FLAG = 0x2000;
public static final int X_FLAG = 0x4000;
public static final int Y_FLAG = 0x8000;
public static final int UP_FLAG = 0x0001;
public static final int DOWN_FLAG = 0x0002;
public static final int LEFT_FLAG = 0x0004;
public static final int RIGHT_FLAG = 0x0008;
public static final int LB_FLAG = 0x0100;
public static final int RB_FLAG = 0x0200;
public static final int PLAY_FLAG = 0x0010;
public static final int BACK_FLAG = 0x0020;
public static final int LS_CLK_FLAG = 0x0040;
public static final int RS_CLK_FLAG = 0x0080;
public static final int SPECIAL_BUTTON_FLAG = 0x0400;
// Extended buttons (Sunshine only)
public static final int PADDLE1_FLAG = 0x010000;
public static final int PADDLE2_FLAG = 0x020000;
public static final int PADDLE3_FLAG = 0x040000;
public static final int PADDLE4_FLAG = 0x080000;
public static final int TOUCHPAD_FLAG = 0x100000; // Touchpad buttons on Sony controllers
public static final int MISC_FLAG = 0x200000; // Share/Mic/Capture/Mute buttons on various controllers
}

View File

@@ -1,10 +1,11 @@
package com.limelight.nvstream.input;
public class KeyboardPacket {
public static final byte KEY_DOWN = 0x03;
public static final byte KEY_UP = 0x04;
public static final byte KEY_DOWN = 0x03;
public static final byte KEY_UP = 0x04;
public static final byte MODIFIER_SHIFT = 0x01;
public static final byte MODIFIER_CTRL = 0x02;
public static final byte MODIFIER_ALT = 0x04;
}
public static final byte MODIFIER_SHIFT = 0x01;
public static final byte MODIFIER_CTRL = 0x02;
public static final byte MODIFIER_ALT = 0x04;
public static final byte MODIFIER_META = 0x08;
}

View File

@@ -1,12 +1,12 @@
package com.limelight.nvstream.input;
public class MouseButtonPacket {
public static final byte PRESS_EVENT = 0x07;
public static final byte RELEASE_EVENT = 0x08;
public static final byte BUTTON_LEFT = 0x01;
public static final byte BUTTON_MIDDLE = 0x02;
public static final byte BUTTON_RIGHT = 0x03;
public static final byte BUTTON_X1 = 0x04;
public static final byte BUTTON_X2 = 0x05;
public static final byte PRESS_EVENT = 0x07;
public static final byte RELEASE_EVENT = 0x08;
public static final byte BUTTON_LEFT = 0x01;
public static final byte BUTTON_MIDDLE = 0x02;
public static final byte BUTTON_RIGHT = 0x03;
public static final byte BUTTON_X1 = 0x04;
public static final byte BUTTON_X2 = 0x05;
}

View File

@@ -7,24 +7,40 @@ import com.limelight.nvstream.av.video.VideoDecoderRenderer;
public class MoonBridge {
/* See documentation in Limelight.h for information about these functions and constants */
public static final int AUDIO_CONFIGURATION_STEREO = 0;
public static final int AUDIO_CONFIGURATION_51_SURROUND = 1;
public static final AudioConfiguration AUDIO_CONFIGURATION_STEREO = new AudioConfiguration(2, 0x3);
public static final AudioConfiguration AUDIO_CONFIGURATION_51_SURROUND = new AudioConfiguration(6, 0x3F);
public static final AudioConfiguration AUDIO_CONFIGURATION_71_SURROUND = new AudioConfiguration(8, 0x63F);
public static final int VIDEO_FORMAT_H264 = 0x0001;
public static final int VIDEO_FORMAT_H265 = 0x0100;
public static final int VIDEO_FORMAT_H265_MAIN10 = 0x0200;
public static final int VIDEO_FORMAT_AV1_MAIN8 = 0x1000;
public static final int VIDEO_FORMAT_AV1_MAIN10 = 0x2000;
public static final int VIDEO_FORMAT_MASK_H264 = 0x00FF;
public static final int VIDEO_FORMAT_MASK_H265 = 0xFF00;
public static final int VIDEO_FORMAT_MASK_H264 = 0x000F;
public static final int VIDEO_FORMAT_MASK_H265 = 0x0F00;
public static final int VIDEO_FORMAT_MASK_AV1 = 0xF000;
public static final int VIDEO_FORMAT_MASK_10BIT = 0x2200;
public static final int BUFFER_TYPE_PICDATA = 0;
public static final int BUFFER_TYPE_SPS = 1;
public static final int BUFFER_TYPE_PPS = 2;
public static final int BUFFER_TYPE_VPS = 3;
public static final int FRAME_TYPE_PFRAME = 0;
public static final int FRAME_TYPE_IDR = 1;
public static final int COLORSPACE_REC_601 = 0;
public static final int COLORSPACE_REC_709 = 1;
public static final int COLORSPACE_REC_2020 = 2;
public static final int COLOR_RANGE_LIMITED = 0;
public static final int COLOR_RANGE_FULL = 1;
public static final int CAPABILITY_DIRECT_SUBMIT = 1;
public static final int CAPABILITY_REFERENCE_FRAME_INVALIDATION_AVC = 2;
public static final int CAPABILITY_REFERENCE_FRAME_INVALIDATION_HEVC = 4;
public static final int CAPABILITY_REFERENCE_FRAME_INVALIDATION_AV1 = 0x40;
public static final int DR_OK = 0;
public static final int DR_NEED_IDR = -1;
@@ -32,6 +48,82 @@ public class MoonBridge {
public static final int CONN_STATUS_OKAY = 0;
public static final int CONN_STATUS_POOR = 1;
public static final int ML_ERROR_GRACEFUL_TERMINATION = 0;
public static final int ML_ERROR_NO_VIDEO_TRAFFIC = -100;
public static final int ML_ERROR_NO_VIDEO_FRAME = -101;
public static final int ML_ERROR_UNEXPECTED_EARLY_TERMINATION = -102;
public static final int ML_ERROR_PROTECTED_CONTENT = -103;
public static final int ML_ERROR_FRAME_CONVERSION = -104;
public static final int ML_PORT_INDEX_TCP_47984 = 0;
public static final int ML_PORT_INDEX_TCP_47989 = 1;
public static final int ML_PORT_INDEX_TCP_48010 = 2;
public static final int ML_PORT_INDEX_UDP_47998 = 8;
public static final int ML_PORT_INDEX_UDP_47999 = 9;
public static final int ML_PORT_INDEX_UDP_48000 = 10;
public static final int ML_PORT_INDEX_UDP_48010 = 11;
public static final int ML_PORT_FLAG_ALL = 0xFFFFFFFF;
public static final int ML_PORT_FLAG_TCP_47984 = 0x0001;
public static final int ML_PORT_FLAG_TCP_47989 = 0x0002;
public static final int ML_PORT_FLAG_TCP_48010 = 0x0004;
public static final int ML_PORT_FLAG_UDP_47998 = 0x0100;
public static final int ML_PORT_FLAG_UDP_47999 = 0x0200;
public static final int ML_PORT_FLAG_UDP_48000 = 0x0400;
public static final int ML_PORT_FLAG_UDP_48010 = 0x0800;
public static final int ML_TEST_RESULT_INCONCLUSIVE = 0xFFFFFFFF;
public static final byte SS_KBE_FLAG_NON_NORMALIZED = 0x01;
public static final int LI_ERR_UNSUPPORTED = -5501;
public static final byte LI_TOUCH_EVENT_HOVER = 0x00;
public static final byte LI_TOUCH_EVENT_DOWN = 0x01;
public static final byte LI_TOUCH_EVENT_UP = 0x02;
public static final byte LI_TOUCH_EVENT_MOVE = 0x03;
public static final byte LI_TOUCH_EVENT_CANCEL = 0x04;
public static final byte LI_TOUCH_EVENT_BUTTON_ONLY = 0x05;
public static final byte LI_TOUCH_EVENT_HOVER_LEAVE = 0x06;
public static final byte LI_TOUCH_EVENT_CANCEL_ALL = 0x07;
public static final byte LI_TOOL_TYPE_UNKNOWN = 0x00;
public static final byte LI_TOOL_TYPE_PEN = 0x01;
public static final byte LI_TOOL_TYPE_ERASER = 0x02;
public static final byte LI_PEN_BUTTON_PRIMARY = 0x01;
public static final byte LI_PEN_BUTTON_SECONDARY = 0x02;
public static final byte LI_PEN_BUTTON_TERTIARY = 0x04;
public static final byte LI_TILT_UNKNOWN = (byte)0xFF;
public static final short LI_ROT_UNKNOWN = (short)0xFFFF;
public static final byte LI_CTYPE_UNKNOWN = 0x00;
public static final byte LI_CTYPE_XBOX = 0x01;
public static final byte LI_CTYPE_PS = 0x02;
public static final byte LI_CTYPE_NINTENDO = 0x03;
public static final short LI_CCAP_ANALOG_TRIGGERS = 0x01;
public static final short LI_CCAP_RUMBLE = 0x02;
public static final short LI_CCAP_TRIGGER_RUMBLE = 0x04;
public static final short LI_CCAP_TOUCHPAD = 0x08;
public static final short LI_CCAP_ACCEL = 0x10;
public static final short LI_CCAP_GYRO = 0x20;
public static final short LI_CCAP_BATTERY_STATE = 0x40;
public static final short LI_CCAP_RGB_LED = 0x80;
public static final byte LI_MOTION_TYPE_ACCEL = 0x01;
public static final byte LI_MOTION_TYPE_GYRO = 0x02;
public static final byte LI_BATTERY_STATE_UNKNOWN = 0x00;
public static final byte LI_BATTERY_STATE_NOT_PRESENT = 0x01;
public static final byte LI_BATTERY_STATE_DISCHARGING = 0x02;
public static final byte LI_BATTERY_STATE_CHARGING = 0x03;
public static final byte LI_BATTERY_STATE_NOT_CHARGING = 0x04; // Connected to power but not charging
public static final byte LI_BATTERY_STATE_FULL = 0x05;
public static final byte LI_BATTERY_PERCENTAGE_UNKNOWN = (byte)0xFF;
private static AudioRenderer audioRenderer;
private static VideoDecoderRenderer videoRenderer;
private static NvConnectionListener connectionListener;
@@ -45,6 +137,57 @@ public class MoonBridge {
return slices << 24;
}
public static class AudioConfiguration {
public final int channelCount;
public final int channelMask;
public AudioConfiguration(int channelCount, int channelMask) {
this.channelCount = channelCount;
this.channelMask = channelMask;
}
// Creates an AudioConfiguration from the integer value returned by moonlight-common-c
// See CHANNEL_COUNT_FROM_AUDIO_CONFIGURATION() and CHANNEL_MASK_FROM_AUDIO_CONFIGURATION()
// in Limelight.h
private AudioConfiguration(int audioConfiguration) {
// Check the magic byte before decoding to make sure we got something that's actually
// a MAKE_AUDIO_CONFIGURATION()-based value and not something else like an older version
// hardcoded AUDIO_CONFIGURATION value from an earlier version of moonlight-common-c.
if ((audioConfiguration & 0xFF) != 0xCA) {
throw new IllegalArgumentException("Audio configuration has invalid magic byte!");
}
this.channelCount = (audioConfiguration >> 8) & 0xFF;
this.channelMask = (audioConfiguration >> 16) & 0xFFFF;
}
// See SURROUNDAUDIOINFO_FROM_AUDIO_CONFIGURATION() in Limelight.h
public int getSurroundAudioInfo() {
return channelMask << 16 | channelCount;
}
@Override
public boolean equals(Object obj) {
if (obj instanceof AudioConfiguration) {
AudioConfiguration that = (AudioConfiguration)obj;
return this.toInt() == that.toInt();
}
return false;
}
@Override
public int hashCode() {
return toInt();
}
// Returns the integer value expected by moonlight-common-c
// See MAKE_AUDIO_CONFIGURATION() in Limelight.h
public int toInt() {
return ((channelMask) << 16) | (channelCount << 8) | 0xCA;
}
}
public static int bridgeDrSetup(int videoFormat, int width, int height, int redrawRate) {
if (videoRenderer != null) {
return videoRenderer.setup(videoFormat, width, height, redrawRate);
@@ -72,21 +215,21 @@ public class MoonBridge {
}
}
public static int bridgeDrSubmitDecodeUnit(byte[] decodeUnitData, int decodeUnitLength,
int decodeUnitType,
int frameNumber, long receiveTimeMs) {
public static int bridgeDrSubmitDecodeUnit(byte[] decodeUnitData, int decodeUnitLength, int decodeUnitType,
int frameNumber, int frameType, char frameHostProcessingLatency,
long receiveTimeMs, long enqueueTimeMs) {
if (videoRenderer != null) {
return videoRenderer.submitDecodeUnit(decodeUnitData, decodeUnitLength,
decodeUnitType, frameNumber, receiveTimeMs);
decodeUnitType, frameNumber, frameType, frameHostProcessingLatency, receiveTimeMs, enqueueTimeMs);
}
else {
return DR_OK;
}
}
public static int bridgeArInit(int audioConfiguration) {
public static int bridgeArInit(int audioConfiguration, int sampleRate, int samplesPerFrame) {
if (audioRenderer != null) {
return audioRenderer.setup(audioConfiguration);
return audioRenderer.setup(new AudioConfiguration(audioConfiguration), sampleRate, samplesPerFrame);
}
else {
return -1;
@@ -129,9 +272,9 @@ public class MoonBridge {
}
}
public static void bridgeClStageFailed(int stage, long errorCode) {
public static void bridgeClStageFailed(int stage, int errorCode) {
if (connectionListener != null) {
connectionListener.stageFailed(getStageName(stage), errorCode);
connectionListener.stageFailed(getStageName(stage), getPortFlagsFromStage(stage), errorCode);
}
}
@@ -141,7 +284,7 @@ public class MoonBridge {
}
}
public static void bridgeClConnectionTerminated(long errorCode) {
public static void bridgeClConnectionTerminated(int errorCode) {
if (connectionListener != null) {
connectionListener.connectionTerminated(errorCode);
}
@@ -159,6 +302,30 @@ public class MoonBridge {
}
}
public static void bridgeClSetHdrMode(boolean enabled, byte[] hdrMetadata) {
if (connectionListener != null) {
connectionListener.setHdrMode(enabled, hdrMetadata);
}
}
public static void bridgeClRumbleTriggers(short controllerNumber, short leftTrigger, short rightTrigger) {
if (connectionListener != null) {
connectionListener.rumbleTriggers(controllerNumber, leftTrigger, rightTrigger);
}
}
public static void bridgeClSetMotionEventState(short controllerNumber, byte eventType, short sampleRateHz) {
if (connectionListener != null) {
connectionListener.setMotionEventState(controllerNumber, eventType, sampleRateHz);
}
}
public static void bridgeClSetControllerLED(short controllerNumber, byte r, byte g, byte b) {
if (connectionListener != null) {
connectionListener.setControllerLED(controllerNumber, r, g, b);
}
}
public static void setupBridge(VideoDecoderRenderer videoRenderer, AudioRenderer audioRenderer, NvConnectionListener connectionListener) {
MoonBridge.videoRenderer = videoRenderer;
MoonBridge.audioRenderer = audioRenderer;
@@ -172,14 +339,14 @@ public class MoonBridge {
}
public static native int startConnection(String address, String appVersion, String gfeVersion,
String rtspSessionUrl, int serverCodecModeSupport,
int width, int height, int fps,
int bitrate, int packetSize, int streamingRemotely,
int audioConfiguration, boolean supportsHevc,
boolean enableHdr,
int hevcBitratePercentageMultiplier,
int audioConfiguration, int supportedVideoFormats,
int clientRefreshRateX100,
byte[] riAesKey, byte[] riAesIv,
int videoCapabilities);
int videoCapabilities,
int colorSpace, int colorRange);
public static native void stopConnection();
@@ -187,30 +354,67 @@ public class MoonBridge {
public static native void sendMouseMove(short deltaX, short deltaY);
public static native void sendMousePosition(short x, short y, short referenceWidth, short referenceHeight);
public static native void sendMouseMoveAsMousePosition(short deltaX, short deltaY, short referenceWidth, short referenceHeight);
public static native void sendMouseButton(byte buttonEvent, byte mouseButton);
public static native void sendMultiControllerInput(short controllerNumber,
short activeGamepadMask, short buttonFlags,
short activeGamepadMask, int buttonFlags,
byte leftTrigger, byte rightTrigger,
short leftStickX, short leftStickY,
short rightStickX, short rightStickY);
public static native void sendControllerInput(short buttonFlags,
byte leftTrigger, byte rightTrigger,
short leftStickX, short leftStickY,
short rightStickX, short rightStickY);
public static native int sendTouchEvent(byte eventType, int pointerId, float x, float y, float pressure,
float contactAreaMajor, float contactAreaMinor, short rotation);
public static native void sendKeyboardInput(short keyMap, byte keyDirection, byte modifier);
public static native int sendPenEvent(byte eventType, byte toolType, byte penButtons, float x, float y,
float pressure, float contactAreaMajor, float contactAreaMinor,
short rotation, byte tilt);
public static native void sendMouseScroll(byte scrollClicks);
public static native int sendControllerArrivalEvent(byte controllerNumber, short activeGamepadMask, byte type, int supportedButtonFlags, short capabilities);
public static native int sendControllerTouchEvent(byte controllerNumber, byte eventType, int pointerId, float x, float y, float pressure);
public static native int sendControllerMotionEvent(byte controllerNumber, byte motionType, float x, float y, float z);
public static native int sendControllerBatteryEvent(byte controllerNumber, byte batteryState, byte batteryPercentage);
public static native void sendKeyboardInput(short keyMap, byte keyDirection, byte modifier, byte flags);
public static native void sendMouseHighResScroll(short scrollAmount);
public static native void sendMouseHighResHScroll(short scrollAmount);
public static native void sendUtf8Text(String text);
public static native String getStageName(int stage);
public static native String findExternalAddressIP4(String stunHostName, int stunPort);
public static native int getPendingAudioFrames();
public static native int getPendingAudioDuration();
public static native int getPendingVideoFrames();
public static native int testClientConnectivity(String testServerHostName, int referencePort, int testFlags);
public static native int getPortFlagsFromStage(int stage);
public static native int getPortFlagsFromTerminationErrorCode(int errorCode);
public static native String stringifyPortFlags(int portFlags, String separator);
// The RTT is in the top 32 bits, and the RTT variance is in the bottom 32 bits
public static native long getEstimatedRttInfo();
public static native String getLaunchUrlQueryParameters();
public static native byte guessControllerType(int vendorId, int productId);
public static native boolean guessControllerHasPaddles(int vendorId, int productId);
public static native boolean guessControllerHasShareButton(int vendorId, int productId);
public static native void init();
}

View File

@@ -0,0 +1,269 @@
package com.limelight.nvstream.mdns;
import android.content.Context;
import android.net.wifi.WifiManager;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.InetAddress;
import java.net.NetworkInterface;
import java.util.ArrayList;
import java.util.HashSet;
import javax.jmdns.JmmDNS;
import javax.jmdns.NetworkTopologyDiscovery;
import javax.jmdns.ServiceEvent;
import javax.jmdns.ServiceInfo;
import javax.jmdns.ServiceListener;
import javax.jmdns.impl.NetworkTopologyDiscoveryImpl;
import com.limelight.LimeLog;
public class JmDNSDiscoveryAgent extends MdnsDiscoveryAgent implements ServiceListener {
private static final String SERVICE_TYPE = "_nvstream._tcp.local.";
private WifiManager.MulticastLock multicastLock;
private Thread discoveryThread;
private HashSet<String> pendingResolution = new HashSet<>();
// The resolver factory's instance member has a static lifetime which
// means our ref count and listener must be static also.
private static int resolverRefCount = 0;
private static HashSet<ServiceListener> listeners = new HashSet<>();
private static ServiceListener nvstreamListener = new ServiceListener() {
@Override
public void serviceAdded(ServiceEvent event) {
HashSet<ServiceListener> localListeners;
// Copy the listener set into a new set so we can invoke
// the callbacks without holding the listeners monitor the
// whole time.
synchronized (listeners) {
localListeners = new HashSet<ServiceListener>(listeners);
}
for (ServiceListener listener : localListeners) {
listener.serviceAdded(event);
}
}
@Override
public void serviceRemoved(ServiceEvent event) {
HashSet<ServiceListener> localListeners;
// Copy the listener set into a new set so we can invoke
// the callbacks without holding the listeners monitor the
// whole time.
synchronized (listeners) {
localListeners = new HashSet<ServiceListener>(listeners);
}
for (ServiceListener listener : localListeners) {
listener.serviceRemoved(event);
}
}
@Override
public void serviceResolved(ServiceEvent event) {
HashSet<ServiceListener> localListeners;
// Copy the listener set into a new set so we can invoke
// the callbacks without holding the listeners monitor the
// whole time.
synchronized (listeners) {
localListeners = new HashSet<ServiceListener>(listeners);
}
for (ServiceListener listener : localListeners) {
listener.serviceResolved(event);
}
}
};
public static class MyNetworkTopologyDiscovery extends NetworkTopologyDiscoveryImpl {
@Override
public boolean useInetAddress(NetworkInterface networkInterface, InetAddress interfaceAddress) {
// This is an copy of jmDNS's implementation, except we omit the multicast check, since
// it seems at least some devices lie about interfaces not supporting multicast when they really do.
try {
if (!networkInterface.isUp()) {
return false;
}
/*
if (!networkInterface.supportsMulticast()) {
return false;
}
*/
if (networkInterface.isLoopback()) {
return false;
}
return true;
} catch (Exception exception) {
return false;
}
}
}
static {
// Override jmDNS's default topology discovery class with ours
NetworkTopologyDiscovery.Factory.setClassDelegate(new NetworkTopologyDiscovery.Factory.ClassDelegate() {
@Override
public NetworkTopologyDiscovery newNetworkTopologyDiscovery() {
return new MyNetworkTopologyDiscovery();
}
});
}
private static JmmDNS referenceResolver() {
synchronized (JmDNSDiscoveryAgent.class) {
JmmDNS instance = JmmDNS.Factory.getInstance();
if (++resolverRefCount == 1) {
// This will cause the listener to be invoked for known hosts immediately.
// JmDNS only supports one listener per service, so we have to do this here
// with a static listener.
instance.addServiceListener(SERVICE_TYPE, nvstreamListener);
}
return instance;
}
}
private static void dereferenceResolver() {
synchronized (JmDNSDiscoveryAgent.class) {
if (--resolverRefCount == 0) {
try {
JmmDNS.Factory.close();
} catch (IOException e) {}
}
}
}
public JmDNSDiscoveryAgent(Context context, MdnsDiscoveryListener listener) {
super(listener);
// Create the multicast lock required to receive mDNS traffic
WifiManager wifiMgr = (WifiManager) context.getSystemService(Context.WIFI_SERVICE);
multicastLock = wifiMgr.createMulticastLock("Limelight mDNS");
multicastLock.setReferenceCounted(false);
}
private void handleResolvedServiceInfo(ServiceInfo info) {
synchronized (pendingResolution) {
pendingResolution.remove(info.getName());
}
try {
handleServiceInfo(info);
} catch (UnsupportedEncodingException e) {
// Invalid DNS response
LimeLog.info("mDNS: Invalid response for machine: "+info.getName());
return;
}
}
private void handleServiceInfo(ServiceInfo info) throws UnsupportedEncodingException {
reportNewComputer(info.getName(), info.getPort(), info.getInet4Addresses(), info.getInet6Addresses());
}
public void startDiscovery(final int discoveryIntervalMs) {
// Kill any existing discovery before starting a new one
stopDiscovery();
// Acquire the multicast lock to start receiving mDNS traffic
multicastLock.acquire();
// Add our listener to the set
synchronized (listeners) {
listeners.add(JmDNSDiscoveryAgent.this);
}
discoveryThread = new Thread() {
@Override
public void run() {
// This may result in listener callbacks so we must register
// our listener first.
JmmDNS resolver = referenceResolver();
try {
while (!Thread.interrupted()) {
// Start an mDNS request
resolver.requestServiceInfo(SERVICE_TYPE, null, discoveryIntervalMs);
// Run service resolution again for pending machines
ArrayList<String> pendingNames;
synchronized (pendingResolution) {
pendingNames = new ArrayList<String>(pendingResolution);
}
for (String name : pendingNames) {
LimeLog.info("mDNS: Retrying service resolution for machine: "+name);
ServiceInfo[] infos = resolver.getServiceInfos(SERVICE_TYPE, name, 500);
if (infos != null && infos.length != 0) {
LimeLog.info("mDNS: Resolved (retry) with "+infos.length+" service entries");
for (ServiceInfo svcinfo : infos) {
handleResolvedServiceInfo(svcinfo);
}
}
}
// Wait for the next polling interval
try {
Thread.sleep(discoveryIntervalMs);
} catch (InterruptedException e) {
break;
}
}
}
finally {
// Dereference the resolver
dereferenceResolver();
}
}
};
discoveryThread.setName("mDNS Discovery Thread");
discoveryThread.start();
}
public void stopDiscovery() {
// Release the multicast lock to stop receiving mDNS traffic
multicastLock.release();
// Remove our listener from the set
synchronized (listeners) {
listeners.remove(JmDNSDiscoveryAgent.this);
}
// If there's already a running thread, interrupt it
if (discoveryThread != null) {
discoveryThread.interrupt();
discoveryThread = null;
}
}
@Override
public void serviceAdded(ServiceEvent event) {
LimeLog.info("mDNS: Machine appeared: "+event.getInfo().getName());
ServiceInfo info = event.getDNS().getServiceInfo(SERVICE_TYPE, event.getInfo().getName(), 500);
if (info == null) {
// This machine is pending resolution
synchronized (pendingResolution) {
pendingResolution.add(event.getInfo().getName());
}
return;
}
LimeLog.info("mDNS: Resolved (blocking)");
handleResolvedServiceInfo(info);
}
@Override
public void serviceRemoved(ServiceEvent event) {
LimeLog.info("mDNS: Machine disappeared: "+event.getInfo().getName());
}
@Override
public void serviceResolved(ServiceEvent event) {
// We handle this synchronously
}
}

View File

@@ -4,62 +4,68 @@ import java.net.Inet6Address;
import java.net.InetAddress;
public class MdnsComputer {
private InetAddress localAddr;
private Inet6Address v6Addr;
private String name;
private InetAddress localAddr;
private Inet6Address v6Addr;
private int port;
private String name;
public MdnsComputer(String name, InetAddress localAddress, Inet6Address v6Addr) {
this.name = name;
this.localAddr = localAddress;
this.v6Addr = v6Addr;
}
public MdnsComputer(String name, InetAddress localAddress, Inet6Address v6Addr, int port) {
this.name = name;
this.localAddr = localAddress;
this.v6Addr = v6Addr;
this.port = port;
}
public String getName() {
return name;
}
public String getName() {
return name;
}
public InetAddress getLocalAddress() {
return localAddr;
}
public InetAddress getLocalAddress() {
return localAddr;
}
public Inet6Address getIpv6Address() {
return v6Addr;
}
public Inet6Address getIpv6Address() {
return v6Addr;
}
@Override
public int hashCode() {
return name.hashCode();
}
public int getPort() {
return port;
}
@Override
public boolean equals(Object o) {
if (o instanceof MdnsComputer) {
MdnsComputer other = (MdnsComputer)o;
@Override
public int hashCode() {
return name.hashCode();
}
if (!other.name.equals(name)) {
return false;
}
@Override
public boolean equals(Object o) {
if (o instanceof MdnsComputer) {
MdnsComputer other = (MdnsComputer)o;
if ((other.localAddr != null && localAddr == null) ||
(other.localAddr == null && localAddr != null) ||
(other.localAddr != null && !other.localAddr.equals(localAddr))) {
return false;
}
if (!other.name.equals(name) || other.port != port) {
return false;
}
if ((other.v6Addr != null && v6Addr == null) ||
(other.v6Addr == null && v6Addr != null) ||
(other.v6Addr != null && !other.v6Addr.equals(v6Addr))) {
return false;
}
if ((other.localAddr != null && localAddr == null) ||
(other.localAddr == null && localAddr != null) ||
(other.localAddr != null && !other.localAddr.equals(localAddr))) {
return false;
}
return true;
}
if ((other.v6Addr != null && v6Addr == null) ||
(other.v6Addr == null && v6Addr != null) ||
(other.v6Addr != null && !other.v6Addr.equals(v6Addr))) {
return false;
}
return false;
}
return true;
}
@Override
public String toString() {
return "["+name+" - "+localAddr+" - "+v6Addr+"]";
}
return false;
}
@Override
public String toString() {
return "["+name+" - "+localAddr+" - "+v6Addr+"]";
}
}

View File

@@ -1,408 +1,148 @@
package com.limelight.nvstream.mdns;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.Inet4Address;
import java.net.Inet6Address;
import java.net.InetAddress;
import java.net.NetworkInterface;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import javax.jmdns.JmmDNS;
import javax.jmdns.NetworkTopologyDiscovery;
import javax.jmdns.ServiceEvent;
import javax.jmdns.ServiceInfo;
import javax.jmdns.ServiceListener;
import javax.jmdns.impl.NetworkTopologyDiscoveryImpl;
import com.limelight.LimeLog;
public class MdnsDiscoveryAgent implements ServiceListener {
public static final String SERVICE_TYPE = "_nvstream._tcp.local.";
private MdnsDiscoveryListener listener;
private Thread discoveryThread;
private HashMap<InetAddress, MdnsComputer> computers = new HashMap<InetAddress, MdnsComputer>();
private HashSet<String> pendingResolution = new HashSet<String>();
// The resolver factory's instance member has a static lifetime which
// means our ref count and listener must be static also.
private static int resolverRefCount = 0;
private static HashSet<ServiceListener> listeners = new HashSet<ServiceListener>();
private static ServiceListener nvstreamListener = new ServiceListener() {
@Override
public void serviceAdded(ServiceEvent event) {
HashSet<ServiceListener> localListeners;
// Copy the listener set into a new set so we can invoke
// the callbacks without holding the listeners monitor the
// whole time.
synchronized (listeners) {
localListeners = new HashSet<ServiceListener>(listeners);
}
for (ServiceListener listener : localListeners) {
listener.serviceAdded(event);
}
}
import java.net.Inet4Address;
import java.net.Inet6Address;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
@Override
public void serviceRemoved(ServiceEvent event) {
HashSet<ServiceListener> localListeners;
// Copy the listener set into a new set so we can invoke
// the callbacks without holding the listeners monitor the
// whole time.
synchronized (listeners) {
localListeners = new HashSet<ServiceListener>(listeners);
}
for (ServiceListener listener : localListeners) {
listener.serviceRemoved(event);
}
}
public abstract class MdnsDiscoveryAgent {
protected MdnsDiscoveryListener listener;
@Override
public void serviceResolved(ServiceEvent event) {
HashSet<ServiceListener> localListeners;
// Copy the listener set into a new set so we can invoke
// the callbacks without holding the listeners monitor the
// whole time.
synchronized (listeners) {
localListeners = new HashSet<ServiceListener>(listeners);
}
for (ServiceListener listener : localListeners) {
listener.serviceResolved(event);
}
}
};
protected HashSet<MdnsComputer> computers = new HashSet<>();
public static class MyNetworkTopologyDiscovery extends NetworkTopologyDiscoveryImpl {
@Override
public boolean useInetAddress(NetworkInterface networkInterface, InetAddress interfaceAddress) {
// This is an copy of jmDNS's implementation, except we omit the multicast check, since
// it seems at least some devices lie about interfaces not supporting multicast when they really do.
try {
if (!networkInterface.isUp()) {
return false;
}
public MdnsDiscoveryAgent(MdnsDiscoveryListener listener) {
this.listener = listener;
}
/*
if (!networkInterface.supportsMulticast()) {
return false;
}
*/
public abstract void startDiscovery(final int discoveryIntervalMs);
if (networkInterface.isLoopback()) {
return false;
}
public abstract void stopDiscovery();
return true;
} catch (Exception exception) {
return false;
}
}
};
protected void reportNewComputer(String name, int port, Inet4Address[] v4Addrs, Inet6Address[] v6Addrs) {
LimeLog.info("mDNS: "+name+" has "+v4Addrs.length+" IPv4 addresses");
LimeLog.info("mDNS: "+name+" has "+v6Addrs.length+" IPv6 addresses");
static {
// Override jmDNS's default topology discovery class with ours
NetworkTopologyDiscovery.Factory.setClassDelegate(new NetworkTopologyDiscovery.Factory.ClassDelegate() {
@Override
public NetworkTopologyDiscovery newNetworkTopologyDiscovery() {
return new MyNetworkTopologyDiscovery();
}
});
}
Inet6Address v6GlobalAddr = getBestIpv6Address(v6Addrs);
private static JmmDNS referenceResolver() {
synchronized (MdnsDiscoveryAgent.class) {
JmmDNS instance = JmmDNS.Factory.getInstance();
if (++resolverRefCount == 1) {
// This will cause the listener to be invoked for known hosts immediately.
// JmDNS only supports one listener per service, so we have to do this here
// with a static listener.
instance.addServiceListener(SERVICE_TYPE, nvstreamListener);
}
return instance;
}
}
// Add a computer object for each IPv4 address reported by the PC
for (Inet4Address v4Addr : v4Addrs) {
synchronized (computers) {
MdnsComputer computer = new MdnsComputer(name, v4Addr, v6GlobalAddr, port);
if (computers.add(computer)) {
// This was a new entry
listener.notifyComputerAdded(computer);
}
}
}
private static void dereferenceResolver() {
synchronized (MdnsDiscoveryAgent.class) {
if (--resolverRefCount == 0) {
try {
JmmDNS.Factory.close();
} catch (IOException e) {}
}
}
}
// If there were no IPv4 addresses, use IPv6 for registration
if (v4Addrs.length == 0) {
Inet6Address v6LocalAddr = getLocalAddress(v6Addrs);
public MdnsDiscoveryAgent(MdnsDiscoveryListener listener) {
this.listener = listener;
}
if (v6LocalAddr != null || v6GlobalAddr != null) {
MdnsComputer computer = new MdnsComputer(name, v6LocalAddr, v6GlobalAddr, port);
if (computers.add(computer)) {
// This was a new entry
listener.notifyComputerAdded(computer);
}
}
}
}
private void handleResolvedServiceInfo(ServiceInfo info) {
synchronized (pendingResolution) {
pendingResolution.remove(info.getName());
}
public List<MdnsComputer> getComputerSet() {
synchronized (computers) {
return new ArrayList<>(computers);
}
}
try {
handleServiceInfo(info);
} catch (UnsupportedEncodingException e) {
// Invalid DNS response
LimeLog.info("mDNS: Invalid response for machine: "+info.getName());
return;
}
}
protected static Inet6Address getLocalAddress(Inet6Address[] addresses) {
for (Inet6Address addr : addresses) {
if (addr.isLinkLocalAddress() || addr.isSiteLocalAddress()) {
return addr;
}
// fc00::/7 - ULAs
else if ((addr.getAddress()[0] & 0xfe) == 0xfc) {
return addr;
}
}
private Inet6Address getLocalAddress(Inet6Address[] addresses) {
for (Inet6Address addr : addresses) {
if (addr.isLinkLocalAddress() || addr.isSiteLocalAddress()) {
return addr;
}
// fc00::/7 - ULAs
else if ((addr.getAddress()[0] & 0xfe) == 0xfc) {
return addr;
}
}
return null;
}
return null;
}
protected static Inet6Address getLinkLocalAddress(Inet6Address[] addresses) {
for (Inet6Address addr : addresses) {
if (addr.isLinkLocalAddress()) {
LimeLog.info("Found link-local address: "+addr.getHostAddress());
return addr;
}
}
private Inet6Address getLinkLocalAddress(Inet6Address[] addresses) {
for (Inet6Address addr : addresses) {
if (addr.isLinkLocalAddress()) {
LimeLog.info("Found link-local address: "+addr.getHostAddress());
return addr;
}
}
return null;
}
return null;
}
protected static Inet6Address getBestIpv6Address(Inet6Address[] addresses) {
// First try to find a link local address, so we can match the interface identifier
// with a global address (this will work for SLAAC but not DHCPv6).
Inet6Address linkLocalAddr = getLinkLocalAddress(addresses);
private Inet6Address getBestIpv6Address(Inet6Address[] addresses) {
// First try to find a link local address, so we can match the interface identifier
// with a global address (this will work for SLAAC but not DHCPv6).
Inet6Address linkLocalAddr = getLinkLocalAddress(addresses);
// We will try once to match a SLAAC interface suffix, then
// pick the first matching address
for (int tries = 0; tries < 2; tries++) {
// We assume the addresses are already sorted in descending order
// of preference from Bonjour.
for (Inet6Address addr : addresses) {
if (addr.isLinkLocalAddress() || addr.isSiteLocalAddress() || addr.isLoopbackAddress()) {
// Link-local, site-local, and loopback aren't global
LimeLog.info("Ignoring non-global address: "+addr.getHostAddress());
continue;
}
// We will try once to match a SLAAC interface suffix, then
// pick the first matching address
for (int tries = 0; tries < 2; tries++) {
// We assume the addresses are already sorted in descending order
// of preference from Bonjour.
for (Inet6Address addr : addresses) {
if (addr.isLinkLocalAddress() || addr.isSiteLocalAddress() || addr.isLoopbackAddress()) {
// Link-local, site-local, and loopback aren't global
LimeLog.info("Ignoring non-global address: "+addr.getHostAddress());
continue;
}
byte[] addrBytes = addr.getAddress();
byte[] addrBytes = addr.getAddress();
// 2002::/16
if (addrBytes[0] == 0x20 && addrBytes[1] == 0x02) {
// 6to4 has horrible performance
LimeLog.info("Ignoring 6to4 address: "+addr.getHostAddress());
continue;
}
// 2001::/32
else if (addrBytes[0] == 0x20 && addrBytes[1] == 0x01 && addrBytes[2] == 0x00 && addrBytes[3] == 0x00) {
// Teredo also has horrible performance
LimeLog.info("Ignoring Teredo address: "+addr.getHostAddress());
continue;
}
// fc00::/7
else if ((addrBytes[0] & 0xfe) == 0xfc) {
// ULAs aren't global
LimeLog.info("Ignoring ULA: "+addr.getHostAddress());
continue;
}
// 2002::/16
if (addrBytes[0] == 0x20 && addrBytes[1] == 0x02) {
// 6to4 has horrible performance
LimeLog.info("Ignoring 6to4 address: "+addr.getHostAddress());
continue;
}
// 2001::/32
else if (addrBytes[0] == 0x20 && addrBytes[1] == 0x01 && addrBytes[2] == 0x00 && addrBytes[3] == 0x00) {
// Teredo also has horrible performance
LimeLog.info("Ignoring Teredo address: "+addr.getHostAddress());
continue;
}
// fc00::/7
else if ((addrBytes[0] & 0xfe) == 0xfc) {
// ULAs aren't global
LimeLog.info("Ignoring ULA: "+addr.getHostAddress());
continue;
}
// Compare the final 64-bit interface identifier and skip the address
// if it doesn't match our link-local address.
if (linkLocalAddr != null && tries == 0) {
boolean matched = true;
// Compare the final 64-bit interface identifier and skip the address
// if it doesn't match our link-local address.
if (linkLocalAddr != null && tries == 0) {
boolean matched = true;
for (int i = 8; i < 16; i++) {
if (linkLocalAddr.getAddress()[i] != addr.getAddress()[i]) {
matched = false;
break;
}
}
for (int i = 8; i < 16; i++) {
if (linkLocalAddr.getAddress()[i] != addr.getAddress()[i]) {
matched = false;
break;
}
}
if (!matched) {
LimeLog.info("Ignoring non-matching global address: "+addr.getHostAddress());
continue;
}
}
if (!matched) {
LimeLog.info("Ignoring non-matching global address: "+addr.getHostAddress());
continue;
}
}
return addr;
}
}
return addr;
}
}
return null;
}
private void handleServiceInfo(ServiceInfo info) throws UnsupportedEncodingException {
Inet4Address v4Addrs[] = info.getInet4Addresses();
Inet6Address v6Addrs[] = info.getInet6Addresses();
LimeLog.info("mDNS: "+info.getName()+" has "+v4Addrs.length+" IPv4 addresses");
LimeLog.info("mDNS: "+info.getName()+" has "+v6Addrs.length+" IPv6 addresses");
Inet6Address v6GlobalAddr = getBestIpv6Address(v6Addrs);
// Add a computer object for each IPv4 address reported by the PC
for (Inet4Address v4Addr : v4Addrs) {
synchronized (computers) {
MdnsComputer computer = new MdnsComputer(info.getName(), v4Addr, v6GlobalAddr);
if (computers.put(computer.getLocalAddress(), computer) == null) {
// This was a new entry
listener.notifyComputerAdded(computer);
}
}
}
// If there were no IPv4 addresses, use IPv6 for registration
if (v4Addrs.length == 0) {
Inet6Address v6LocalAddr = getLocalAddress(v6Addrs);
if (v6LocalAddr != null || v6GlobalAddr != null) {
MdnsComputer computer = new MdnsComputer(info.getName(), v6LocalAddr, v6GlobalAddr);
if (computers.put(v6LocalAddr != null ?
computer.getLocalAddress() : computer.getIpv6Address(), computer) == null) {
// This was a new entry
listener.notifyComputerAdded(computer);
}
}
}
}
public void startDiscovery(final int discoveryIntervalMs) {
// Kill any existing discovery before starting a new one
stopDiscovery();
// Add our listener to the set
synchronized (listeners) {
listeners.add(MdnsDiscoveryAgent.this);
}
discoveryThread = new Thread() {
@Override
public void run() {
// This may result in listener callbacks so we must register
// our listener first.
JmmDNS resolver = referenceResolver();
try {
while (!Thread.interrupted()) {
// Start an mDNS request
resolver.requestServiceInfo(SERVICE_TYPE, null, discoveryIntervalMs);
// Run service resolution again for pending machines
ArrayList<String> pendingNames;
synchronized (pendingResolution) {
pendingNames = new ArrayList<String>(pendingResolution);
}
for (String name : pendingNames) {
LimeLog.info("mDNS: Retrying service resolution for machine: "+name);
ServiceInfo[] infos = resolver.getServiceInfos(SERVICE_TYPE, name, 500);
if (infos != null && infos.length != 0) {
LimeLog.info("mDNS: Resolved (retry) with "+infos.length+" service entries");
for (ServiceInfo svcinfo : infos) {
handleResolvedServiceInfo(svcinfo);
}
}
}
// Wait for the next polling interval
try {
Thread.sleep(discoveryIntervalMs);
} catch (InterruptedException e) {
break;
}
}
}
finally {
// Dereference the resolver
dereferenceResolver();
}
}
};
discoveryThread.setName("mDNS Discovery Thread");
discoveryThread.start();
}
public void stopDiscovery() {
// Remove our listener from the set
synchronized (listeners) {
listeners.remove(MdnsDiscoveryAgent.this);
}
// If there's already a running thread, interrupt it
if (discoveryThread != null) {
discoveryThread.interrupt();
discoveryThread = null;
}
}
public List<MdnsComputer> getComputerSet() {
synchronized (computers) {
return new ArrayList<MdnsComputer>(computers.values());
}
}
@Override
public void serviceAdded(ServiceEvent event) {
LimeLog.info("mDNS: Machine appeared: "+event.getInfo().getName());
ServiceInfo info = event.getDNS().getServiceInfo(SERVICE_TYPE, event.getInfo().getName(), 500);
if (info == null) {
// This machine is pending resolution
synchronized (pendingResolution) {
pendingResolution.add(event.getInfo().getName());
}
return;
}
LimeLog.info("mDNS: Resolved (blocking)");
handleResolvedServiceInfo(info);
}
@Override
public void serviceRemoved(ServiceEvent event) {
LimeLog.info("mDNS: Machine disappeared: "+event.getInfo().getName());
Inet4Address v4Addrs[] = event.getInfo().getInet4Addresses();
for (Inet4Address addr : v4Addrs) {
synchronized (computers) {
MdnsComputer computer = computers.remove(addr);
if (computer != null) {
listener.notifyComputerRemoved(computer);
break;
}
}
}
Inet6Address v6Addrs[] = event.getInfo().getInet6Addresses();
for (Inet6Address addr : v6Addrs) {
synchronized (computers) {
MdnsComputer computer = computers.remove(addr);
if (computer != null) {
listener.notifyComputerRemoved(computer);
break;
}
}
}
}
@Override
public void serviceResolved(ServiceEvent event) {
// We handle this synchronously
}
return null;
}
}

View File

@@ -1,7 +1,6 @@
package com.limelight.nvstream.mdns;
public interface MdnsDiscoveryListener {
void notifyComputerAdded(MdnsComputer computer);
void notifyComputerRemoved(MdnsComputer computer);
void notifyDiscoveryFailure(Exception e);
void notifyComputerAdded(MdnsComputer computer);
void notifyDiscoveryFailure(Exception e);
}

View File

@@ -0,0 +1,234 @@
package com.limelight.nvstream.mdns;
import android.annotation.TargetApi;
import android.content.Context;
import android.net.nsd.NsdManager;
import android.net.nsd.NsdServiceInfo;
import android.os.Build;
import com.limelight.LimeLog;
import java.net.Inet4Address;
import java.net.Inet6Address;
import java.net.InetAddress;
import java.util.HashMap;
import java.util.List;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
@TargetApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
public class NsdManagerDiscoveryAgent extends MdnsDiscoveryAgent {
private static final String SERVICE_TYPE = "_nvstream._tcp";
private final NsdManager nsdManager;
private final Object listenerLock = new Object();
private NsdManager.DiscoveryListener pendingListener;
private NsdManager.DiscoveryListener activeListener;
private final HashMap<String, NsdManager.ServiceInfoCallback> serviceCallbacks = new HashMap<>();
private final ThreadPoolExecutor executor = new ThreadPoolExecutor(1, 1, 0, TimeUnit.SECONDS, new LinkedBlockingQueue<>());
private NsdManager.DiscoveryListener createDiscoveryListener() {
return new NsdManager.DiscoveryListener() {
@Override
public void onStartDiscoveryFailed(String serviceType, int errorCode) {
LimeLog.severe("NSD: Service discovery start failed: " + errorCode);
// This listener is no longer pending after this failure
synchronized (listenerLock) {
if (pendingListener != this) {
return;
}
pendingListener = null;
}
listener.notifyDiscoveryFailure(new RuntimeException("onStartDiscoveryFailed(): " + errorCode));
}
@Override
public void onStopDiscoveryFailed(String serviceType, int errorCode) {
LimeLog.severe("NSD: Service discovery stop failed: " + errorCode);
// This listener is no longer active after this failure
synchronized (listenerLock) {
if (activeListener != this) {
return;
}
activeListener = null;
}
}
@Override
public void onDiscoveryStarted(String serviceType) {
LimeLog.info("NSD: Service discovery started");
synchronized (listenerLock) {
if (pendingListener != this) {
// If we registered another discovery listener in the meantime, stop this one
nsdManager.stopServiceDiscovery(this);
return;
}
pendingListener = null;
activeListener = this;
}
}
@Override
public void onDiscoveryStopped(String serviceType) {
LimeLog.info("NSD: Service discovery stopped");
synchronized (listenerLock) {
if (activeListener != this) {
return;
}
activeListener = null;
}
}
@Override
public void onServiceFound(NsdServiceInfo nsdServiceInfo) {
// Protect against racing stopDiscovery() call
synchronized (listenerLock) {
// Ignore callbacks if we're not the active listener
if (activeListener != this) {
return;
}
LimeLog.info("NSD: Machine appeared: " + nsdServiceInfo.getServiceName());
NsdManager.ServiceInfoCallback serviceInfoCallback = new NsdManager.ServiceInfoCallback() {
@Override
public void onServiceInfoCallbackRegistrationFailed(int errorCode) {
LimeLog.severe("NSD: Service info callback registration failed: " + errorCode);
listener.notifyDiscoveryFailure(new RuntimeException("onServiceInfoCallbackRegistrationFailed(): " + errorCode));
}
@Override
public void onServiceUpdated(NsdServiceInfo nsdServiceInfo) {
LimeLog.info("NSD: Machine resolved: " + nsdServiceInfo.getServiceName());
reportNewComputer(nsdServiceInfo.getServiceName(), nsdServiceInfo.getPort(),
getV4Addrs(nsdServiceInfo.getHostAddresses()),
getV6Addrs(nsdServiceInfo.getHostAddresses()));
}
@Override
public void onServiceLost() {
}
@Override
public void onServiceInfoCallbackUnregistered() {
}
};
nsdManager.registerServiceInfoCallback(nsdServiceInfo, executor, serviceInfoCallback);
serviceCallbacks.put(nsdServiceInfo.getServiceName(), serviceInfoCallback);
}
}
@Override
public void onServiceLost(NsdServiceInfo nsdServiceInfo) {
// Protect against racing stopDiscovery() call
synchronized (listenerLock) {
// Ignore callbacks if we're not the active listener
if (activeListener != this) {
return;
}
LimeLog.info("NSD: Machine lost: " + nsdServiceInfo.getServiceName());
NsdManager.ServiceInfoCallback serviceInfoCallback = serviceCallbacks.remove(nsdServiceInfo.getServiceName());
if (serviceInfoCallback != null) {
nsdManager.unregisterServiceInfoCallback(serviceInfoCallback);
}
}
}
};
}
public NsdManagerDiscoveryAgent(Context context, MdnsDiscoveryListener listener) {
super(listener);
this.nsdManager = context.getSystemService(NsdManager.class);
}
@Override
public void startDiscovery(int discoveryIntervalMs) {
synchronized (listenerLock) {
// Register a new service discovery listener if there's not already one starting or running
if (pendingListener == null && activeListener == null) {
pendingListener = createDiscoveryListener();
nsdManager.discoverServices(SERVICE_TYPE, NsdManager.PROTOCOL_DNS_SD, pendingListener);
}
}
}
@Override
public void stopDiscovery() {
// Protect against racing ServiceInfoCallback and DiscoveryListener callbacks
synchronized (listenerLock) {
// Clear any pending listener to ensure the discoverStarted() callback
// will realize it's gone and stop itself.
pendingListener = null;
// Unregister the service discovery listener
if (activeListener != null) {
nsdManager.stopServiceDiscovery(activeListener);
// Even though listener stoppage is asynchronous, the listener is gone as far as
// we're concerned. We null this right now to ensure pending callbacks know it's
// stopped and startDiscovery() can immediately create a new listener. If we left
// it until onDiscoveryStopped() was called, startDiscovery() would get confused
// and assume a listener was already running, even though it's stopping.
activeListener = null;
}
// Unregister all service info callbacks
for (NsdManager.ServiceInfoCallback callback : serviceCallbacks.values()) {
nsdManager.unregisterServiceInfoCallback(callback);
}
serviceCallbacks.clear();
}
}
private static Inet4Address[] getV4Addrs(List<InetAddress> addrs) {
int matchCount = 0;
for (InetAddress addr : addrs) {
if (addr instanceof Inet4Address) {
matchCount++;
}
}
Inet4Address[] matching = new Inet4Address[matchCount];
int i = 0;
for (InetAddress addr : addrs) {
if (addr instanceof Inet4Address) {
matching[i++] = (Inet4Address) addr;
}
}
return matching;
}
private static Inet6Address[] getV6Addrs(List<InetAddress> addrs) {
int matchCount = 0;
for (InetAddress addr : addrs) {
if (addr instanceof Inet6Address) {
matchCount++;
}
}
Inet6Address[] matching = new Inet6Address[matchCount];
int i = 0;
for (InetAddress addr : addrs) {
if (addr instanceof Inet6Address) {
matching[i++] = (Inet6Address) addr;
}
}
return matching;
}
}

View File

@@ -10,90 +10,140 @@ import com.limelight.LimeLog;
import com.limelight.nvstream.http.ComputerDetails;
public class WakeOnLanSender {
private static final int[] PORTS_TO_TRY = new int[] {
7, 9, // Standard WOL ports
47998, 47999, 48000, 48002, 48010 // Ports opened by GFE
};
public static void sendWolPacket(ComputerDetails computer) throws IOException {
DatagramSocket sock = new DatagramSocket(0);
byte[] payload = createWolPayload(computer);
IOException lastException = null;
boolean sentWolPacket = false;
// These ports will always be tried as-is.
private static final int[] STATIC_PORTS_TO_TRY = new int[] {
9, // Standard WOL port (privileged port)
47009, // Port opened by Moonlight Internet Hosting Tool for WoL (non-privileged port)
};
try {
// Try all resolved remote and local addresses and IPv4 broadcast address.
// The broadcast address is required to avoid stale ARP cache entries
// making the sleeping machine unreachable.
for (String unresolvedAddress : new String[] {
computer.localAddress, computer.remoteAddress, computer.manualAddress, computer.ipv6Address, "255.255.255.255"
}) {
if (unresolvedAddress == null) {
// These ports will be offset by the base port number (47989) to support alternate ports.
private static final int[] DYNAMIC_PORTS_TO_TRY = new int[] {
47998, 47999, 48000, 48002, 48010, // Ports opened by GFE
};
private static void sendPacketsForAddress(InetAddress address, int httpPort, DatagramSocket sock, byte[] payload) throws IOException {
IOException lastException = null;
boolean sentWolPacket = false;
// Try the static ports
for (int port : STATIC_PORTS_TO_TRY) {
try {
DatagramPacket dp = new DatagramPacket(payload, payload.length);
dp.setAddress(address);
dp.setPort(port);
sock.send(dp);
sentWolPacket = true;
} catch (IOException e) {
e.printStackTrace();
lastException = e;
}
}
// Try the dynamic ports
for (int port : DYNAMIC_PORTS_TO_TRY) {
try {
DatagramPacket dp = new DatagramPacket(payload, payload.length);
dp.setAddress(address);
dp.setPort((port - 47989) + httpPort);
sock.send(dp);
sentWolPacket = true;
} catch (IOException e) {
e.printStackTrace();
lastException = e;
}
}
if (!sentWolPacket) {
throw lastException;
}
}
public static void sendWolPacket(ComputerDetails computer) throws IOException {
byte[] payload = createWolPayload(computer);
IOException lastException = null;
boolean sentWolPacket = false;
try (final DatagramSocket sock = new DatagramSocket(0)) {
// Try all resolved remote and local addresses and broadcast addresses.
// The broadcast address is required to avoid stale ARP cache entries
// making the sleeping machine unreachable.
for (ComputerDetails.AddressTuple address : new ComputerDetails.AddressTuple[] {
computer.localAddress, computer.remoteAddress,
computer.manualAddress, computer.ipv6Address,
}) {
if (address == null) {
continue;
}
try {
for (InetAddress resolvedAddress : InetAddress.getAllByName(unresolvedAddress)) {
// Try all the ports for each resolved address
for (int port : PORTS_TO_TRY) {
DatagramPacket dp = new DatagramPacket(payload, payload.length);
dp.setAddress(resolvedAddress);
dp.setPort(port);
sock.send(dp);
sentWolPacket = true;
}
}
} catch (IOException e) {
// We may have addresses that don't resolve on this subnet,
// but don't throw and exit the whole function if that happens.
// We'll throw it at the end if we didn't send a single packet.
e.printStackTrace();
lastException = e;
}
}
} finally {
sock.close();
}
try {
sendPacketsForAddress(InetAddress.getByName("255.255.255.255"), address.port, sock, payload);
sentWolPacket = true;
} catch (IOException e) {
e.printStackTrace();
lastException = e;
}
// Propagate the DNS resolution exception if we didn't
// manage to get a single packet out to the host.
if (!sentWolPacket && lastException != null) {
throw lastException;
}
}
private static byte[] macStringToBytes(String macAddress) {
byte[] macBytes = new byte[6];
@SuppressWarnings("resource")
Scanner scan = new Scanner(macAddress).useDelimiter(":");
for (int i = 0; i < macBytes.length && scan.hasNext(); i++) {
try {
macBytes[i] = (byte) Integer.parseInt(scan.next(), 16);
} catch (NumberFormatException e) {
LimeLog.warning("Malformed MAC address: "+macAddress+" (index: "+i+")");
break;
}
}
scan.close();
return macBytes;
}
private static byte[] createWolPayload(ComputerDetails computer) {
byte[] payload = new byte[102];
byte[] macAddress = macStringToBytes(computer.macAddress);
int i;
// 6 bytes of FF
for (i = 0; i < 6; i++) {
payload[i] = (byte)0xFF;
}
// 16 repetitions of the MAC address
for (int j = 0; j < 16; j++) {
System.arraycopy(macAddress, 0, payload, i, macAddress.length);
i += macAddress.length;
}
return payload;
}
try {
for (InetAddress resolvedAddress : InetAddress.getAllByName(address.address)) {
try {
sendPacketsForAddress(resolvedAddress, address.port, sock, payload);
sentWolPacket = true;
} catch (IOException e) {
e.printStackTrace();
lastException = e;
}
}
} catch (IOException e) {
// We may have addresses that don't resolve on this subnet,
// but don't throw and exit the whole function if that happens.
// We'll throw it at the end if we didn't send a single packet.
e.printStackTrace();
lastException = e;
}
}
}
// Propagate the DNS resolution exception if we didn't
// manage to get a single packet out to the host.
if (!sentWolPacket && lastException != null) {
throw lastException;
}
}
private static byte[] macStringToBytes(String macAddress) {
byte[] macBytes = new byte[6];
try (@SuppressWarnings("resource")
final Scanner scan = new Scanner(macAddress).useDelimiter(":")
) {
for (int i = 0; i < macBytes.length && scan.hasNext(); i++) {
try {
macBytes[i] = (byte) Integer.parseInt(scan.next(), 16);
} catch (NumberFormatException e) {
LimeLog.warning("Malformed MAC address: " + macAddress + " (index: " + i + ")");
break;
}
}
return macBytes;
}
}
private static byte[] createWolPayload(ComputerDetails computer) {
byte[] payload = new byte[102];
byte[] macAddress = macStringToBytes(computer.macAddress);
int i;
// 6 bytes of FF
for (i = 0; i < 6; i++) {
payload[i] = (byte)0xFF;
}
// 16 repetitions of the MAC address
for (int j = 0; j < 16; j++) {
System.arraycopy(macAddress, 0, payload, i, macAddress.length);
i += macAddress.length;
}
return payload;
}
}

View File

@@ -6,6 +6,8 @@ import java.net.InetAddress;
import java.net.InterfaceAddress;
import java.net.NetworkInterface;
import java.net.SocketException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.UnknownHostException;
import java.util.Collections;
import java.util.concurrent.LinkedBlockingQueue;
@@ -15,7 +17,9 @@ import com.limelight.computers.ComputerManagerService;
import com.limelight.R;
import com.limelight.nvstream.http.ComputerDetails;
import com.limelight.nvstream.http.NvHTTP;
import com.limelight.nvstream.jni.MoonBridge;
import com.limelight.utils.Dialog;
import com.limelight.utils.ServerHelper;
import com.limelight.utils.SpinnerDialog;
import com.limelight.utils.UiHelper;
@@ -86,48 +90,108 @@ public class AddComputerManually extends Activity {
// Couldn't find a matching interface
return true;
} catch (SocketException e) {
} catch (Exception e) {
// Catch all exceptions because some broken Android devices
// will throw an NPE from inside getNetworkInterfaces().
e.printStackTrace();
return false;
} catch (UnknownHostException e) {
return false;
}
}
private void doAddPc(String host) {
private URI parseRawUserInputToUri(String rawUserInput) {
try {
// Try adding a scheme and parsing the remaining input.
// This handles input like 127.0.0.1:47989, [::1], [::1]:47989, and 127.0.0.1.
URI uri = new URI("moonlight://" + rawUserInput);
if (uri.getHost() != null && !uri.getHost().isEmpty()) {
return uri;
}
} catch (URISyntaxException ignored) {}
try {
// Attempt to escape the input as an IPv6 literal.
// This handles input like ::1.
URI uri = new URI("moonlight://[" + rawUserInput + "]");
if (uri.getHost() != null && !uri.getHost().isEmpty()) {
return uri;
}
} catch (URISyntaxException ignored) {}
return null;
}
private void doAddPc(String rawUserInput) throws InterruptedException {
boolean wrongSiteLocal = false;
boolean invalidInput = false;
boolean success;
int portTestResult;
SpinnerDialog dialog = SpinnerDialog.displayDialog(this, getResources().getString(R.string.title_add_pc),
getResources().getString(R.string.msg_add_pc), false);
try {
ComputerDetails details = new ComputerDetails();
details.manualAddress = host;
try {
NvHTTP http = new NvHTTP(host, managerBinder.getUniqueId(), null, PlatformBinding.getCryptoProvider(this));
details.serverCert = http.getCertificateIfTrusted();
} catch (IOException ignored) {}
// Check if we parsed a host address successfully
URI uri = parseRawUserInputToUri(rawUserInput);
if (uri != null && uri.getHost() != null && !uri.getHost().isEmpty()) {
String host = uri.getHost();
int port = uri.getPort();
success = managerBinder.addComputerBlocking(details);
// If a port was not specified, use the default
if (port == -1) {
port = NvHTTP.DEFAULT_HTTP_PORT;
}
details.manualAddress = new ComputerDetails.AddressTuple(host, port);
success = managerBinder.addComputerBlocking(details);
if (!success){
wrongSiteLocal = isWrongSubnetSiteLocalAddress(host);
}
} else {
// Invalid user input
success = false;
invalidInput = true;
}
} catch (InterruptedException e) {
// Propagate the InterruptedException to the caller for proper handling
dialog.dismiss();
throw e;
} catch (IllegalArgumentException e) {
// This can be thrown from OkHttp if the host fails to canonicalize to a valid name.
// https://github.com/square/okhttp/blob/okhttp_27/okhttp/src/main/java/com/squareup/okhttp/HttpUrl.java#L705
e.printStackTrace();
success = false;
invalidInput = true;
}
if (!success){
wrongSiteLocal = isWrongSubnetSiteLocalAddress(host);
// Keep the SpinnerDialog open while testing connectivity
if (!success && !wrongSiteLocal && !invalidInput) {
// Run the test before dismissing the spinner because it can take a few seconds.
portTestResult = MoonBridge.testClientConnectivity(ServerHelper.CONNECTION_TEST_SERVER, 443,
MoonBridge.ML_PORT_FLAG_TCP_47984 | MoonBridge.ML_PORT_FLAG_TCP_47989);
} else {
// Don't bother with the test if we succeeded or the IP address was bogus
portTestResult = MoonBridge.ML_TEST_RESULT_INCONCLUSIVE;
}
dialog.dismiss();
if (wrongSiteLocal) {
if (invalidInput) {
Dialog.displayDialog(this, getResources().getString(R.string.conn_error_title), getResources().getString(R.string.addpc_unknown_host), false);
}
else if (wrongSiteLocal) {
Dialog.displayDialog(this, getResources().getString(R.string.conn_error_title), getResources().getString(R.string.addpc_wrong_sitelocal), false);
}
else if (!success) {
Dialog.displayDialog(this, getResources().getString(R.string.conn_error_title), getResources().getString(R.string.addpc_fail), false);
String dialogText;
if (portTestResult != MoonBridge.ML_TEST_RESULT_INCONCLUSIVE && portTestResult != 0) {
dialogText = getResources().getString(R.string.nettest_text_blocked);
}
else {
dialogText = getResources().getString(R.string.addpc_fail);
}
Dialog.displayDialog(this, getResources().getString(R.string.conn_error_title), dialogText, false);
}
else {
AddComputerManually.this.runOnUiThread(new Runnable() {
@@ -150,15 +214,12 @@ public class AddComputerManually extends Activity {
@Override
public void run() {
while (!isInterrupted()) {
String computer;
try {
computer = computersToAdd.take();
String computer = computersToAdd.take();
doAddPc(computer);
} catch (InterruptedException e) {
return;
}
doAddPc(computer);
}
}
};
@@ -172,7 +233,14 @@ public class AddComputerManually extends Activity {
try {
addThread.join();
} catch (InterruptedException ignored) {}
} catch (InterruptedException e) {
e.printStackTrace();
// InterruptedException clears the thread's interrupt status. Since we can't
// handle that here, we will re-interrupt the thread to set the interrupt
// status back to true.
Thread.currentThread().interrupt();
}
addThread = null;
}

View File

@@ -13,7 +13,6 @@ import com.limelight.R;
import static com.limelight.binding.input.virtual_controller.VirtualControllerConfigurationLoader.OSC_PREFERENCE;
public class ConfirmDeleteOscPreference extends DialogPreference {
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
public ConfirmDeleteOscPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
@@ -26,7 +25,6 @@ public class ConfirmDeleteOscPreference extends DialogPreference {
super(context, attrs);
}
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
public ConfirmDeleteOscPreference(Context context) {
super(context);
}

View File

@@ -0,0 +1,49 @@
package com.limelight.preferences;
import android.annotation.TargetApi;
import android.content.ActivityNotFoundException;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.Build;
import android.preference.ListPreference;
import android.provider.Settings;
import android.util.AttributeSet;
public class LanguagePreference extends ListPreference {
public LanguagePreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
public LanguagePreference(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
public LanguagePreference(Context context, AttributeSet attrs) {
super(context, attrs);
}
public LanguagePreference(Context context) {
super(context);
}
@Override
protected void onClick() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
try {
// Launch the Android native app locale settings page
Intent intent = new Intent(Settings.ACTION_APP_LOCALE_SETTINGS);
intent.addCategory(Intent.CATEGORY_DEFAULT);
intent.setData(Uri.parse("package:" + getContext().getPackageName()));
getContext().startActivity(intent, null);
return;
} catch (ActivityNotFoundException e) {
// App locale settings should be present on all Android 13 devices,
// but if not, we'll launch the old language chooser.
}
}
// If we don't have native app locale settings, launch the normal dialog
super.onClick();
}
}

View File

@@ -5,10 +5,26 @@ import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.os.Build;
import android.preference.PreferenceManager;
import android.view.Display;
import com.limelight.nvstream.jni.MoonBridge;
public class PreferenceConfiguration {
private static final String LEGACY_RES_FPS_PREF_STRING = "list_resolution_fps";
public enum FormatOption {
AUTO,
FORCE_AV1,
FORCE_HEVC,
FORCE_H264,
};
public enum AnalogStickForScrolling {
NONE,
RIGHT,
LEFT
}
private static final String LEGACY_RES_FPS_PREF_STRING = "list_resolution_fps";
private static final String LEGACY_ENABLE_51_SURROUND_PREF_STRING = "checkbox_51_surround";
static final String RESOLUTION_PREF_STRING = "list_resolution";
static final String FPS_PREF_STRING = "list_fps";
@@ -19,128 +35,224 @@ public class PreferenceConfiguration {
private static final String DISABLE_TOASTS_PREF_STRING = "checkbox_disable_warnings";
private static final String HOST_AUDIO_PREF_STRING = "checkbox_host_audio";
private static final String DEADZONE_PREF_STRING = "seekbar_deadzone";
private static final String OSC_OPACITY_PREF_STRING = "seekbar_osc_opacity";
private static final String LANGUAGE_PREF_STRING = "list_languages";
private static final String LIST_MODE_PREF_STRING = "checkbox_list_mode";
private static final String SMALL_ICONS_PREF_STRING = "checkbox_small_icon_mode";
private static final String MULTI_CONTROLLER_PREF_STRING = "checkbox_multi_controller";
private static final String ENABLE_51_SURROUND_PREF_STRING = "checkbox_51_surround";
static final String AUDIO_CONFIG_PREF_STRING = "list_audio_config";
private static final String USB_DRIVER_PREF_SRING = "checkbox_usb_driver";
private static final String VIDEO_FORMAT_PREF_STRING = "video_format";
private static final String ONSCREEN_CONTROLLER_PREF_STRING = "checkbox_show_onscreen_controls";
private static final String ONLY_L3_R3_PREF_STRING = "checkbox_only_show_L3R3";
private static final String DISABLE_FRAME_DROP_PREF_STRING = "checkbox_disable_frame_drop";
private static final String SHOW_GUIDE_BUTTON_PREF_STRING = "checkbox_show_guide_button";
private static final String LEGACY_DISABLE_FRAME_DROP_PREF_STRING = "checkbox_disable_frame_drop";
private static final String ENABLE_HDR_PREF_STRING = "checkbox_enable_hdr";
private static final String ENABLE_PIP_PREF_STRING = "checkbox_enable_pip";
private static final String ENABLE_PERF_OVERLAY_STRING = "checkbox_enable_perf_overlay";
private static final String BIND_ALL_USB_STRING = "checkbox_usb_bind_all";
private static final String MOUSE_EMULATION_STRING = "checkbox_mouse_emulation";
private static final String ANALOG_SCROLLING_PREF_STRING = "analog_scrolling";
private static final String MOUSE_NAV_BUTTONS_STRING = "checkbox_mouse_nav_buttons";
static final String UNLOCK_FPS_STRING = "checkbox_unlock_fps";
private static final String VIBRATE_OSC_PREF_STRING = "checkbox_vibrate_osc";
private static final String VIBRATE_FALLBACK_PREF_STRING = "checkbox_vibrate_fallback";
private static final String VIBRATE_FALLBACK_STRENGTH_PREF_STRING = "seekbar_vibrate_fallback_strength";
private static final String FLIP_FACE_BUTTONS_PREF_STRING = "checkbox_flip_face_buttons";
private static final String TOUCHSCREEN_TRACKPAD_PREF_STRING = "checkbox_touchscreen_trackpad";
private static final String LATENCY_TOAST_PREF_STRING = "checkbox_enable_post_stream_toast";
private static final String FRAME_PACING_PREF_STRING = "frame_pacing";
private static final String ABSOLUTE_MOUSE_MODE_PREF_STRING = "checkbox_absolute_mouse_mode";
private static final String ENABLE_AUDIO_FX_PREF_STRING = "checkbox_enable_audiofx";
private static final String REDUCE_REFRESH_RATE_PREF_STRING = "checkbox_reduce_refresh_rate";
private static final String FULL_RANGE_PREF_STRING = "checkbox_full_range";
private static final String GAMEPAD_TOUCHPAD_AS_MOUSE_PREF_STRING = "checkbox_gamepad_touchpad_as_mouse";
private static final String GAMEPAD_MOTION_SENSORS_PREF_STRING = "checkbox_gamepad_motion_sensors";
private static final String GAMEPAD_MOTION_FALLBACK_PREF_STRING = "checkbox_gamepad_motion_fallback";
static final String DEFAULT_RESOLUTION = "720p";
static final String DEFAULT_RESOLUTION = "1280x720";
static final String DEFAULT_FPS = "60";
private static final boolean DEFAULT_STRETCH = false;
private static final boolean DEFAULT_SOPS = true;
private static final boolean DEFAULT_DISABLE_TOASTS = false;
private static final boolean DEFAULT_HOST_AUDIO = false;
private static final int DEFAULT_DEADZONE = 15;
private static final int DEFAULT_DEADZONE = 7;
private static final int DEFAULT_OPACITY = 90;
public static final String DEFAULT_LANGUAGE = "default";
private static final boolean DEFAULT_LIST_MODE = false;
private static final boolean DEFAULT_MULTI_CONTROLLER = true;
private static final boolean DEFAULT_ENABLE_51_SURROUND = false;
private static final boolean DEFAULT_USB_DRIVER = true;
private static final String DEFAULT_VIDEO_FORMAT = "auto";
private static final boolean ONSCREEN_CONTROLLER_DEFAULT = false;
private static final boolean ONLY_L3_R3_DEFAULT = false;
private static final boolean DEFAULT_DISABLE_FRAME_DROP = false;
private static final boolean SHOW_GUIDE_BUTTON_DEFAULT = true;
private static final boolean DEFAULT_ENABLE_HDR = false;
private static final boolean DEFAULT_ENABLE_PIP = false;
private static final boolean DEFAULT_ENABLE_PERF_OVERLAY = false;
private static final boolean DEFAULT_BIND_ALL_USB = false;
private static final boolean DEFAULT_MOUSE_EMULATION = true;
private static final String DEFAULT_ANALOG_STICK_FOR_SCROLLING = "right";
private static final boolean DEFAULT_MOUSE_NAV_BUTTONS = false;
private static final boolean DEFAULT_UNLOCK_FPS = false;
private static final boolean DEFAULT_VIBRATE_OSC = true;
private static final boolean DEFAULT_VIBRATE_FALLBACK = false;
private static final int DEFAULT_VIBRATE_FALLBACK_STRENGTH = 100;
private static final boolean DEFAULT_FLIP_FACE_BUTTONS = false;
private static final boolean DEFAULT_TOUCHSCREEN_TRACKPAD = true;
private static final String DEFAULT_AUDIO_CONFIG = "2"; // Stereo
private static final boolean DEFAULT_LATENCY_TOAST = false;
private static final String DEFAULT_FRAME_PACING = "latency";
private static final boolean DEFAULT_ABSOLUTE_MOUSE_MODE = false;
private static final boolean DEFAULT_ENABLE_AUDIO_FX = false;
private static final boolean DEFAULT_REDUCE_REFRESH_RATE = false;
private static final boolean DEFAULT_FULL_RANGE = false;
private static final boolean DEFAULT_GAMEPAD_TOUCHPAD_AS_MOUSE = false;
private static final boolean DEFAULT_GAMEPAD_MOTION_SENSORS = true;
private static final boolean DEFAULT_GAMEPAD_MOTION_FALLBACK = false;
public static final int FORCE_H265_ON = -1;
public static final int AUTOSELECT_H265 = 0;
public static final int FORCE_H265_OFF = 1;
public static final int FRAME_PACING_MIN_LATENCY = 0;
public static final int FRAME_PACING_BALANCED = 1;
public static final int FRAME_PACING_CAP_FPS = 2;
public static final int FRAME_PACING_MAX_SMOOTHNESS = 3;
public static final String RES_360P = "640x360";
public static final String RES_480P = "854x480";
public static final String RES_720P = "1280x720";
public static final String RES_1080P = "1920x1080";
public static final String RES_1440P = "2560x1440";
public static final String RES_4K = "3840x2160";
public static final String RES_NATIVE = "Native";
public int width, height, fps;
public int bitrate;
public int videoFormat;
public FormatOption videoFormat;
public int deadzonePercentage;
public int oscOpacity;
public boolean stretchVideo, enableSops, playHostAudio, disableWarnings;
public String language;
public boolean listMode, smallIconMode, multiController, enable51Surround, usbDriver;
public boolean smallIconMode, multiController, usbDriver, flipFaceButtons;
public boolean onscreenController;
public boolean onlyL3R3;
public boolean disableFrameDrop;
public boolean showGuideButton;
public boolean enableHdr;
public boolean enablePip;
public boolean enablePerfOverlay;
public boolean enableLatencyToast;
public boolean bindAllUsb;
public boolean mouseEmulation;
public AnalogStickForScrolling analogStickForScrolling;
public boolean mouseNavButtons;
public boolean unlockFps;
public boolean vibrateOsc;
public boolean vibrateFallbackToDevice;
public int vibrateFallbackToDeviceStrength;
public boolean touchscreenTrackpad;
public MoonBridge.AudioConfiguration audioConfiguration;
public int framePacing;
public boolean absoluteMouseMode;
public boolean enableAudioFx;
public boolean reduceRefreshRate;
public boolean fullRange;
public boolean gamepadMotionSensors;
public boolean gamepadTouchpadAsMouse;
public boolean gamepadMotionSensorsFallbackToDevice;
private static int getHeightFromResolutionString(String resString) {
public static boolean isNativeResolution(int width, int height) {
// It's not a native resolution if it matches an existing resolution option
if (width == 640 && height == 360) {
return false;
}
else if (width == 854 && height == 480) {
return false;
}
else if (width == 1280 && height == 720) {
return false;
}
else if (width == 1920 && height == 1080) {
return false;
}
else if (width == 2560 && height == 1440) {
return false;
}
else if (width == 3840 && height == 2160) {
return false;
}
return true;
}
// If we have a screen that has semi-square dimensions, we may want to change our behavior
// to allow any orientation and vertical+horizontal resolutions.
public static boolean isSquarishScreen(int width, int height) {
float longDim = Math.max(width, height);
float shortDim = Math.min(width, height);
// We just put the arbitrary cutoff for a square-ish screen at 1.3
return longDim / shortDim < 1.3f;
}
public static boolean isSquarishScreen(Display display) {
int width, height;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
width = display.getMode().getPhysicalWidth();
height = display.getMode().getPhysicalHeight();
}
else {
width = display.getWidth();
height = display.getHeight();
}
return isSquarishScreen(width, height);
}
private static String convertFromLegacyResolutionString(String resString) {
if (resString.equalsIgnoreCase("360p")) {
return 360;
return RES_360P;
}
else if (resString.equalsIgnoreCase("480p")) {
return 480;
return RES_480P;
}
else if (resString.equalsIgnoreCase("720p")) {
return 720;
return RES_720P;
}
else if (resString.equalsIgnoreCase("1080p")) {
return 1080;
return RES_1080P;
}
else if (resString.equalsIgnoreCase("1440p")) {
return 1440;
return RES_1440P;
}
else if (resString.equalsIgnoreCase("4K")) {
return 2160;
return RES_4K;
}
else {
// Should be unreachable
return 720;
return RES_720P;
}
}
private static int getWidthFromResolutionString(String resString) {
int height = getHeightFromResolutionString(resString);
if (height == 480) {
// This isn't an exact 16:9 resolution
return 854;
}
else {
return (height * 16) / 9;
}
return Integer.parseInt(resString.split("x")[0]);
}
private static int getHeightFromResolutionString(String resString) {
return Integer.parseInt(resString.split("x")[1]);
}
private static String getResolutionString(int width, int height) {
switch (height) {
case 360:
return "360p";
return RES_360P;
case 480:
return "480p";
return RES_480P;
default:
case 720:
return "720p";
return RES_720P;
case 1080:
return "1080p";
return RES_1080P;
case 1440:
return "1440p";
return RES_1440P;
case 2160:
return "4K";
return RES_4K;
}
}
@@ -149,33 +261,62 @@ public class PreferenceConfiguration {
int height = getHeightFromResolutionString(resString);
int fps = Integer.parseInt(fpsString);
// This table prefers 16:10 resolutions because they are
// only slightly more pixels than the 16:9 equivalents, so
// we don't want to bump those 16:10 resolutions up to the
// next 16:9 slot.
//
// This logic is shamelessly stolen from Moonlight Qt:
// https://github.com/moonlight-stream/moonlight-qt/blob/master/app/settings/streamingpreferences.cpp
if (width * height <= 640 * 360) {
return (int)(1000 * (fps / 30.0));
}
else if (width * height <= 854 * 480) {
return (int)(1500 * (fps / 30.0));
}
// This covers 1280x720 and 1280x800 too
else if (width * height <= 1366 * 768) {
return (int)(5000 * (fps / 30.0));
}
else if (width * height <= 1920 * 1200) {
return (int)(10000 * (fps / 30.0));
}
else if (width * height <= 2560 * 1600) {
return (int)(20000 * (fps / 30.0));
}
else /* if (width * height <= 3840 * 2160) */ {
return (int)(40000 * (fps / 30.0));
// Don't scale bitrate linearly beyond 60 FPS. It's definitely not a linear
// bitrate increase for frame rate once we get to values that high.
double frameRateFactor = (fps <= 60 ? fps : (Math.sqrt(fps / 60.f) * 60.f)) / 30.f;
// TODO: Collect some empirical data to see if these defaults make sense.
// We're just using the values that the Shield used, as we have for years.
int[] pixelVals = {
640 * 360,
854 * 480,
1280 * 720,
1920 * 1080,
2560 * 1440,
3840 * 2160,
-1,
};
int[] factorVals = {
1,
2,
5,
10,
20,
40,
-1
};
// Calculate the resolution factor by linear interpolation of the resolution table
float resolutionFactor;
int pixels = width * height;
for (int i = 0; ; i++) {
if (pixels == pixelVals[i]) {
// We can bail immediately for exact matches
resolutionFactor = factorVals[i];
break;
}
else if (pixels < pixelVals[i]) {
if (i == 0) {
// Never go below the lowest resolution entry
resolutionFactor = factorVals[i];
}
else {
// Interpolate between the entry greater than the chosen resolution (i) and the entry less than the chosen resolution (i-1)
resolutionFactor = ((float)(pixels - pixelVals[i-1]) / (pixelVals[i] - pixelVals[i-1])) * (factorVals[i] - factorVals[i-1]) + factorVals[i-1];
}
break;
}
else if (pixelVals[i] == -1) {
// Never go above the highest resolution entry
resolutionFactor = factorVals[i-1];
break;
}
}
return (int)Math.round(resolutionFactor * frameRateFactor) * 1000;
}
public static boolean getDefaultSmallMode(Context context) {
@@ -205,22 +346,71 @@ public class PreferenceConfiguration {
prefs.getString(FPS_PREF_STRING, DEFAULT_FPS));
}
private static int getVideoFormatValue(Context context) {
private static FormatOption getVideoFormatValue(Context context) {
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
String str = prefs.getString(VIDEO_FORMAT_PREF_STRING, DEFAULT_VIDEO_FORMAT);
if (str.equals("auto")) {
return AUTOSELECT_H265;
return FormatOption.AUTO;
}
else if (str.equals("forceav1")) {
return FormatOption.FORCE_AV1;
}
else if (str.equals("forceh265")) {
return FORCE_H265_ON;
return FormatOption.FORCE_HEVC;
}
else if (str.equals("neverh265")) {
return FORCE_H265_OFF;
return FormatOption.FORCE_H264;
}
else {
// Should never get here
return AUTOSELECT_H265;
return FormatOption.AUTO;
}
}
private static int getFramePacingValue(Context context) {
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
// Migrate legacy never drop frames option to the new location
if (prefs.contains(LEGACY_DISABLE_FRAME_DROP_PREF_STRING)) {
boolean legacyNeverDropFrames = prefs.getBoolean(LEGACY_DISABLE_FRAME_DROP_PREF_STRING, false);
prefs.edit()
.remove(LEGACY_DISABLE_FRAME_DROP_PREF_STRING)
.putString(FRAME_PACING_PREF_STRING, legacyNeverDropFrames ? "balanced" : "latency")
.apply();
}
String str = prefs.getString(FRAME_PACING_PREF_STRING, DEFAULT_FRAME_PACING);
if (str.equals("latency")) {
return FRAME_PACING_MIN_LATENCY;
}
else if (str.equals("balanced")) {
return FRAME_PACING_BALANCED;
}
else if (str.equals("cap-fps")) {
return FRAME_PACING_CAP_FPS;
}
else if (str.equals("smoothness")) {
return FRAME_PACING_MAX_SMOOTHNESS;
}
else {
// Should never get here
return FRAME_PACING_MIN_LATENCY;
}
}
private static AnalogStickForScrolling getAnalogStickForScrollingValue(Context context) {
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
String str = prefs.getString(ANALOG_SCROLLING_PREF_STRING, DEFAULT_ANALOG_STICK_FOR_SCROLLING);
if (str.equals("right")) {
return AnalogStickForScrolling.RIGHT;
}
else if (str.equals("left")) {
return AnalogStickForScrolling.LEFT;
}
else {
return AnalogStickForScrolling.NONE;
}
}
@@ -236,14 +426,37 @@ public class PreferenceConfiguration {
.remove(VIDEO_FORMAT_PREF_STRING)
.remove(ENABLE_HDR_PREF_STRING)
.remove(UNLOCK_FPS_STRING)
.remove(FULL_RANGE_PREF_STRING)
.apply();
}
public static void completeLanguagePreferenceMigration(Context context) {
// Put our language option back to default which tells us that we've already migrated it
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
prefs.edit().putString(LANGUAGE_PREF_STRING, DEFAULT_LANGUAGE).apply();
}
public static boolean isShieldAtvFirmwareWithBrokenHdr() {
// This particular Shield TV firmware crashes when using HDR
// https://www.nvidia.com/en-us/geforce/forums/notifications/comment/155192/
return Build.MANUFACTURER.equalsIgnoreCase("NVIDIA") &&
Build.FINGERPRINT.contains("PPR1.180610.011/4079208_2235.1395");
}
public static PreferenceConfiguration readPreferences(Context context) {
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
PreferenceConfiguration config = new PreferenceConfiguration();
// Migrate legacy preferences to the new locations
if (prefs.contains(LEGACY_ENABLE_51_SURROUND_PREF_STRING)) {
if (prefs.getBoolean(LEGACY_ENABLE_51_SURROUND_PREF_STRING, false)) {
prefs.edit()
.remove(LEGACY_ENABLE_51_SURROUND_PREF_STRING)
.putString(AUDIO_CONFIG_PREF_STRING, "51")
.apply();
}
}
String str = prefs.getString(LEGACY_RES_FPS_PREF_STRING, null);
if (str != null) {
if (str.equals("360p30")) {
@@ -302,21 +515,59 @@ public class PreferenceConfiguration {
else {
// Use the new preference location
String resStr = prefs.getString(RESOLUTION_PREF_STRING, PreferenceConfiguration.DEFAULT_RESOLUTION);
// Convert legacy resolution strings to the new style
if (!resStr.contains("x")) {
resStr = PreferenceConfiguration.convertFromLegacyResolutionString(resStr);
prefs.edit().putString(RESOLUTION_PREF_STRING, resStr).apply();
}
config.width = PreferenceConfiguration.getWidthFromResolutionString(resStr);
config.height = PreferenceConfiguration.getHeightFromResolutionString(resStr);
config.fps = Integer.parseInt(prefs.getString(FPS_PREF_STRING, PreferenceConfiguration.DEFAULT_FPS));
}
if (!prefs.contains(SMALL_ICONS_PREF_STRING)) {
// We need to write small icon mode's default to disk for the settings page to display
// the current state of the option properly
prefs.edit().putBoolean(SMALL_ICONS_PREF_STRING, getDefaultSmallMode(context)).apply();
}
if (!prefs.contains(GAMEPAD_MOTION_SENSORS_PREF_STRING) && Build.VERSION.SDK_INT == Build.VERSION_CODES.S) {
// Android 12 has a nasty bug that causes crashes when the app touches the InputDevice's
// associated InputDeviceSensorManager (just calling getSensorManager() is enough).
// As a workaround, we will override the default value for the gamepad motion sensor
// option to disabled on Android 12 to reduce the impact of this bug.
// https://cs.android.com/android/_/android/platform/frameworks/base/+/8970010a5e9f3dc5c069f56b4147552accfcbbeb
prefs.edit().putBoolean(GAMEPAD_MOTION_SENSORS_PREF_STRING, false).apply();
}
// This must happen after the preferences migration to ensure the preferences are populated
config.bitrate = prefs.getInt(BITRATE_PREF_STRING, prefs.getInt(BITRATE_PREF_OLD_STRING, 0) * 1000);
if (config.bitrate == 0) {
config.bitrate = getDefaultBitrate(context);
}
String audioConfig = prefs.getString(AUDIO_CONFIG_PREF_STRING, DEFAULT_AUDIO_CONFIG);
if (audioConfig.equals("71")) {
config.audioConfiguration = MoonBridge.AUDIO_CONFIGURATION_71_SURROUND;
}
else if (audioConfig.equals("51")) {
config.audioConfiguration = MoonBridge.AUDIO_CONFIGURATION_51_SURROUND;
}
else /* if (audioConfig.equals("2")) */ {
config.audioConfiguration = MoonBridge.AUDIO_CONFIGURATION_STEREO;
}
config.videoFormat = getVideoFormatValue(context);
config.framePacing = getFramePacingValue(context);
config.analogStickForScrolling = getAnalogStickForScrollingValue(context);
config.deadzonePercentage = prefs.getInt(DEADZONE_PREF_STRING, DEFAULT_DEADZONE);
config.oscOpacity = prefs.getInt(OSC_OPACITY_PREF_STRING, DEFAULT_OPACITY);
config.language = prefs.getString(LANGUAGE_PREF_STRING, DEFAULT_LANGUAGE);
// Checkbox preferences
@@ -324,15 +575,13 @@ public class PreferenceConfiguration {
config.enableSops = prefs.getBoolean(SOPS_PREF_STRING, DEFAULT_SOPS);
config.stretchVideo = prefs.getBoolean(STRETCH_PREF_STRING, DEFAULT_STRETCH);
config.playHostAudio = prefs.getBoolean(HOST_AUDIO_PREF_STRING, DEFAULT_HOST_AUDIO);
config.listMode = prefs.getBoolean(LIST_MODE_PREF_STRING, DEFAULT_LIST_MODE);
config.smallIconMode = prefs.getBoolean(SMALL_ICONS_PREF_STRING, getDefaultSmallMode(context));
config.multiController = prefs.getBoolean(MULTI_CONTROLLER_PREF_STRING, DEFAULT_MULTI_CONTROLLER);
config.enable51Surround = prefs.getBoolean(ENABLE_51_SURROUND_PREF_STRING, DEFAULT_ENABLE_51_SURROUND);
config.usbDriver = prefs.getBoolean(USB_DRIVER_PREF_SRING, DEFAULT_USB_DRIVER);
config.onscreenController = prefs.getBoolean(ONSCREEN_CONTROLLER_PREF_STRING, ONSCREEN_CONTROLLER_DEFAULT);
config.onlyL3R3 = prefs.getBoolean(ONLY_L3_R3_PREF_STRING, ONLY_L3_R3_DEFAULT);
config.disableFrameDrop = prefs.getBoolean(DISABLE_FRAME_DROP_PREF_STRING, DEFAULT_DISABLE_FRAME_DROP);
config.enableHdr = prefs.getBoolean(ENABLE_HDR_PREF_STRING, DEFAULT_ENABLE_HDR);
config.showGuideButton = prefs.getBoolean(SHOW_GUIDE_BUTTON_PREF_STRING, SHOW_GUIDE_BUTTON_DEFAULT);
config.enableHdr = prefs.getBoolean(ENABLE_HDR_PREF_STRING, DEFAULT_ENABLE_HDR) && !isShieldAtvFirmwareWithBrokenHdr();
config.enablePip = prefs.getBoolean(ENABLE_PIP_PREF_STRING, DEFAULT_ENABLE_PIP);
config.enablePerfOverlay = prefs.getBoolean(ENABLE_PERF_OVERLAY_STRING, DEFAULT_ENABLE_PERF_OVERLAY);
config.bindAllUsb = prefs.getBoolean(BIND_ALL_USB_STRING, DEFAULT_BIND_ALL_USB);
@@ -341,6 +590,17 @@ public class PreferenceConfiguration {
config.unlockFps = prefs.getBoolean(UNLOCK_FPS_STRING, DEFAULT_UNLOCK_FPS);
config.vibrateOsc = prefs.getBoolean(VIBRATE_OSC_PREF_STRING, DEFAULT_VIBRATE_OSC);
config.vibrateFallbackToDevice = prefs.getBoolean(VIBRATE_FALLBACK_PREF_STRING, DEFAULT_VIBRATE_FALLBACK);
config.vibrateFallbackToDeviceStrength = prefs.getInt(VIBRATE_FALLBACK_STRENGTH_PREF_STRING, DEFAULT_VIBRATE_FALLBACK_STRENGTH);
config.flipFaceButtons = prefs.getBoolean(FLIP_FACE_BUTTONS_PREF_STRING, DEFAULT_FLIP_FACE_BUTTONS);
config.touchscreenTrackpad = prefs.getBoolean(TOUCHSCREEN_TRACKPAD_PREF_STRING, DEFAULT_TOUCHSCREEN_TRACKPAD);
config.enableLatencyToast = prefs.getBoolean(LATENCY_TOAST_PREF_STRING, DEFAULT_LATENCY_TOAST);
config.absoluteMouseMode = prefs.getBoolean(ABSOLUTE_MOUSE_MODE_PREF_STRING, DEFAULT_ABSOLUTE_MOUSE_MODE);
config.enableAudioFx = prefs.getBoolean(ENABLE_AUDIO_FX_PREF_STRING, DEFAULT_ENABLE_AUDIO_FX);
config.reduceRefreshRate = prefs.getBoolean(REDUCE_REFRESH_RATE_PREF_STRING, DEFAULT_REDUCE_REFRESH_RATE);
config.fullRange = prefs.getBoolean(FULL_RANGE_PREF_STRING, DEFAULT_FULL_RANGE);
config.gamepadTouchpadAsMouse = prefs.getBoolean(GAMEPAD_TOUCHPAD_AS_MOUSE_PREF_STRING, DEFAULT_GAMEPAD_TOUCHPAD_AS_MOUSE);
config.gamepadMotionSensors = prefs.getBoolean(GAMEPAD_MOTION_SENSORS_PREF_STRING, DEFAULT_GAMEPAD_MOTION_SENSORS);
config.gamepadMotionSensorsFallbackToDevice = prefs.getBoolean(GAMEPAD_MOTION_FALLBACK_PREF_STRING, DEFAULT_GAMEPAD_MOTION_FALLBACK);
return config;
}

View File

@@ -5,6 +5,7 @@ import android.content.Context;
import android.os.Bundle;
import android.preference.DialogPreference;
import android.util.AttributeSet;
import android.util.Log;
import android.view.Gravity;
import android.view.View;
import android.view.View.OnClickListener;
@@ -13,6 +14,8 @@ import android.widget.LinearLayout;
import android.widget.SeekBar;
import android.widget.TextView;
import java.util.Locale;
// Based on a Stack Overflow example: http://stackoverflow.com/questions/1974193/slider-on-my-preferencescreen
public class SeekBarPreference extends DialogPreference
{
@@ -29,6 +32,8 @@ public class SeekBarPreference extends DialogPreference
private final int maxValue;
private final int minValue;
private final int stepSize;
private final int keyStepSize;
private final int divisor;
private int currentValue;
public SeekBarPreference(Context context, AttributeSet attrs) {
@@ -58,6 +63,8 @@ public class SeekBarPreference extends DialogPreference
maxValue = attrs.getAttributeIntValue(ANDROID_SCHEMA_URL, "max", 100);
minValue = attrs.getAttributeIntValue(SEEKBAR_SCHEMA_URL, "min", 1);
stepSize = attrs.getAttributeIntValue(SEEKBAR_SCHEMA_URL, "step", 1);
divisor = attrs.getAttributeIntValue(SEEKBAR_SCHEMA_URL, "divisor", 1);
keyStepSize = attrs.getAttributeIntValue(SEEKBAR_SCHEMA_URL, "keyStep", 0);
}
@Override
@@ -78,6 +85,8 @@ public class SeekBarPreference extends DialogPreference
valueText = new TextView(context);
valueText.setGravity(Gravity.CENTER_HORIZONTAL);
valueText.setTextSize(32);
// Default text for value; hides bug where OnSeekBarChangeListener isn't called when opacity is 0%
valueText.setText("0%");
params = new LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.WRAP_CONTENT);
@@ -98,7 +107,14 @@ public class SeekBarPreference extends DialogPreference
return;
}
String t = String.valueOf(value);
String t;
if (divisor != 1) {
float floatValue = roundedValue / (float)divisor;
t = String.format((Locale)null, "%.1f", floatValue);
}
else {
t = String.valueOf(value);
}
valueText.setText(suffix == null ? t : t.concat(suffix.length() > 1 ? " "+suffix : suffix));
}
@@ -116,6 +132,9 @@ public class SeekBarPreference extends DialogPreference
}
seekBar.setMax(maxValue);
if (keyStepSize != 0) {
seekBar.setKeyProgressIncrement(keyStepSize);
}
seekBar.setProgress(currentValue);
return layout;
@@ -125,6 +144,9 @@ public class SeekBarPreference extends DialogPreference
protected void onBindDialogView(View v) {
super.onBindDialogView(v);
seekBar.setMax(maxValue);
if (keyStepSize != 0) {
seekBar.setKeyProgressIncrement(keyStepSize);
}
seekBar.setProgress(currentValue);
}

View File

@@ -1,37 +1,57 @@
package com.limelight.preferences;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.content.res.Configuration;
import android.media.MediaCodecInfo;
import android.os.Build;
import android.os.Bundle;
import android.app.Activity;
import android.os.Handler;
import android.os.Vibrator;
import android.preference.CheckBoxPreference;
import android.preference.ListPreference;
import android.preference.Preference;
import android.preference.PreferenceCategory;
import android.preference.PreferenceFragment;
import android.preference.PreferenceManager;
import android.preference.PreferenceScreen;
import android.util.DisplayMetrics;
import android.util.Range;
import android.view.Display;
import android.view.DisplayCutout;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowInsets;
import com.limelight.LimeLog;
import com.limelight.PcView;
import com.limelight.R;
import com.limelight.binding.video.MediaCodecHelper;
import com.limelight.utils.Dialog;
import com.limelight.utils.UiHelper;
import java.lang.reflect.Method;
import java.util.Arrays;
public class StreamSettings extends Activity {
private PreferenceConfiguration previousPrefs;
private int previousDisplayPixelCount;
// HACK for Android 9
static DisplayCutout displayCutoutP;
void reloadSettings() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
Display.Mode mode = getWindowManager().getDefaultDisplay().getMode();
previousDisplayPixelCount = mode.getPhysicalWidth() * mode.getPhysicalHeight();
}
getFragmentManager().beginTransaction().replace(
R.id.stream_settings, new SettingsFragment()
).commit();
).commitAllowingStateLoss();
}
@Override
@@ -43,28 +63,67 @@ public class StreamSettings extends Activity {
UiHelper.setLocale(this);
setContentView(R.layout.activity_stream_settings);
reloadSettings();
UiHelper.notifyNewRootView(this);
}
@Override
public void onAttachedToWindow() {
super.onAttachedToWindow();
// We have to use this hack on Android 9 because we don't have Display.getCutout()
// which was added in Android 10.
if (Build.VERSION.SDK_INT == Build.VERSION_CODES.P) {
// Insets can be null when the activity is recreated on screen rotation
// https://stackoverflow.com/questions/61241255/windowinsets-getdisplaycutout-is-null-everywhere-except-within-onattachedtowindo
WindowInsets insets = getWindow().getDecorView().getRootWindowInsets();
if (insets != null) {
displayCutoutP = insets.getDisplayCutout();
}
}
reloadSettings();
}
@Override
public void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
Display.Mode mode = getWindowManager().getDefaultDisplay().getMode();
// If the display's physical pixel count has changed, we consider that it's a new display
// and we should reload our settings (which include display-dependent values).
//
// NB: We aren't using displayId here because that stays the same (DEFAULT_DISPLAY) when
// switching between screens on a foldable device.
if (mode.getPhysicalWidth() * mode.getPhysicalHeight() != previousDisplayPixelCount) {
reloadSettings();
}
}
}
@Override
// NOTE: This will NOT be called on Android 13+ with android:enableOnBackInvokedCallback="true"
public void onBackPressed() {
finish();
// Check for changes that require a UI reload to take effect
PreferenceConfiguration newPrefs = PreferenceConfiguration.readPreferences(this);
if (newPrefs.listMode != previousPrefs.listMode ||
newPrefs.smallIconMode != previousPrefs.smallIconMode ||
!newPrefs.language.equals(previousPrefs.language)) {
// Restart the PC view to apply UI changes
Intent intent = new Intent(this, PcView.class);
intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK | Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(intent, null);
// Language changes are handled via configuration changes in Android 13+,
// so manual activity relaunching is no longer required.
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
PreferenceConfiguration newPrefs = PreferenceConfiguration.readPreferences(this);
if (!newPrefs.language.equals(previousPrefs.language)) {
// Restart the PC view to apply UI changes
Intent intent = new Intent(this, PcView.class);
intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK | Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(intent, null);
}
}
}
public static class SettingsFragment extends PreferenceFragment {
private int nativeResolutionStartIndex = Integer.MAX_VALUE;
private boolean nativeFramerateShown = false;
private void setValue(String preferenceKey, String value) {
ListPreference pref = (ListPreference) findPreference(preferenceKey);
@@ -72,6 +131,88 @@ public class StreamSettings extends Activity {
pref.setValue(value);
}
private void appendPreferenceEntry(ListPreference pref, String newEntryName, String newEntryValue) {
CharSequence[] newEntries = Arrays.copyOf(pref.getEntries(), pref.getEntries().length + 1);
CharSequence[] newValues = Arrays.copyOf(pref.getEntryValues(), pref.getEntryValues().length + 1);
// Add the new option
newEntries[newEntries.length - 1] = newEntryName;
newValues[newValues.length - 1] = newEntryValue;
pref.setEntries(newEntries);
pref.setEntryValues(newValues);
}
private void addNativeResolutionEntry(int nativeWidth, int nativeHeight, boolean insetsRemoved, boolean portrait) {
ListPreference pref = (ListPreference) findPreference(PreferenceConfiguration.RESOLUTION_PREF_STRING);
String newName;
if (insetsRemoved) {
newName = getResources().getString(R.string.resolution_prefix_native_fullscreen);
}
else {
newName = getResources().getString(R.string.resolution_prefix_native);
}
if (PreferenceConfiguration.isSquarishScreen(nativeWidth, nativeHeight)) {
if (portrait) {
newName += " " + getResources().getString(R.string.resolution_prefix_native_portrait);
}
else {
newName += " " + getResources().getString(R.string.resolution_prefix_native_landscape);
}
}
newName += " ("+nativeWidth+"x"+nativeHeight+")";
String newValue = nativeWidth+"x"+nativeHeight;
// Check if the native resolution is already present
for (CharSequence value : pref.getEntryValues()) {
if (newValue.equals(value.toString())) {
// It is present in the default list, so don't add it again
return;
}
}
if (pref.getEntryValues().length < nativeResolutionStartIndex) {
nativeResolutionStartIndex = pref.getEntryValues().length;
}
appendPreferenceEntry(pref, newName, newValue);
}
private void addNativeResolutionEntries(int nativeWidth, int nativeHeight, boolean insetsRemoved) {
if (PreferenceConfiguration.isSquarishScreen(nativeWidth, nativeHeight)) {
addNativeResolutionEntry(nativeHeight, nativeWidth, insetsRemoved, true);
}
addNativeResolutionEntry(nativeWidth, nativeHeight, insetsRemoved, false);
}
private void addNativeFrameRateEntry(float framerate) {
int frameRateRounded = Math.round(framerate);
if (frameRateRounded == 0) {
return;
}
ListPreference pref = (ListPreference) findPreference(PreferenceConfiguration.FPS_PREF_STRING);
String fpsValue = Integer.toString(frameRateRounded);
String fpsName = getResources().getString(R.string.resolution_prefix_native) +
" (" + fpsValue + " " + getResources().getString(R.string.fps_suffix_fps) + ")";
// Check if the native frame rate is already present
for (CharSequence value : pref.getEntryValues()) {
if (fpsValue.equals(value.toString())) {
// It is present in the default list, so don't add it again
nativeFramerateShown = false;
return;
}
}
appendPreferenceEntry(pref, fpsName, fpsValue);
nativeFramerateShown = true;
}
private void removeValue(String preferenceKey, String value, Runnable onMatched) {
int matchingCount = 0;
@@ -108,8 +249,6 @@ public class StreamSettings extends Activity {
pref.setEntryValues(entryValues);
}
private void resetBitrateToDefault(SharedPreferences prefs, String res, String fps) {
if (res == null) {
res = prefs.getString(PreferenceConfiguration.RESOLUTION_PREF_STRING, PreferenceConfiguration.DEFAULT_RESOLUTION);
@@ -139,28 +278,118 @@ public class StreamSettings extends Activity {
PreferenceScreen screen = getPreferenceScreen();
// hide on-screen controls category on non touch screen devices
if (!getActivity().getPackageManager().
hasSystemFeature("android.hardware.touchscreen")) {
if (!getActivity().getPackageManager().hasSystemFeature(PackageManager.FEATURE_TOUCHSCREEN)) {
PreferenceCategory category =
(PreferenceCategory) findPreference("category_onscreen_controls");
screen.removePreference(category);
}
// Remove PiP mode on devices pre-Oreo
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
// Hide remote desktop mouse mode on pre-Oreo (which doesn't have pointer capture)
// and NVIDIA SHIELD devices (which support raw mouse input in pointer capture mode)
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O ||
getActivity().getPackageManager().hasSystemFeature("com.nvidia.feature.shield")) {
PreferenceCategory category =
(PreferenceCategory) findPreference("category_basic_settings");
(PreferenceCategory) findPreference("category_input_settings");
category.removePreference(findPreference("checkbox_absolute_mouse_mode"));
}
// Hide gamepad motion sensor option when running on OSes before Android 12.
// Support for motion, LED, battery, and other extensions were introduced in S.
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
PreferenceCategory category =
(PreferenceCategory) findPreference("category_gamepad_settings");
category.removePreference(findPreference("checkbox_gamepad_motion_sensors"));
}
// Hide gamepad motion sensor fallback option if the device has no gyro or accelerometer
if (!getActivity().getPackageManager().hasSystemFeature(PackageManager.FEATURE_SENSOR_ACCELEROMETER) &&
!getActivity().getPackageManager().hasSystemFeature(PackageManager.FEATURE_SENSOR_GYROSCOPE)) {
PreferenceCategory category =
(PreferenceCategory) findPreference("category_gamepad_settings");
category.removePreference(findPreference("checkbox_gamepad_motion_fallback"));
}
// Hide USB driver options on devices without USB host support
if (!getActivity().getPackageManager().hasSystemFeature(PackageManager.FEATURE_USB_HOST)) {
PreferenceCategory category =
(PreferenceCategory) findPreference("category_gamepad_settings");
category.removePreference(findPreference("checkbox_usb_bind_all"));
category.removePreference(findPreference("checkbox_usb_driver"));
}
// Remove PiP mode on devices pre-Oreo, where the feature is not available (some low RAM devices),
// and on Fire OS where it violates the Amazon App Store guidelines for some reason.
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O ||
!getActivity().getPackageManager().hasSystemFeature("android.software.picture_in_picture") ||
getActivity().getPackageManager().hasSystemFeature("com.amazon.software.fireos")) {
PreferenceCategory category =
(PreferenceCategory) findPreference("category_ui_settings");
category.removePreference(findPreference("checkbox_enable_pip"));
}
int maxSupportedFps = 0;
// Fire TV apps are not allowed to use WebViews or browsers, so hide the Help category
/*if (getActivity().getPackageManager().hasSystemFeature("amazon.hardware.fire_tv")) {
PreferenceCategory category =
(PreferenceCategory) findPreference("category_help");
screen.removePreference(category);
}*/
PreferenceCategory category_gamepad_settings =
(PreferenceCategory) findPreference("category_gamepad_settings");
// Remove the vibration options if the device can't vibrate
if (!((Vibrator)getActivity().getSystemService(Context.VIBRATOR_SERVICE)).hasVibrator()) {
category_gamepad_settings.removePreference(findPreference("checkbox_vibrate_fallback"));
category_gamepad_settings.removePreference(findPreference("seekbar_vibrate_fallback_strength"));
// The entire OSC category may have already been removed by the touchscreen check above
PreferenceCategory category = (PreferenceCategory) findPreference("category_onscreen_controls");
if (category != null) {
category.removePreference(findPreference("checkbox_vibrate_osc"));
}
}
else if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O ||
!((Vibrator)getActivity().getSystemService(Context.VIBRATOR_SERVICE)).hasAmplitudeControl() ) {
// Remove the vibration strength selector of the device doesn't have amplitude control
category_gamepad_settings.removePreference(findPreference("seekbar_vibrate_fallback_strength"));
}
Display display = getActivity().getWindowManager().getDefaultDisplay();
float maxSupportedFps = display.getRefreshRate();
// Hide non-supported resolution/FPS combinations
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
Display display = getActivity().getWindowManager().getDefaultDisplay();
int maxSupportedResW = 0;
// Add a native resolution with any insets included for users that don't want content
// behind the notch of their display
boolean hasInsets = false;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
DisplayCutout cutout;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
// Use the much nicer Display.getCutout() API on Android 10+
cutout = display.getCutout();
}
else {
// Android 9 only
cutout = displayCutoutP;
}
if (cutout != null) {
int widthInsets = cutout.getSafeInsetLeft() + cutout.getSafeInsetRight();
int heightInsets = cutout.getSafeInsetBottom() + cutout.getSafeInsetTop();
if (widthInsets != 0 || heightInsets != 0) {
DisplayMetrics metrics = new DisplayMetrics();
display.getRealMetrics(metrics);
int width = Math.max(metrics.widthPixels - widthInsets, metrics.heightPixels - heightInsets);
int height = Math.min(metrics.widthPixels - widthInsets, metrics.heightPixels - heightInsets);
addNativeResolutionEntries(width, height, false);
hasInsets = true;
}
}
}
// Always allow resolutions that are smaller or equal to the active
// display resolution because decoders can report total non-sense to us.
// For example, a p201 device reports:
@@ -176,6 +405,13 @@ public class StreamSettings extends Activity {
int width = Math.max(candidate.getPhysicalWidth(), candidate.getPhysicalHeight());
int height = Math.min(candidate.getPhysicalWidth(), candidate.getPhysicalHeight());
// Some TVs report strange values here, so let's avoid native resolutions on a TV
// unless they report greater than 4K resolutions.
if (!getActivity().getPackageManager().hasSystemFeature(PackageManager.FEATURE_TELEVISION) ||
(width > 3840 || height > 2160)) {
addNativeResolutionEntries(width, height, hasInsets);
}
if ((width >= 3840 || height >= 2160) && maxSupportedResW < 3840) {
maxSupportedResW = 3840;
}
@@ -187,7 +423,7 @@ public class StreamSettings extends Activity {
}
if (candidate.getRefreshRate() > maxSupportedFps) {
maxSupportedFps = (int)candidate.getRefreshRate();
maxSupportedFps = candidate.getRefreshRate();
}
}
@@ -241,33 +477,33 @@ public class StreamSettings extends Activity {
if (maxSupportedResW != 0) {
if (maxSupportedResW < 3840) {
// 4K is unsupported
removeValue(PreferenceConfiguration.RESOLUTION_PREF_STRING, "4K", new Runnable() {
removeValue(PreferenceConfiguration.RESOLUTION_PREF_STRING, PreferenceConfiguration.RES_4K, new Runnable() {
@Override
public void run() {
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(SettingsFragment.this.getActivity());
setValue(PreferenceConfiguration.RESOLUTION_PREF_STRING, "1440p");
setValue(PreferenceConfiguration.RESOLUTION_PREF_STRING, PreferenceConfiguration.RES_1440P);
resetBitrateToDefault(prefs, null, null);
}
});
}
if (maxSupportedResW < 2560) {
// 1440p is unsupported
removeValue(PreferenceConfiguration.RESOLUTION_PREF_STRING, "1440p", new Runnable() {
removeValue(PreferenceConfiguration.RESOLUTION_PREF_STRING, PreferenceConfiguration.RES_1440P, new Runnable() {
@Override
public void run() {
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(SettingsFragment.this.getActivity());
setValue(PreferenceConfiguration.RESOLUTION_PREF_STRING, "1080p");
setValue(PreferenceConfiguration.RESOLUTION_PREF_STRING, PreferenceConfiguration.RES_1080P);
resetBitrateToDefault(prefs, null, null);
}
});
}
if (maxSupportedResW < 1920) {
// 1080p is unsupported
removeValue(PreferenceConfiguration.RESOLUTION_PREF_STRING, "1080p", new Runnable() {
removeValue(PreferenceConfiguration.RESOLUTION_PREF_STRING, PreferenceConfiguration.RES_1080P, new Runnable() {
@Override
public void run() {
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(SettingsFragment.this.getActivity());
setValue(PreferenceConfiguration.RESOLUTION_PREF_STRING, "720p");
setValue(PreferenceConfiguration.RESOLUTION_PREF_STRING, PreferenceConfiguration.RES_720P);
resetBitrateToDefault(prefs, null, null);
}
});
@@ -275,6 +511,15 @@ public class StreamSettings extends Activity {
// Never remove 720p
}
}
else {
// We can get the true metrics via the getRealMetrics() function (unlike the lies
// that getWidth() and getHeight() tell to us).
DisplayMetrics metrics = new DisplayMetrics();
display.getRealMetrics(metrics);
int width = Math.max(metrics.widthPixels, metrics.heightPixels);
int height = Math.min(metrics.widthPixels, metrics.heightPixels);
addNativeResolutionEntries(width, height, false);
}
if (!PreferenceConfiguration.readPreferences(this.getActivity()).unlockFps) {
// We give some extra room in case the FPS is rounded down
@@ -301,38 +546,31 @@ public class StreamSettings extends Activity {
}
// Never remove 30 FPS or 60 FPS
}
addNativeFrameRateEntry(maxSupportedFps);
// Android L introduces the drop duplicate behavior of releaseOutputBuffer()
// that the unlock FPS option relies on to not massively increase latency.
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
LimeLog.info("Excluding unlock FPS toggle based on OS");
PreferenceCategory category =
(PreferenceCategory) findPreference("category_basic_settings");
category.removePreference(findPreference("checkbox_unlock_fps"));
}
else {
findPreference(PreferenceConfiguration.UNLOCK_FPS_STRING).setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
@Override
public boolean onPreferenceChange(Preference preference, Object newValue) {
// HACK: We need to let the preference change succeed before reinitializing to ensure
// it's reflected in the new layout.
final Handler h = new Handler();
h.postDelayed(new Runnable() {
@Override
public void run() {
// Ensure the activity is still open when this timeout expires
StreamSettings settingsActivity = (StreamSettings)SettingsFragment.this.getActivity();
if (settingsActivity != null) {
settingsActivity.reloadSettings();
}
findPreference(PreferenceConfiguration.UNLOCK_FPS_STRING).setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
@Override
public boolean onPreferenceChange(Preference preference, Object newValue) {
// HACK: We need to let the preference change succeed before reinitializing to ensure
// it's reflected in the new layout.
final Handler h = new Handler();
h.postDelayed(new Runnable() {
@Override
public void run() {
// Ensure the activity is still open when this timeout expires
StreamSettings settingsActivity = (StreamSettings) SettingsFragment.this.getActivity();
if (settingsActivity != null) {
settingsActivity.reloadSettings();
}
}, 500);
}
}, 500);
// Allow the original preference change to take place
return true;
}
});
}
// Allow the original preference change to take place
return true;
}
});
// Remove HDR preference for devices below Nougat
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
@@ -342,7 +580,6 @@ public class StreamSettings extends Activity {
category.removePreference(findPreference("checkbox_enable_hdr"));
}
else {
Display display = getActivity().getWindowManager().getDefaultDisplay();
Display.HdrCapabilities hdrCaps = display.getHdrCapabilities();
// We must now ensure our display is compatible with HDR10
@@ -352,6 +589,7 @@ public class StreamSettings extends Activity {
for (int hdrType : hdrCaps.getSupportedHdrTypes()) {
if (hdrType == Display.HdrCapabilities.HDR_TYPE_HDR10) {
foundHdr10 = true;
break;
}
}
}
@@ -362,6 +600,15 @@ public class StreamSettings extends Activity {
(PreferenceCategory) findPreference("category_advanced_settings");
category.removePreference(findPreference("checkbox_enable_hdr"));
}
else if (PreferenceConfiguration.isShieldAtvFirmwareWithBrokenHdr()) {
LimeLog.info("Disabling HDR toggle on old broken SHIELD TV firmware");
PreferenceCategory category =
(PreferenceCategory) findPreference("category_advanced_settings");
CheckBoxPreference hdrPref = (CheckBoxPreference) category.findPreference("checkbox_enable_hdr");
hdrPref.setEnabled(false);
hdrPref.setChecked(false);
hdrPref.setSummary("Update the firmware on your NVIDIA SHIELD Android TV to enable HDR");
}
}
// Add a listener to the FPS and resolution preference
@@ -372,6 +619,25 @@ public class StreamSettings extends Activity {
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(SettingsFragment.this.getActivity());
String valueStr = (String) newValue;
// Detect if this value is the native resolution option
CharSequence[] values = ((ListPreference)preference).getEntryValues();
boolean isNativeRes = true;
for (int i = 0; i < values.length; i++) {
// Look for a match prior to the start of the native resolution entries
if (valueStr.equals(values[i].toString()) && i < nativeResolutionStartIndex) {
isNativeRes = false;
break;
}
}
// If this is native resolution, show the warning dialog
if (isNativeRes) {
Dialog.displayDialog(getActivity(),
getResources().getString(R.string.title_native_res_dialog),
getResources().getString(R.string.text_native_res_dialog),
false);
}
// Write the new bitrate value
resetBitrateToDefault(prefs, valueStr, null);
@@ -385,6 +651,15 @@ public class StreamSettings extends Activity {
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(SettingsFragment.this.getActivity());
String valueStr = (String) newValue;
// If this is native frame rate, show the warning dialog
CharSequence[] values = ((ListPreference)preference).getEntryValues();
if (nativeFramerateShown && values[values.length - 1].toString().equals(newValue.toString())) {
Dialog.displayDialog(getActivity(),
getResources().getString(R.string.title_native_fps_dialog),
getResources().getString(R.string.text_native_res_dialog),
false);
}
// Write the new bitrate value
resetBitrateToDefault(prefs, null, valueStr);

View File

@@ -0,0 +1,44 @@
package com.limelight.preferences;
import android.annotation.TargetApi;
import android.content.Context;
import android.os.Build;
import android.preference.Preference;
import android.util.AttributeSet;
import com.limelight.utils.HelpLauncher;
public class WebLauncherPreference extends Preference {
private String url;
public WebLauncherPreference(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
initialize(attrs);
}
public WebLauncherPreference(Context context, AttributeSet attrs) {
super(context, attrs);
initialize(attrs);
}
public WebLauncherPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
initialize(attrs);
}
private void initialize(AttributeSet attrs) {
if (attrs == null) {
throw new IllegalStateException("WebLauncherPreference must have attributes!");
}
url = attrs.getAttributeValue(null, "url");
if (url == null) {
throw new IllegalStateException("WebLauncherPreference must have 'url' attribute!");
}
}
@Override
public void onClick() {
HelpLauncher.launchUrl(getContext(), url);
}
}

View File

@@ -30,6 +30,6 @@ public class AdapterFragment extends Fragment {
@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
callbacks.receiveAbsListView((AbsListView) getView().findViewById(R.id.fragmentView));
callbacks.receiveAbsListView(getView().findViewById(R.id.fragmentView));
}
}

View File

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

View File

@@ -30,7 +30,6 @@ public class StreamView extends SurfaceView {
super(context, attrs, defStyleAttr);
}
@TargetApi(21)
public StreamView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}

View File

@@ -3,41 +3,22 @@ package com.limelight.utils;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.net.Uri;
import com.limelight.HelpActivity;
public class HelpLauncher {
private static boolean isKnownBrowser(Context context, Intent i) {
ResolveInfo resolvedActivity = context.getPackageManager().resolveActivity(i, PackageManager.MATCH_DEFAULT_ONLY);
if (resolvedActivity == null) {
// No browser
return false;
}
String name = resolvedActivity.activityInfo.name;
if (name == null) {
return false;
}
name = name.toLowerCase();
return name.contains("chrome") || name.contains("firefox");
}
private static void launchUrl(Context context, String url) {
public static void launchUrl(Context context, String url) {
// Try to launch the default browser
try {
Intent i = new Intent(Intent.ACTION_VIEW);
i.setData(Uri.parse(url));
// Several Android TV devices will lie and say they do have a browser
// even though the OS just shows an error dialog if we try to use it. We need to
// be a bit more clever on these devices and detect if the browser is a legitimate
// browser or just a fake error message activity.
if (!context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_LEANBACK) ||
isKnownBrowser(context, i)) {
// Several Android TV devices will lie and say they do have a browser even though the OS
// just shows an error dialog if we try to use it. We used to try to be clever and check
// the package name of the resolved intent, but it's not worth it anymore with Android 11's
// package visibility changes. We'll just always use the WebView on Android TV.
if (!context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_LEANBACK)) {
context.startActivity(i);
return;
}
@@ -63,4 +44,8 @@ public class HelpLauncher {
public static void launchTroubleshooting(Context context) {
launchUrl(context, "https://github.com/moonlight-stream/moonlight-docs/wiki/Troubleshooting");
}
public static void launchGameStreamEolFaq(Context context) {
launchUrl(context, "https://github.com/moonlight-stream/moonlight-docs/wiki/NVIDIA-GameStream-End-Of-Service-Announcement-FAQ");
}
}

View File

@@ -0,0 +1,32 @@
package com.limelight.utils;
import android.content.Context;
import android.net.ConnectivityManager;
import android.net.Network;
import android.net.NetworkCapabilities;
import android.net.NetworkInfo;
import android.os.Build;
public class NetHelper {
public static boolean isActiveNetworkVpn(Context context) {
ConnectivityManager connMgr = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
Network activeNetwork = connMgr.getActiveNetwork();
if (activeNetwork != null) {
NetworkCapabilities netCaps = connMgr.getNetworkCapabilities(activeNetwork);
if (netCaps != null) {
return netCaps.hasTransport(NetworkCapabilities.TRANSPORT_VPN) ||
!netCaps.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN);
}
}
}
else {
NetworkInfo activeNetworkInfo = connMgr.getActiveNetworkInfo();
if (activeNetworkInfo != null) {
return activeNetworkInfo.getType() == ConnectivityManager.TYPE_VPN;
}
}
return false;
}
}

View File

@@ -11,9 +11,10 @@ import com.limelight.ShortcutTrampoline;
import com.limelight.binding.PlatformBinding;
import com.limelight.computers.ComputerManagerService;
import com.limelight.nvstream.http.ComputerDetails;
import com.limelight.nvstream.http.GfeHttpResponseException;
import com.limelight.nvstream.http.HostHttpResponseException;
import com.limelight.nvstream.http.NvApp;
import com.limelight.nvstream.http.NvHTTP;
import com.limelight.nvstream.jni.MoonBridge;
import org.xmlpull.v1.XmlPullParserException;
@@ -23,7 +24,12 @@ import java.net.UnknownHostException;
import java.security.cert.CertificateEncodingException;
public class ServerHelper {
public static String getCurrentAddressFromComputer(ComputerDetails computer) {
public static final String CONNECTION_TEST_SERVER = "android.conntest.moonlight-stream.org";
public static ComputerDetails.AddressTuple getCurrentAddressFromComputer(ComputerDetails computer) throws IOException {
if (computer.activeAddress == null) {
throw new IOException("No active address for "+computer.name);
}
return computer.activeAddress;
}
@@ -49,7 +55,9 @@ public class ServerHelper {
public static Intent createStartIntent(Activity parent, NvApp app, ComputerDetails computer,
ComputerManagerService.ComputerManagerBinder managerBinder) {
Intent intent = new Intent(parent, Game.class);
intent.putExtra(Game.EXTRA_HOST, getCurrentAddressFromComputer(computer));
intent.putExtra(Game.EXTRA_HOST, computer.activeAddress.address);
intent.putExtra(Game.EXTRA_PORT, computer.activeAddress.port);
intent.putExtra(Game.EXTRA_HTTPS_PORT, computer.httpsPort);
intent.putExtra(Game.EXTRA_APP_NAME, app.getAppName());
intent.putExtra(Game.EXTRA_APP_ID, app.getAppId());
intent.putExtra(Game.EXTRA_APP_HDR, app.isHdrSupported());
@@ -68,14 +76,45 @@ public class ServerHelper {
public static void doStart(Activity parent, NvApp app, ComputerDetails computer,
ComputerManagerService.ComputerManagerBinder managerBinder) {
if (computer.state == ComputerDetails.State.OFFLINE ||
ServerHelper.getCurrentAddressFromComputer(computer) == null) {
if (computer.state == ComputerDetails.State.OFFLINE || computer.activeAddress == null) {
Toast.makeText(parent, parent.getResources().getString(R.string.pair_pc_offline), Toast.LENGTH_SHORT).show();
return;
}
parent.startActivity(createStartIntent(parent, app, computer, managerBinder));
}
public static void doNetworkTest(final Activity parent) {
new Thread(new Runnable() {
@Override
public void run() {
SpinnerDialog spinnerDialog = SpinnerDialog.displayDialog(parent,
parent.getResources().getString(R.string.nettest_title_waiting),
parent.getResources().getString(R.string.nettest_text_waiting),
false);
int ret = MoonBridge.testClientConnectivity(CONNECTION_TEST_SERVER, 443, MoonBridge.ML_PORT_FLAG_ALL);
spinnerDialog.dismiss();
String dialogSummary;
if (ret == MoonBridge.ML_TEST_RESULT_INCONCLUSIVE) {
dialogSummary = parent.getResources().getString(R.string.nettest_text_inconclusive);
}
else if (ret == 0) {
dialogSummary = parent.getResources().getString(R.string.nettest_text_success);
}
else {
dialogSummary = parent.getResources().getString(R.string.nettest_text_failure);
dialogSummary += MoonBridge.stringifyPortFlags(ret, "\n");
}
Dialog.displayDialog(parent,
parent.getResources().getString(R.string.nettest_title_done),
dialogSummary,
false);
}
}).start();
}
public static void doQuit(final Activity parent,
final ComputerDetails computer,
final NvApp app,
@@ -88,14 +127,14 @@ public class ServerHelper {
NvHTTP httpConn;
String message;
try {
httpConn = new NvHTTP(ServerHelper.getCurrentAddressFromComputer(computer),
httpConn = new NvHTTP(ServerHelper.getCurrentAddressFromComputer(computer), computer.httpsPort,
managerBinder.getUniqueId(), computer.serverCert, PlatformBinding.getCryptoProvider(parent));
if (httpConn.quitApp()) {
message = parent.getResources().getString(R.string.applist_quit_success) + " " + app.getAppName();
} else {
message = parent.getResources().getString(R.string.applist_quit_fail) + " " + app.getAppName();
}
} catch (GfeHttpResponseException e) {
} catch (HostHttpResponseException e) {
if (e.getErrorCode() == 599) {
message = "This session wasn't started by this device," +
" so it cannot be quit. End streaming on the original " +

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