Compare commits

...

414 Commits

Author SHA1 Message Date
Cameron Gutman
1d9efb30e2 Update to version 3.1.10 2015-10-11 17:11:48 -07:00
Cameron Gutman
ed7be00881 Update IML file 2015-10-11 17:11:08 -07:00
Cameron Gutman
a6003f6bff Remove MediaTek decoders from the decoders that need bitstream restrictions. The correct fix was to lower level_idc to reduce the required buffering. On newer MediaTek chipsets, sending bitstream restrictions actually slows down decoding by a factor of 3. 2015-10-11 16:57:37 -07:00
Cameron Gutman
4619045375 Revert "Update common to increase SSL handshake timeout"
This reverts commit 57b0da1a3a.
2015-10-11 14:54:45 -07:00
Cameron Gutman
e61b8f1b34 Try a TCP connection before trying HTTPS to quickly eliminate transport layer connectivity issues 2015-10-11 14:39:02 -07:00
Cameron Gutman
79b6ec839a Fix machines becoming unreachable after they report IP addresses that they can't be contacted with 2015-10-11 14:16:38 -07:00
Cameron Gutman
fd12e30c53 Set constraint flags corresponding to Constrained High Profile on KitKat and higher. Fixes Nexus Player high latency on Android 6.0. 2015-10-10 23:28:48 -07:00
Cameron Gutman
87a9ca4318 Make touchscreen and stylus support more robust (supporting Bluetooth stylus in 6.0 and hopefully fixing broken touchscreen input on some devices) 2015-10-10 19:17:19 -07:00
Cameron Gutman
3f64411174 Only reload the PcView activity if UI settings were changed 2015-10-10 18:53:10 -07:00
Cameron Gutman
57b0da1a3a Update common to increase SSL handshake timeout 2015-10-10 18:15:01 -07:00
Cameron Gutman
7d3e74a67f Allow the offline context menu to be opened when the PC state is unknown 2015-10-10 18:10:57 -07:00
Cameron Gutman
d704e322df Use RGB_565 for box art to reduce image size in memory 2015-10-10 18:09:51 -07:00
Cameron Gutman
f598153818 Small improvements to Media Codec DR 2015-10-10 15:17:24 -07:00
Cameron Gutman
f395a0c170 Fix warning 2015-10-10 15:04:49 -07:00
Cameron Gutman
654b33d27f Update build tools version to 23.0.1 2015-10-10 15:04:16 -07:00
Cameron Gutman
6c12da96c9 Add patched Jcodec library built from master a5d138efec2e940897e7e3d91a63a1f58abedd95 with changes from https://github.com/jcodec/jcodec/pull/90 2015-10-10 15:03:47 -07:00
Cameron Gutman
1a6f639b81 Fix discovery issues when adding a PC 2015-10-10 14:43:29 -07:00
Cameron Gutman
59a00a38c9 Limit box art assets to 5 MB each to prevent OOM crashes 2015-10-10 14:43:17 -07:00
Cameron Gutman
2beee168e3 Update README.md with additional download links 2015-08-30 12:37:23 -07:00
Cameron Gutman
a92bbc7e5a Increment version to 3.1.9 2015-08-18 00:44:01 -07:00
Cameron Gutman
fbc921dd07 Update build files 2015-08-18 00:39:01 -07:00
Cameron Gutman
59c6c3d777 Target Android 6.0 SDK 2015-08-18 00:38:49 -07:00
Cameron Gutman
e7ab61c8d0 Use jmDNS 3.4.2 which works properly on Android 6.0 2015-08-17 23:14:20 -07:00
Cameron Gutman
7023760782 Use monotonic system time for rendering timestamps. This is required now in Android 6.0 since these timestamps are propagated to the codec. 2015-08-17 18:40:25 -07:00
Cameron Gutman
932ce435b5 Remove unnecessary buffer clear 2015-08-17 18:26:33 -07:00
Cameron Gutman
af384d88f7 Handle potentially poisoned bitmap cache caused by truncated images from the server 2015-08-17 17:59:15 -07:00
Cameron Gutman
792846ddad Update MediaCodec renderer to avoid deprecated features on Lollipop 2015-08-17 17:52:57 -07:00
Cameron Gutman
1187d9c78c Update libraries 2015-08-14 09:29:56 -07:00
Cameron Gutman
37db9ab072 Update common with latest fixes 2015-08-12 00:39:07 -07:00
Cameron Gutman
fb40060560 Upgrade common jar to incorporate IDR frame fix 2015-08-04 23:46:20 -07:00
Cameron Gutman
a4f4887647 Upgrade build tools and libraries 2015-08-04 23:46:03 -07:00
Cameron Gutman
f1d7f556fd Bump to version 3.1.8 2015-07-21 18:03:37 -07:00
Cameron Gutman
1e70e1d329 GFE 2.5.11 update to fix black screen on Fire TV Stick 2015-07-18 17:06:41 -07:00
Cameron Gutman
e02a009635 Add support for the Razer Serval controller. The start and select buttons are manually handled for devices without a mapping for them. The back button is ignored so it can be used to exit the stream. 2015-07-18 00:46:25 -07:00
Cameron Gutman
bd6ff35603 Update to 3.1.7 2015-06-15 10:37:58 -07:00
Cameron Gutman
1cb7727dc7 Update common 2015-06-15 10:28:31 -07:00
Cameron Gutman
0c73e3d0ae Only propagate a decoder exception if it happens at the beginning of a stream 2015-06-15 10:28:09 -07:00
Cameron Gutman
6371d364e1 Lint warning cleanup 2015-05-29 23:22:40 -05:00
Cameron Gutman
ded9c9140d Handle being online but not having a known reachability 2015-05-29 23:20:04 -05:00
Cameron Gutman
7c8a108e28 Use the leanback feature on API 21+ devices 2015-05-29 23:18:56 -05:00
Cameron Gutman
2a18ffcdba Update to Gradle 1.2.3 2015-05-19 10:10:18 -05:00
Cameron Gutman
381d0d5e81 Add support for multi-window functionality on Samsung devices 2015-05-10 00:02:04 -05:00
Cameron Gutman
be126acfd1 Update version info to 3.1.6 2015-05-05 20:52:53 -04:00
Cameron Gutman
fc2f5cfe4d Manually pass through Samsung capacitive buttons 2015-05-05 20:20:37 -04:00
Cameron Gutman
9878902a89 Use IDs to track controllers instead of descriptors. Fixes #64 2015-05-05 20:08:58 -04:00
Cameron Gutman
f1230d46f3 Android Studio 1.2 and Grade 1.2.2 update 2015-05-05 20:02:53 -04:00
Michelle Bergeron
d8822392f1 Link to site 2015-05-03 23:36:19 -07:00
Cameron Gutman
1d9cf71517 Total Eclipse of the Lime 2015-04-21 21:50:40 -04:00
Cameron Gutman
2160e87fef Fix division by zero in ARC 2015-03-31 20:29:22 -04:00
Cameron Gutman
88249ba8aa Enable direct submission for ARC 2015-03-31 19:59:16 -04:00
Cameron Gutman
2856617fb3 Only release controller numbers if they were reserved 2015-03-31 19:58:47 -04:00
Cameron Gutman
d822980d5a Fix missing close of Closeables caught by StrictMode 2015-03-29 23:25:00 -04:00
Cameron Gutman
b5ba59b413 Fix database reference leak 2015-03-29 23:06:32 -04:00
Cameron Gutman
1148e0163c Only assign a controller number when a valid controller input has been received. Fixes misdetection of other input devices as controllers (issue #65). 2015-03-29 22:54:48 -04:00
Cameron Gutman
cf36c7adb1 Increment version 2015-03-25 02:33:46 -04:00
Cameron Gutman
eac6998e17 Update the latency message strings to be more clear that this isn't end to end latency 2015-03-25 01:20:55 -04:00
Cameron Gutman
17afbffdb5 Include the time it takes to get an input buffer in the frame latency calculation 2015-03-25 01:08:23 -04:00
Cameron Gutman
072a439c2d Update common and decode unit API 2015-03-25 00:32:22 -04:00
Cameron Gutman
c533600983 Update for 3.1.3 release 2015-03-23 17:26:37 -04:00
Cameron Gutman
5847fbb6b6 Add TI decoders to the direct submit whitelist 2015-03-23 17:14:02 -04:00
Cameron Gutman
1876b30c1b Forgot this file 2015-03-23 16:51:57 -04:00
Cameron Gutman
5c71f55993 Add another Exynos prefix 2015-03-23 16:51:32 -04:00
Cameron Gutman
9c0960d03d Add options to quit and resume streaming from the PC view 2015-03-23 16:36:43 -04:00
Cameron Gutman
29a395f3f4 Prevent updating the UI while quitting is in progress 2015-03-23 15:57:29 -04:00
Cameron Gutman
a676b8d8e6 Restore the legacy path and only use direct submit for certain whitelisted decoders 2015-03-23 15:51:11 -04:00
Cameron Gutman
7ab0be3b62 Optimize app grid performance on lower end devices 2015-03-23 15:12:25 -04:00
Cameron Gutman
115853fed2 Update version to 3.1.3-beta1 2015-03-16 21:29:07 -04:00
Cameron Gutman
60beb81ae4 Target API 22 2015-03-16 21:28:49 -04:00
Cameron Gutman
5310375d42 Target Android 5.1 2015-03-16 21:28:33 -04:00
Cameron Gutman
7ce29e3a09 Add a workaround for the Nexus 9 dropping frames with the new renderer 2015-03-16 21:26:02 -04:00
Cameron Gutman
42c65f4f16 Use smaller deadzones for SHIELD controllers 2015-03-16 19:36:09 -04:00
Cameron Gutman
bf2cc2a4d5 Don't assign controller numbers to devices that don't have an analog stick 2015-03-16 19:35:43 -04:00
Cameron Gutman
6d6d7121f6 Remove the Playpad Pro hack that worked around an issue with old firmware and caused the D-pad to be unresponsive on updated firmware. Fixes #41 2015-03-15 14:30:56 -07:00
Cameron Gutman
2ab67380d6 Use direct submit decoding for MediaCodec. Based on my profiling of a few devices, dequeueInputBuffer and queueInputBuffer don't take much time anyway. It allows us to stop our semi-busy looping which saves power. The depacketizer can avoid expensive synchronization and additional context switching which costs time and CPU cycles. 2015-03-09 01:49:52 -05:00
Cameron Gutman
899387caa1 Use a separate executor for network loads to avoid stalling cached loads. Optimize background cache fill loads. 2015-03-02 18:34:21 -05:00
Cameron Gutman
56c8a9e6fe Use the regular serverinfo query to update the running status of apps 2015-03-02 17:05:45 -05:00
Cameron Gutman
896288a40b Use AsyncTasks and attached Drawables to track background image loading 2015-03-02 17:03:08 -05:00
Cameron Gutman
fc8ce5e4b9 Quiet down disk cache misses 2015-03-02 16:13:54 -05:00
Cameron Gutman
4affc3c4ce Update to 3.1.2 release 2015-02-27 18:12:49 -05:00
Cameron Gutman
067be54715 Show the discovery in progress view if no computers remain after one is deleted 2015-02-27 18:05:02 -05:00
Cameron Gutman
0dad2dc64b Only close the app list activity if the PC is offline not unknown 2015-02-27 15:15:01 -05:00
Cameron Gutman
867b703644 Evict cached bitmaps when closing the app list 2015-02-27 15:13:43 -05:00
Cameron Gutman
3d398ef6dd Update common 2015-02-27 14:22:35 -05:00
Cameron Gutman
85d95b2d8e Revert "Immediately show the PC as offline if the first poll fails"
This reverts commit 7b12fd1ad2.
2015-02-27 13:52:17 -05:00
Cameron Gutman
d091d9db6b Start apps by ID to work correctly with duplicate app names 2015-02-27 13:42:40 -05:00
Cameron Gutman
e081ab5239 Code cleanup and Lint suggestions 2015-02-27 01:43:24 -05:00
Cameron Gutman
7b12fd1ad2 Immediately show the PC as offline if the first poll fails 2015-02-27 01:33:33 -05:00
Cameron Gutman
80d8c5953e Rewrite the app art caching and fetching (again!) to finally address OOM problems and speed up art loading 2015-02-27 01:16:06 -05:00
Cameron Gutman
194037ff41 Clear the bitmap cache since it can get pretty large 2015-02-26 22:04:39 -05:00
Cameron Gutman
094d642739 Stop scaling bitmaps down 2015-02-26 22:04:22 -05:00
Cameron Gutman
010e03252e Encapsulate the cache IO streams in buffered streams 2015-02-26 21:39:26 -05:00
Cameron Gutman
98638186b5 Use weak references to allow the image views to be garbage collected while a load is in progress 2015-02-26 21:05:33 -05:00
Cameron Gutman
c5293ef21f Reduce the size of the LRU cache by 2 2015-02-26 21:04:40 -05:00
Cameron Gutman
366a1c91b8 Always close the output stream 2015-02-26 21:04:17 -05:00
Cameron Gutman
157450e674 Update common with stricter applist parser 2015-02-26 18:33:18 -05:00
Cameron Gutman
1b8d2bc81c Cancel asset fetching when the app view is paused 2015-02-26 18:30:02 -05:00
Cameron Gutman
f1787c43e5 Generalize the polling grace period to all users of CMS 2015-02-26 18:27:50 -05:00
Cameron Gutman
95ea88e932 Only replace the MAC address if the existing one is non-null 2015-02-26 15:11:46 -05:00
Cameron Gutman
f2b8461bb9 Increment version to beta 3.1.2 2015-02-25 23:15:10 -05:00
Cameron Gutman
7838a787df Fix a bug in app removal 2015-02-25 22:27:38 -05:00
Cameron Gutman
cc3f2ecb07 Always close the cache output stream if an exception occurs 2015-02-25 22:15:41 -05:00
Cameron Gutman
833b7c3916 Fetch app assets in the background while in the app view 2015-02-25 21:57:54 -05:00
Cameron Gutman
90209f2ca2 Update iml files generated by Android Studio 1.1 2015-02-25 21:07:59 -05:00
Cameron Gutman
2681036c32 Update common 2015-02-25 21:07:44 -05:00
Cameron Gutman
ee58071ff1 Fix huge performance issues when dealing with large app lists 2015-02-25 21:07:35 -05:00
Cameron Gutman
e222f2f6c3 Fix fast polling 2015-02-22 18:34:28 -05:00
Cameron Gutman
0b7becb161 Remove unused function 2015-02-22 18:10:08 -05:00
Cameron Gutman
bf795ab7a5 Fix removal of apps in app list updates 2015-02-22 18:04:42 -05:00
Cameron Gutman
59df38ae8a Cancel app icon requests when the view is recycled 2015-02-22 17:49:52 -05:00
Cameron Gutman
e04ff048b8 Implement a fast polling method to speed up polling. Save the old MAC address if it's empty. 2015-02-11 17:04:31 -05:00
Cameron Gutman
7d25d07c6d Update version to 3.1.1 2015-02-11 00:55:47 -05:00
Cameron Gutman
7b0ddfae42 Update iml files 2015-02-11 00:34:56 -05:00
Cameron Gutman
43fa1a7245 Update common to fix null app name issue 2015-02-09 00:12:29 -05:00
Cameron Gutman
057530eed0 Correctly identify computers that are the same 2015-02-08 23:46:03 -05:00
Cameron Gutman
aee34f6365 Remove redundant null checks 2015-02-08 23:44:33 -05:00
Cameron Gutman
5519d92243 Disable the start key shortcut to start the keyboard because the keyboard can't receive input after it's started 2015-02-07 13:58:53 -05:00
Cameron Gutman
3d95ac1f93 Fix keyboard dismissal on Fire TV devices 2015-02-07 13:42:49 -05:00
Cameron Gutman
5c938535be Fix app list focus issues with remotes/gamepads 2015-02-07 13:20:01 -05:00
Cameron Gutman
2fdecc551a Tabs -> Spaces 2015-02-07 11:54:46 -05:00
Cameron Gutman
10204afdb4 Only add PCs to the computer list when they have been polled once to get a UUID for equality comparison. Fix equality comparison in PcView to avoid duplicate PCs enumerated over mDNS. 2015-02-07 11:44:56 -05:00
Cameron Gutman
55c800c2a5 Fade in box art when scrolling 2015-02-07 06:52:28 -05:00
Cameron Gutman
265b3f9963 Use image alpha to make images transparent while loading 2015-02-07 06:23:35 -05:00
Cameron Gutman
a8bf2cd1cf Fix UI dropped frames when loading images 2015-02-07 06:08:00 -05:00
Cameron Gutman
4fcd8b3dfe Replace unpair option with delete PC 2015-02-07 05:57:30 -05:00
Cameron Gutman
e1a1a6344d Fill the whole height with the list view 2015-02-06 13:38:32 -05:00
Cameron Gutman
a095c10a25 Increment version to 3.1 and update build files 2015-02-05 16:15:16 -05:00
Cameron Gutman
b1ea487e22 Use the mode (power) button on the Asus Nexus Player Gamepad as a select button 2015-02-05 16:06:55 -05:00
Cameron Gutman
47265d0d10 Add another SELinux policy change needed on Nexus 9 2015-02-05 16:06:22 -05:00
Cameron Gutman
6a41b41a38 Merge pull request #55 from Ansa89/italian-translation
Italian translation: update
2015-02-05 13:34:03 -05:00
Cameron Gutman
2247e43a48 Remove unused imports 2015-02-05 13:23:01 -05:00
Cameron Gutman
d3986080a3 Tighten up a bunch of declarations to make Lint happier 2015-02-05 13:21:04 -05:00
Cameron Gutman
07277e1a5b Fix a few Lint warnings 2015-02-05 13:01:35 -05:00
Ansa89
39d7fc748f Italian translation: update 2015-02-03 01:06:36 +01:00
Cameron Gutman
4d3a69cf6a Fix GFE 2.1.x controller regression 2015-02-02 18:10:18 -05:00
Cameron Gutman
b806522751 Unassign the controller number when a device is removed 2015-02-02 02:13:27 -05:00
Cameron Gutman
256fa897a7 Fix build issues 2015-02-01 18:31:34 -05:00
Cameron Gutman
5c812eed6c Beta 2 version update 2015-02-01 18:20:55 -05:00
Cameron Gutman
f0b22f9119 Don't use small mode on TVs 2015-02-01 18:20:39 -05:00
Cameron Gutman
7e1884acb5 Trap Shield's back button as controller 0 2015-02-01 18:07:03 -05:00
Cameron Gutman
9512521783 Update common 2015-02-01 15:06:27 -05:00
Cameron Gutman
da7904a767 Add multiple controller support 2015-02-01 15:06:18 -05:00
Cameron Gutman
3a0c1db168 Merge pull request #54 from Ansa89/italian-translation
Italian translation: update
2015-02-01 00:44:17 -05:00
Cameron Gutman
bd21692323 Properly center text on the app view 2015-02-01 00:43:57 -05:00
Cameron Gutman
5ae245bdca Trim spaces from the IP address 2015-02-01 00:39:47 -05:00
Cameron Gutman
d3052cd97d Set small icon by default on phones 2015-02-01 00:33:43 -05:00
Cameron Gutman
336f85a31c Fix loading bugs with uncached images 2015-01-31 22:14:12 -05:00
Cameron Gutman
b01f7c796e Fix duplicated fragments 2015-01-31 17:19:54 -05:00
Cameron Gutman
56f438fe47 Fix some crashes and caching issues 2015-01-31 17:01:46 -05:00
Cameron Gutman
baa5199b83 Load cached images in the background to avoid stalling the UI thread 2015-01-31 16:59:45 -05:00
Cameron Gutman
23ca62b304 Fix dp constant 2015-01-31 14:00:47 -05:00
Cameron Gutman
2c3511195c Use small mode by default on things smaller than 7 inch tablets 2015-01-31 13:21:21 -05:00
Ansa89
d31ef481f3 Italian translation: update 2015-01-31 10:03:37 +01:00
Cameron Gutman
a490da5e5c Fix some caching bugs 2015-01-31 00:13:51 -05:00
Cameron Gutman
72d3576257 Fix a crash and a hang in the new computer manager code 2015-01-30 19:33:42 -05:00
Cameron Gutman
ebd93a55a0 Fix small icon mode 2015-01-30 19:17:00 -05:00
Cameron Gutman
4d01e1afe6 Stub icon scaling and allow background updating of the applist 2015-01-30 18:49:01 -05:00
Cameron Gutman
9ff1386751 Add a quit confirmation dialog 2015-01-27 15:31:01 -05:00
Cameron Gutman
5fca35f0b1 Sort app list alphabetically 2015-01-26 20:58:33 -05:00
Cameron Gutman
d23c763441 Remove unused imports 2015-01-26 20:50:14 -05:00
Cameron Gutman
fa058c4783 Merge pull request #53 from Ansa89/italian-translation
Italian translation: update
2015-01-26 20:47:00 -05:00
Ansa89
e0ddd5f045 Italian translation: update 2015-01-26 10:53:30 +01:00
Cameron Gutman
b7443451a4 Fix release build failure for beta 2015-01-25 23:38:33 -05:00
Cameron Gutman
e90e4a22c4 Increment version 2015-01-25 23:35:24 -05:00
Cameron Gutman
3a53172145 Apply list mode preference immediately 2015-01-25 23:28:13 -05:00
Cameron Gutman
1dfcb7bc29 Fix root input device capture on the Nexus 9 2015-01-25 23:16:32 -05:00
Cameron Gutman
897bb76858 Forgot this file 2015-01-25 22:58:17 -05:00
Cameron Gutman
bcc67269ab Add gestures to bring up the software keyboard - Long press start or tap with 3 fingers 2015-01-25 22:55:12 -05:00
Cameron Gutman
4d24c654b9 Remove the old fragment when adding the new one 2015-01-25 22:11:38 -05:00
Cameron Gutman
cba44b091b Add common with GFE 2.1.x backwards compatibility 2015-01-25 21:35:03 -05:00
Cameron Gutman
f2d8f8a41b Update preference text 2015-01-25 21:04:27 -05:00
Cameron Gutman
4b1c7e7e3c Fix state loss crashes 2015-01-25 21:04:13 -05:00
Cameron Gutman
1cba278876 Cache box art locally 2015-01-25 21:00:34 -05:00
Cameron Gutman
766898fdf9 Add list support back for users that don't like the grid 2015-01-25 20:23:35 -05:00
Cameron Gutman
13e91d594b Fix Lint and build issues 2015-01-25 18:50:31 -05:00
Cameron Gutman
ca0a0da19f Fix fusion of computers that were re-added after becoming unreachable 2015-01-25 18:41:44 -05:00
Cameron Gutman
82cabce86e Merge pull request #33 from Ansa89/language_chooser
Add language chooser
2015-01-25 16:46:10 -05:00
Cameron Gutman
51a630995a Update common for GFE 2.2.2+ support 2015-01-22 15:29:58 -05:00
Cameron Gutman
3a74f0726c Updated libs 2015-01-22 15:29:46 -05:00
Cameron Gutman
efa6c7bba0 Fix build of root package 2015-01-22 15:29:16 -05:00
Michelle Bergeron
b8141542f8 Add wiki link 2015-01-17 18:44:31 -08:00
Cameron Gutman
8fc9a90207 Switch back to Maven repos of ion and androidasync packages 2014-12-13 20:41:32 -08:00
Cameron Gutman
13d707d98d Use final release of Gradle 1.0 2014-12-09 01:13:13 -08:00
Cameron Gutman
aae0ff6e7a Migrate the project to Android Studio 1.0 RC4 2014-12-08 00:12:37 -08:00
Cameron Gutman
69c7b5a0d5 Update version 2014-12-03 20:52:52 -08:00
Cameron Gutman
d1ad3115fa Add remote to stream config 2014-12-02 00:55:46 -08:00
Cameron Gutman
770af402a4 Reduce default 1080p60 bitrate to 20 Mbps 2014-12-02 00:55:31 -08:00
Cameron Gutman
3236c0b93a Lower the level_idc of the SPS to the minimum required for streaming at a given resolution 2014-12-01 22:58:52 -08:00
Cameron Gutman
51aacc3f38 Remove extra newlines 2014-12-01 22:39:17 -08:00
Cameron Gutman
397c6f46f9 Fix a security issue which caused input devices to remain world readable after the stream is ended 2014-12-01 22:29:16 -08:00
Cameron Gutman
d00f78f859 Revert square to circle analog work since it seems to be handled correctly already 2014-12-01 22:27:02 -08:00
Cameron Gutman
29fec2e0de Add initial support for rooted devices running Lollipop with SELinux set to enforcing. This should really be improved in the future since we're modifying policies for untrusted_app. 2014-12-01 22:26:35 -08:00
Cameron Gutman
88d28665ef Attempt to fix IndexOutOfBoundsException (index 0 size 0) reported by a couple users 2014-11-30 18:34:34 -06:00
Cameron Gutman
de1f4da258 Apply the square to circle plane mapping before evaluating the deadzone. Cleanup some dead code. 2014-11-30 15:52:49 -06:00
Cameron Gutman
7985be57ab Translate the analog stick values of controllers with "square" analog stick planes (DS3, DS4, and others) to the circular plane that XInput programs expect 2014-11-30 15:35:20 -06:00
Cameron Gutman
a835e7aaa2 Increase DS4 controller responsiveness by ignoring historical values again 2014-11-30 12:34:30 -06:00
Ansa89
22958cfbb1 Language chooser: use constants 2014-11-29 14:40:03 +01:00
Cameron Gutman
c4dc5eb9e1 Update common for faster IDR recovery 2014-11-28 22:17:42 -06:00
Cameron Gutman
db758f386e Comment out unused variable 2014-11-28 22:16:46 -06:00
Cameron Gutman
3fb3eefa94 Fix Nyko Playpad input issue 2014-11-28 22:16:33 -06:00
Ansa89
9340dff45d PreferenceConfiguration.java: add language preference 2014-11-28 10:26:14 +01:00
Cameron Gutman
2d6c756e70 Always consider a PC to be remote if localIP == remoteIP 2014-11-27 21:56:20 -06:00
Cameron Gutman
03e965d449 Merge pull request #34 from Ansa89/italian-translation
Italian translation: better wording
2014-11-27 20:35:57 -06:00
Cameron Gutman
34f72544d8 Increment version 2014-11-25 14:56:40 -08:00
Cameron Gutman
d839ea9781 Increase deadzone on triggers to Xinput defaults and add special handling of the Nexus Player Controller and Nexus Remote 2014-11-25 14:54:36 -08:00
Cameron Gutman
2b7f13fdbb Increase max frame time to improve accuracy of latency counter 2014-11-25 13:34:00 -08:00
Cameron Gutman
7557a3a4ae Don't capture the back button on remotes 2014-11-25 11:16:47 -08:00
Cameron Gutman
fcecba484f Fix a crash caught by Monkey 2014-11-25 02:05:24 -08:00
Cameron Gutman
fa85a0a0bd Improve CPU decoder frame latency when rendering speed is less than decoding speed 2014-11-25 02:04:51 -08:00
Cameron Gutman
dc64bfeba2 Slightly reduce max packet size in an attempt to cut packet losses 2014-11-25 01:05:55 -08:00
Cameron Gutman
871b73c48d Fix PC duplication issue when multiple machines report the same remote IP address 2014-11-24 20:10:02 -08:00
Cameron Gutman
5dcff91d27 Only grab Fire TV remotes if a gamepad isn't attached 2014-11-24 18:43:08 -08:00
Cameron Gutman
0041fc1dab Fix broken de-duplication of computers 2014-11-24 18:25:58 -08:00
Cameron Gutman
314242ab08 Update to Ion with fixes for SSLContext and self-signed certificates 2014-11-24 18:10:23 -08:00
Cameron Gutman
09e8ddfd74 Use the bitstream restrictions fixup on Broadcom VideoCore IV devices 2014-11-24 18:03:47 -08:00
Ansa89
4cea483a87 Italian translation: better wording 2014-11-24 11:53:18 +01:00
Ansa89
99aa616188 Add language chooser
Implement limelight-stream/limelight-android#32.
2014-11-24 11:47:02 +01:00
Cameron Gutman
444c4602c1 Update libraries. Seems to improve image caching behavior with Ion. 2014-11-23 23:39:20 -08:00
Cameron Gutman
5b6eac7140 Update build.gradle for re-release 2014-11-23 02:07:03 -08:00
Cameron Gutman
7cdd184197 Fix null pointer exception on ATV emulator 2014-11-23 02:06:49 -08:00
Cameron Gutman
be153b84cb Update build for final 3.0 release 2014-11-22 23:15:11 -08:00
Cameron Gutman
06c53e2251 Update decoder errata 2014-11-22 22:08:29 -08:00
Cameron Gutman
695519bdf5 Reduce Nexus Player video latency by 10x 2014-11-22 22:05:59 -08:00
Cameron Gutman
bf7d033ab2 Don't use adaptive playback at all to avoid extra added latency on some decoders 2014-11-22 20:35:31 -08:00
Cameron Gutman
df67795c4a Use back as start on Android TV 2014-11-22 19:33:26 -08:00
Cameron Gutman
72c1696f43 Fix missing PCs in PC list after my NPE fix 2014-11-21 22:56:56 -08:00
Cameron Gutman
8eca3683c9 Add method for getting video decoder name 2014-11-21 11:08:35 -08:00
Cameron Gutman
80c17b4913 Update common 2014-11-20 19:22:28 -08:00
Cameron Gutman
e5050f10bb Fix a potential null pointer exception 2014-11-20 19:22:20 -08:00
Cameron Gutman
e912e4de57 Don't do deadzone scaling because the PC should be handling that. Return to non-scaled controller packets. Disable the deadzone option in preferences. 2014-11-20 00:00:48 -08:00
Cameron Gutman
8dee1f0d80 Add a trigger deadzone 2014-11-19 23:59:42 -08:00
Cameron Gutman
53594ada66 Disable the Android TV controller hack for now 2014-11-19 23:27:10 -08:00
Cameron Gutman
848ed1ad72 Scale touch inputs based on the ratio of the stream size to the screen size 2014-11-19 23:26:50 -08:00
Cameron Gutman
307e807c8f Replay motion event history during input processing 2014-11-19 23:08:34 -08:00
Cameron Gutman
6a27780d56 Remove hat flat values 2014-11-19 22:57:17 -08:00
Cameron Gutman
57f98dbb4a Add missing import 2014-11-19 22:07:57 -08:00
Cameron Gutman
5af7d83ec1 Fix RTL Lint warnings by using start/end 2014-11-19 22:06:22 -08:00
Cameron Gutman
4a6f77f43a Remove an unused string 2014-11-19 22:05:51 -08:00
Cameron Gutman
c96f9fb635 Prevent deadzone and bitrate from dropping below 1 2014-11-19 20:11:13 -08:00
Cameron Gutman
e3a477a243 Don't send a bunch of duplicate controller packets if a button is being held down 2014-11-19 19:05:59 -08:00
Cameron Gutman
9fcd641143 Make the back button function as the start button on Android TV controllers (needs testing) 2014-11-19 18:40:22 -08:00
Cameron Gutman
6d1cbc5a64 Add a hack for the Tablet Remote app to fix the B button 2014-11-19 18:39:15 -08:00
Cameron Gutman
ec71060d98 Fix broken keyboards and gamepads when an input device wasn't provided (such as a virtual gamepad or IME) 2014-11-19 18:37:47 -08:00
Cameron Gutman
03f706fb85 Update common 2014-11-19 10:43:09 -08:00
Cameron Gutman
7ad87bd3ee Small fix to the frame timing code 2014-11-19 10:43:00 -08:00
Cameron Gutman
4e088f6183 Fix minor grammar error 2014-11-18 19:09:23 -08:00
Cameron Gutman
1b16ea6f53 Merge pull request #31 from Ansa89/NewUI-italian-translation
NewUI: Add italian translation
2014-11-17 19:51:38 -08:00
Ansa89
f262503bc8 Italian translation: update 2014-11-17 09:50:17 +01:00
Cameron Gutman
b2ba216cd1 Poll every 3 seconds instead of every 5 seconds 2014-11-16 18:14:50 -08:00
Cameron Gutman
94ba7f8e45 Fix a bunch of bugs in the new (and old) computer manager service 2014-11-16 18:09:31 -08:00
Cameron Gutman
a267cf59c7 Increment version 2014-11-16 17:20:27 -08:00
Cameron Gutman
79e8bef289 Update common 2014-11-16 17:20:11 -08:00
Cameron Gutman
99e3b5f33b Rewrite a large portion of the computer manager service to fix some thread leaks and improve performance 2014-11-16 17:20:04 -08:00
Cameron Gutman
afbe64f3ff Remove an unused import 2014-11-16 17:19:07 -08:00
Cameron Gutman
43b1a73ae0 Use a transparent background for the streaming activity to avoid overdraw 2014-11-16 17:18:53 -08:00
Cameron Gutman
d08eeb8a2d Don't display a toast after pairing has completed 2014-11-16 16:37:41 -08:00
Cameron Gutman
7c39e5c974 Fix a race condition 2014-11-16 14:57:54 -08:00
Cameron Gutman
cd49334199 If we've previously been able to reach a machine via a local or remote IP, always try that one first when polling on subsequent tries 2014-11-16 14:35:36 -08:00
Cameron Gutman
dd59f0bc6d Fix app grid UI issues 2014-11-16 14:27:20 -08:00
Cameron Gutman
cf2d83a1ea Fix comment typo 2014-11-16 14:23:58 -08:00
Cameron Gutman
d5b6130936 Use 40% larger packets (1450 bytes) on local networks 2014-11-16 12:09:32 -08:00
Cameron Gutman
4ae29b0075 Improve performance of the CPU decoder and add some details about changing decoders 2014-11-16 11:52:08 -08:00
Ansa89
34e35cd493 Add italian translation 2014-11-14 11:19:03 +01:00
Cameron Gutman
a17af070c5 Condense some text to better fit the UI 2014-11-13 23:30:42 -08:00
Cameron Gutman
fbe0a26800 Select the PC grid when the down button is pressed when focused on one of the buttons 2014-11-13 23:28:23 -08:00
Cameron Gutman
25ad99df94 Update common 2014-11-13 23:22:37 -08:00
Cameron Gutman
6338e7b8eb Add deadzone preference 2014-11-13 23:22:13 -08:00
Cameron Gutman
1b9846d519 Close the app list instead of displaying an error if the app view is resumed and fails to update 2014-11-13 22:31:19 -08:00
Cameron Gutman
a4ece13a1d Fix refreshing apps text 2014-11-13 22:30:45 -08:00
Cameron Gutman
066b8430a0 Update common with fix for 404 error message 2014-11-13 21:56:28 -08:00
Cameron Gutman
2b54a91f3d Replace ... with elipsis character 2014-11-13 21:50:12 -08:00
Cameron Gutman
2d01633372 Fix small error in strings.xml 2014-11-13 21:49:59 -08:00
Cameron Gutman
5dc01069fc Update PC view to avoid scrunched up text when looking for a PC on phones in portrait orientation 2014-11-13 21:49:44 -08:00
Cameron Gutman
d450008833 Don't use deprecated constants 2014-11-13 21:49:12 -08:00
Cameron Gutman
a37fff6eb5 Fix a bunch of Lint errors 2014-11-13 21:37:11 -08:00
Cameron Gutman
6604675bf9 Lint: Remove unused imports 2014-11-13 21:30:32 -08:00
Cameron Gutman
1965cc2347 Merge branch 'NewUI-prepare-for-translation' into NewUI
Conflicts:
	app/src/main/java/com/limelight/PcView.java
2014-11-13 21:30:11 -08:00
Cameron Gutman
312ca27906 Open the app list after successfully pairing 2014-11-13 21:22:45 -08:00
Cameron Gutman
0bceadbd9a New common with disabled FEC 2014-11-13 21:16:32 -08:00
Cameron Gutman
dfc3daabcd Use a 2 frame audio buffer if possible to reduce audio latency 2014-11-13 21:16:14 -08:00
Ansa89
b9ba9adc1f Forgot about these 2014-11-12 12:52:18 +01:00
Ansa89
f112d45e1a Some cleanup 2014-11-12 09:58:26 +01:00
Ansa89
88f139873c Resolve merge conflicts 2014-11-12 09:41:12 +01:00
Ansa89
d317c5bf03 Try to make limelight more translatable 2014-11-11 16:30:20 +01:00
Cameron Gutman
9d72314b9c Update common again 2014-11-11 01:14:32 -08:00
Cameron Gutman
2cc7243573 Update beta version 2014-11-10 22:06:02 -08:00
Cameron Gutman
269d9a6bc6 Update to support GFE 2.1.4 2014-11-10 22:01:19 -08:00
Cameron Gutman
244130fc1b Add visual indication when no PCs have been found yet 2014-11-08 13:56:16 -08:00
Cameron Gutman
a67791b8aa Display the delete PC option for local PCs too, even though it may not always work 2014-11-08 13:20:14 -08:00
Cameron Gutman
21e46a5c3b Display machines as they are being refreshed 2014-11-08 13:14:35 -08:00
Cameron Gutman
2df2f850d5 Remove dead code 2014-11-08 13:13:39 -08:00
Cameron Gutman
406d26ec1c Add visual feeback for offline machines and running games 2014-11-08 12:51:07 -08:00
Cameron Gutman
68c1aaf433 Add new app view UI 2014-11-08 01:07:21 -08:00
Cameron Gutman
9ef577dbdd Update UI for add PC 2014-11-07 23:09:45 -08:00
Cameron Gutman
982ecbc015 Improve the look of the buttons and PC view UI 2014-11-07 22:28:07 -08:00
Cameron Gutman
7e44b5abd5 Remove margins from landscape pc view 2014-11-07 01:20:55 -08:00
Cameron Gutman
6dbb1a0c1f Fix UI performance issues 2014-11-07 01:18:14 -08:00
Cameron Gutman
94b1c04fa6 GridView WIP 2014-11-07 00:27:58 -08:00
Cameron Gutman
9758276f1c Use the normal margins for AddComputerManually 2014-11-06 22:30:10 -08:00
Cameron Gutman
971263c52d Update common 2014-11-06 22:14:12 -08:00
Cameron Gutman
9b58e7bb4d Fix right clicking inconsistency on different devices 2014-11-06 20:38:29 -08:00
Cameron Gutman
69ecf0251d Forgot one activity 2014-11-06 20:07:59 -08:00
Cameron Gutman
350a4d8825 Add a helper class to perform initial UI fixups (currently adding padding on TV devices) 2014-11-06 20:07:01 -08:00
Cameron Gutman
44f447df7b Remove some old layout cruft 2014-11-06 20:01:32 -08:00
Cameron Gutman
e8c4df4897 Add new Material-style launcher assets 2014-11-06 09:29:28 -08:00
Cameron Gutman
5ee16124bc Add new banners from phantom-playR 2014-11-05 18:50:40 -08:00
Cameron Gutman
8702ac72f0 Update readme 2014-11-05 18:43:16 -08:00
Cameron Gutman
004552ec30 Update version 2014-11-02 21:24:52 -08:00
Cameron Gutman
2f28400234 Update option text 2014-11-02 21:20:01 -08:00
Cameron Gutman
78d213d686 Fix thread spawning issue and remove some dead code 2014-11-02 21:16:09 -08:00
Cameron Gutman
1a71dda243 Add support for local audio playback mode 2014-11-02 20:55:15 -08:00
Cameron Gutman
21822f259c Significantly improve speed of PC list updates 2014-11-02 14:30:06 -08:00
Cameron Gutman
4f79607015 Fix full-screen theme 2014-11-02 13:41:35 -08:00
Cameron Gutman
d8576d4c50 Auto-adjust bitrate when resolution/FPS changes 2014-11-02 13:25:43 -08:00
Cameron Gutman
2f4042da8f Pull preferences into their own class 2014-11-02 13:05:17 -08:00
Cameron Gutman
c1397e331b Finish GUI for all preferences supported by the old preferences views 2014-11-02 12:10:21 -08:00
Cameron Gutman
cd182b3265 Begin work on new preferences UI and massive code cleanup of settings-related activities 2014-10-30 01:27:43 -07:00
Cameron Gutman
28f2d7b84a Remove lint.xml 2014-10-30 00:25:32 -07:00
Cameron Gutman
e8de7908fd Fix a bunch of static analysis warnings 2014-10-30 00:21:34 -07:00
Cameron Gutman
419c4c5592 Fix warning in JNI code 2014-10-29 23:57:11 -07:00
Cameron Gutman
a9a8346f58 Fix app label 2014-10-29 23:42:37 -07:00
Cameron Gutman
7e1b3f861f Remove superfluous manifest information 2014-10-29 23:12:31 -07:00
Cameron Gutman
f4204e1268 Add missing version info 2014-10-29 23:05:34 -07:00
Cameron Gutman
60f35cd0aa Start of work to get both root and non-root versions building in the same branch/project 2014-10-29 22:47:47 -07:00
Cameron Gutman
bbcdaa94a1 Remove and ignore compiled JNI libraries 2014-10-29 21:55:54 -07:00
Cameron Gutman
8f6e8c00ef Replace BouncyCastle and Jcodec jars with Maven repo dependencies 2014-10-29 21:52:13 -07:00
Cameron Gutman
24cb347b10 Add license back 2014-10-29 21:39:05 -07:00
Cameron Gutman
d1b4e9464f Fix iml files 2014-10-29 21:19:17 -07:00
Cameron Gutman
18f7bfab7f Add some other files that weren't migrated with the project 2014-10-29 21:17:03 -07:00
Cameron Gutman
d84b4bcf9a Initial migration to Android Studio 2014-10-29 21:16:09 -07:00
Cameron Gutman
57d919798a Merge branch 'root'
Conflicts:
	AndroidManifest.xml
2014-10-28 20:42:00 -07:00
Michelle Bergeron
efeeebb0a2 fix grammar and limelight windows link 2014-10-23 01:33:20 -04:00
Cameron Gutman
bc1409ba6c Increment version 2014-10-21 19:24:43 -04:00
Cameron Gutman
6f05b2af8a Attempt to detect Exynos 4 to apply the bitstream fixup code 2014-10-21 19:20:54 -04:00
Cameron Gutman
d441bef33e Merge branch 'master' into root
Conflicts:
	libs/limelight-common.jar
2014-10-17 23:42:51 -07:00
Cameron Gutman
7b1f6ee483 Update common and disable the new renderer for now 2014-10-17 22:49:36 -07:00
Cameron Gutman
332960922a Small addendum to the timestamp fix 2014-10-17 15:54:07 -07:00
Cameron Gutman
ac03f73cf9 Add new experimental decoder code for Lollipop's asynchronous MediaCodec capability 2014-10-17 15:51:50 -07:00
Cameron Gutman
fa847ef2fc Ensure that no input buffers will ever be submitted with the same timestamp per SDK docs 2014-10-17 14:45:58 -07:00
Cameron Gutman
b19360ac75 Suppress deprecation warnings for MediaCodecList APIs 2014-10-17 14:27:33 -07:00
Cameron Gutman
562569dc6b Fix evdev_reader build on NDK r10c and add updated libevdev_reader.so binaries 2014-10-17 14:14:47 -07:00
Cameron Gutman
a18aa51f5a Merge branch 'master' into root
Conflicts:
	AndroidManifest.xml
	libs/limelight-common.jar
	res/values/strings.xml
2014-10-17 13:55:45 -07:00
Cameron Gutman
b492ac43f8 Rebuild libraries with NDK r10c 2014-10-17 13:33:14 -07:00
Cameron Gutman
5bf3efb247 Update for Android 5.0 2014-10-17 11:54:59 -07:00
Cameron Gutman
2d833c32b0 Update decoder errata with testing results 2014-10-17 09:58:48 -07:00
Cameron Gutman
19bade01b8 Move to the improved axis scaling algorithm 2014-10-16 21:17:35 -07:00
Cameron Gutman
1b991ba432 More layout and manifest fixes. Notably, moving most hardcoded strings to strings.xml 2014-10-16 21:05:46 -07:00
Cameron Gutman
9c48850bb7 More layout and manifest changes for Android TV 2014-10-16 20:09:06 -07:00
Cameron Gutman
3e6f5ff11c Remove v11 styles. Add Android TV banner. Disable title bar on themes. 2014-10-15 22:39:35 -07:00
Cameron Gutman
57c3d8af8b Add experimental support for Xbox 360 controller dpad events on devices without proper mappings 2014-10-14 22:55:22 -07:00
Cameron Gutman
8530451c8b Only get motion ranges from joystick or gamepad sources. Fixes reading deadzones from the wrong source when dealing with multiple input sources in the same device with overlapping axis values. 2014-10-14 21:56:02 -07:00
Cameron Gutman
69a5c0b5b3 Add bitstream restrictions to MediaTek devices and update decoder-errata.txt 2014-10-12 14:43:59 -07:00
Cameron Gutman
a7c36dcde6 Include video dimensions in RendererException 2014-10-12 13:32:34 -07:00
Cameron Gutman
dff6fc21f4 Increase minimum deadzone to 15% in an attempt to fix a reported deadzone issue 2014-10-12 10:02:10 -07:00
Cameron Gutman
895e0250d9 Increment app version and fix some manifest issues 2014-10-12 09:50:37 -07:00
Cameron Gutman
fd538cbaff Remove Samsung decoders from the list of bitstream_restrictions fixup devices 2014-10-11 21:48:56 -07:00
Cameron Gutman
27ce6fa203 Always patch num_ref_frames in the SPS to increase compatibility (read: attempt to fix some crashes on various Chinese SoCs) 2014-10-11 21:47:58 -07:00
Cameron Gutman
8efe194682 Merge branch 'master' into root 2014-10-10 22:55:37 -07:00
Cameron Gutman
947882d16f Fix modifier keys on root version 2014-10-10 22:53:34 -07:00
Cameron Gutman
f07e927103 Merge branch 'master' into root
Conflicts:
	libs/limelight-common.jar
2014-10-10 22:46:25 -07:00
Cameron Gutman
a61b85b494 Update common 2014-10-10 22:45:12 -07:00
Cameron Gutman
0c4a049a80 More performance optimizations by submitting a batch of decode units to get them to the hardware decoder ASAP 2014-10-10 17:51:39 -07:00
Cameron Gutman
8403101d0f Small performance optimization by only blocking on an input buffer if we've already got a DU 2014-10-09 19:05:57 -07:00
Cameron Gutman
47d47afd73 Update common 2014-10-07 23:18:08 -07:00
Cameron Gutman
1430801888 Fix a potential deadlock that could occur if the input buffers are exhausted. A couple other performance optimizations by overlapping some latency. 2014-10-07 23:17:32 -07:00
Cameron Gutman
6efc7e254b Don't prefer the newer Samsung decoder because it seems to cause issues on older devices 2014-10-07 23:16:07 -07:00
Cameron Gutman
ace1339811 Check local IP first always to properly handle cases where we're behind a router that won't get UDP traffic back to us if we use the remote address 2014-10-07 23:01:39 -07:00
Cameron Gutman
a5171a1701 Fix some localization issues (for Arabic at least) 2014-10-05 15:47:36 -07:00
Cameron Gutman
a50211ab95 Merge branch 'master' into root 2014-10-04 18:43:27 -07:00
Cameron Gutman
31677adaa0 Add code to handle OUYA reporting invalid flat values 2014-10-04 18:41:18 -07:00
Cameron Gutman
247a19766c Merge branch 'master' into root 2014-10-03 23:35:12 -07:00
Cameron Gutman
1f09cbd609 Fix mouse scrolling and remove unreachable code 2014-10-03 23:20:09 -07:00
Cameron Gutman
731e4dc31e Merge branch 'master' into root 2014-10-03 23:09:40 -07:00
Cameron Gutman
b6ee0764ff Clear connection state before stopping to avoid potential deadlocks 2014-10-03 23:07:42 -07:00
Cameron Gutman
cd4bf9a28b Merge branch 'master' into root
Conflicts:
	AndroidManifest.xml
	libs/limelight-common.jar
	src/com/limelight/Game.java
2014-10-03 23:05:06 -07:00
Cameron Gutman
f56b7ff79e Bump to 2.5.6 2014-10-03 22:56:30 -07:00
Cameron Gutman
645ea683ee Increase the minimum deadzone to 12% because I'm paranoid 2014-10-03 22:52:48 -07:00
Cameron Gutman
67e22fca6b Improve re-hiding of the system UI by using a proper system UI visibility listener 2014-10-03 22:49:36 -07:00
Cameron Gutman
a726ba8ea7 Add an ungrab key combo (Ctrl+Shift+Z). Ignore repeat key down events. Fix some mishandling of input events that could cause crashes. 2014-10-03 22:18:36 -07:00
Cameron Gutman
23fcaa1bab Add axis scaling support 2014-10-01 20:24:40 -07:00
Cameron Gutman
ad684a6f6b Merge version update 2014-09-28 16:38:56 -07:00
Cameron Gutman
d3438f4938 Update common jar 2014-09-28 16:37:51 -07:00
Cameron Gutman
cafdc21bf2 Prefer Samsung's OMX.SEC.AVC.Decoder if it's in the list of decoders 2014-09-28 16:37:41 -07:00
Cameron Gutman
ceb9bd3342 Change bitstream restrictions to match default values 2014-09-28 16:37:30 -07:00
Cameron Gutman
196c0e6cbc Update version 2014-09-28 16:30:37 -07:00
Cameron Gutman
e2cb7c953c Update common jar 2014-09-28 16:27:11 -07:00
Cameron Gutman
426b3c8522 Prefer Samsung's OMX.SEC.AVC.Decoder if it's in the list of decoders 2014-09-28 14:17:31 -07:00
Cameron Gutman
9648cf257f Change bitstream restrictions to match default values 2014-09-28 12:27:21 -07:00
Cameron Gutman
31d8687f67 Add failure tracing to EvdevReader 2014-09-27 20:02:10 -07:00
Cameron Gutman
991407a2cf Merge branch 'master' into root 2014-09-27 19:32:39 -07:00
Cameron Gutman
13b80eda8a Use a common cleanup function and stop input capturing after closing the connection to allow captured devices to accept the "Connection Failed" dialog 2014-09-27 19:32:10 -07:00
Cameron Gutman
6677949614 Update common jar 2014-09-27 15:49:59 -07:00
Cameron Gutman
080dcd92d7 Suppress a warning 2014-09-27 15:45:54 -07:00
Cameron Gutman
31b0bcf041 Fix manually adding PCs 2014-09-27 15:45:37 -07:00
Cameron Gutman
36664133f8 Speed up PC polling by only trying once if the remote and local IPs are the same 2014-09-27 15:43:43 -07:00
Cameron Gutman
a3106bffca Speed up initial discovery by generating a new keypair while discovering machines. 2014-09-27 15:42:12 -07:00
Cameron Gutman
94a26fb831 Force CPU decoding to low performance to make the experience less horrible 2014-09-26 20:50:34 -07:00
Cameron Gutman
1b026f1354 Merge branch 'master' into root
Conflicts:
	AndroidManifest.xml
	src/com/limelight/binding/input/evdev/EvdevWatcher.java
2014-09-20 02:54:15 -07:00
Cameron Gutman
0517e8a530 Increment version and update common jar 2014-09-20 02:36:14 -07:00
Cameron Gutman
a9fea34ac1 Add support for adaptive resolution changes. It's enabled by default on devices that claim support (KitKat+) or decoders that we know are okay. I pulled in jcodec to allow us to do proper SPS fixups without hardcoded offsets. 2014-09-19 22:25:38 -07:00
Cameron Gutman
201704dc9d Finally fix the random pairing failure. It turns out that it was causing by background querying for serverinfo during the pairing process. Now we stop polling computers while pairing is in progress. 2014-09-19 22:23:06 -07:00
Cameron Gutman
62ecb1af50 Use fixed point libopus builds on ARM and MIPS. This improves performance and allows the use of NEON on ARM for a huge perf boost 2014-09-17 19:36:51 -07:00
Cameron Gutman
9d4ca6293f Merge branch 'root' of github.com:limelight-stream/limelight-android into root 2014-09-17 19:31:07 -07:00
Cameron Gutman
2296b80edb Use fixed point libopus builds on ARM and MIPS. This improves performance and allows the use of NEON on ARM for a huge perf boost 2014-09-17 19:29:55 -07:00
Cameron Gutman
5577d48dcf Don't crash if no files are present in /dev/input 2014-09-15 18:50:38 -07:00
Cameron Gutman
e92a281fd8 Close the fd to wake the reading thread up for termination 2014-09-10 02:45:21 -07:00
Cameron Gutman
b4c3f9678a Use poll() to avoid an infinite blocking read() that causes ANRs during cleanup 2014-09-10 02:35:55 -07:00
Cameron Gutman
82f79c466a Version to 2.5.4.1 2014-09-10 01:57:59 -07:00
Cameron Gutman
d428f316b4 Don't unbind after an unexpected event 2014-09-10 01:57:24 -07:00
Cameron Gutman
828f4877b6 Only bind to keyboards and mice that aren't gamepads 2014-09-06 16:25:09 -07:00
Cameron Gutman
09e8e8e6b3 Remove isMouse() and replace it with more precise has*() functions 2014-09-06 16:03:09 -07:00
Cameron Gutman
77c8051ec6 Add support for keyboard and mouse combo devices in raw input mode 2014-09-06 14:09:09 -07:00
Cameron Gutman
6bae056e3a Fix a bug where an error change any permissions would cause the operation to fail and other files to not be changed 2014-09-03 23:33:25 -07:00
Cameron Gutman
bb869a51fd Start using com.limelight.root package name 2014-09-03 23:32:37 -07:00
Cameron Gutman
25b3d08bb9 Revert "Remove root-specific stuff. DO NOT MERGE TO root!"
This reverts commit 2c23dbd2be.
2014-09-03 23:08:54 -07:00
641 changed files with 10523 additions and 8352 deletions

View File

@@ -1,9 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<classpath>
<classpathentry kind="src" path="src"/>
<classpathentry kind="src" path="gen"/>
<classpathentry kind="con" path="com.android.ide.eclipse.adt.ANDROID_FRAMEWORK"/>
<classpathentry exported="true" kind="con" path="com.android.ide.eclipse.adt.LIBRARIES"/>
<classpathentry exported="true" kind="con" path="com.android.ide.eclipse.adt.DEPENDENCIES"/>
<classpathentry kind="output" path="bin/classes"/>
</classpath>

36
.gitignore vendored
View File

@@ -1 +1,35 @@
/bin/*
#built application files
*.apk
*.ap_
# files for the dex VM
*.dex
# Java class files
*.class
# generated files
bin/
gen/
# Local configuration file (sdk path, etc)
local.properties
# Windows thumbnail db
Thumbs.db
# OSX files
.DS_Store
# Eclipse project files
.classpath
.project
# Android Studio
.idea
#.idea/workspace.xml - remove # and delete .idea if it better suit your needs.
.gradle
build/
# Compiled JNI libraries folder
**/jniLibs

View File

View File

@@ -1,2 +0,0 @@
*** SESSION Sep 21, 2013 18:55:11.17 -------------------------------------------
*** SESSION Sep 21, 2013 18:55:55.08 -------------------------------------------

View File

@@ -1,3 +0,0 @@
com.android.ide.eclipse.adt.fixLegacyEditors=1
com.android.ide.eclipse.adt.sdk=C\:\\Users\\Andrew\\Desktop\\ADT\\adt-bundle-windows-x86_64-20130917\\sdk
eclipse.preferences.version=1

View File

@@ -1,4 +0,0 @@
eclipse.preferences.version=1
spelling_locale_initialized=true
useAnnotationsPrefPage=true
useQuickDiffPrefPage=true

View File

@@ -1,2 +0,0 @@
eclipse.preferences.version=1
version=1

View File

@@ -1,13 +0,0 @@
content_assist_proposals_background=255,255,255
content_assist_proposals_foreground=0,0,0
eclipse.preferences.version=1
fontPropagated=true
org.eclipse.jdt.ui.editor.tab.width=
org.eclipse.jdt.ui.formatterprofiles.version=12
org.eclipse.jdt.ui.javadoclocations.migrated=true
org.eclipse.jface.textfont=1|Courier New|10.0|0|WINDOWS|1|0|0|0|0|0|0|0|0|1|0|0|0|0|Courier New;
proposalOrderMigrated=true
spelling_locale_initialized=true
tabWidthPropagated=true
useAnnotationsPrefPage=true
useQuickDiffPrefPage=true

View File

@@ -1,5 +0,0 @@
PROBLEMS_FILTERS_MIGRATE=true
eclipse.preferences.version=1
platformState=1379804095671
quickStart=false
tipsAndTricks=true

View File

@@ -1,2 +0,0 @@
eclipse.preferences.version=1
showIntro=false

File diff suppressed because it is too large Load Diff

View File

@@ -1,2 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<typeInfoHistroy/>

View File

@@ -1,2 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<qualifiedTypeNameHistroy/>

View File

@@ -1,12 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<section name="Workbench">
<section name="org.eclipse.jdt.internal.ui.packageview.PackageExplorerPart">
<item value="true" key="group_libraries"/>
<item value="false" key="linkWithEditor"/>
<item value="2" key="layout"/>
<item value="1" key="rootMode"/>
<item value="&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot;?&gt;&#x0D;&#x0A;&lt;packageExplorer group_libraries=&quot;1&quot; layout=&quot;2&quot; linkWithEditor=&quot;0&quot; rootMode=&quot;1&quot; workingSetName=&quot;&quot;&gt;&#x0D;&#x0A;&lt;customFilters userDefinedPatternsEnabled=&quot;false&quot;&gt;&#x0D;&#x0A;&lt;xmlDefinedFilters&gt;&#x0D;&#x0A;&lt;child filterId=&quot;org.eclipse.jdt.ui.PackageExplorer.LibraryFilter&quot; isEnabled=&quot;false&quot;/&gt;&#x0D;&#x0A;&lt;child filterId=&quot;org.eclipse.jdt.ui.PackageExplorer.LocalTypesFilter&quot; isEnabled=&quot;false&quot;/&gt;&#x0D;&#x0A;&lt;child filterId=&quot;org.eclipse.jdt.ui.PackageExplorer.StaticsFilter&quot; isEnabled=&quot;false&quot;/&gt;&#x0D;&#x0A;&lt;child filterId=&quot;org.eclipse.jdt.ui.PackageExplorer.ClosedProjectsFilter&quot; isEnabled=&quot;false&quot;/&gt;&#x0D;&#x0A;&lt;child filterId=&quot;org.eclipse.jdt.ui.PackageExplorer.NonSharedProjectsFilter&quot; isEnabled=&quot;false&quot;/&gt;&#x0D;&#x0A;&lt;child filterId=&quot;org.eclipse.jdt.ui.PackageExplorer.NonJavaElementFilter&quot; isEnabled=&quot;false&quot;/&gt;&#x0D;&#x0A;&lt;child filterId=&quot;org.eclipse.jdt.ui.PackageExplorer.ContainedLibraryFilter&quot; isEnabled=&quot;false&quot;/&gt;&#x0D;&#x0A;&lt;child filterId=&quot;org.eclipse.jdt.ui.PackageExplorer.CuAndClassFileFilter&quot; isEnabled=&quot;false&quot;/&gt;&#x0D;&#x0A;&lt;child filterId=&quot;org.eclipse.jdt.ui.PackageExplorer.NonJavaProjectsFilter&quot; isEnabled=&quot;false&quot;/&gt;&#x0D;&#x0A;&lt;child filterId=&quot;org.eclipse.jdt.internal.ui.PackageExplorer.EmptyInnerPackageFilter&quot; isEnabled=&quot;true&quot;/&gt;&#x0D;&#x0A;&lt;child filterId=&quot;org.eclipse.jdt.ui.PackageExplorer.PackageDeclarationFilter&quot; isEnabled=&quot;true&quot;/&gt;&#x0D;&#x0A;&lt;child filterId=&quot;org.eclipse.jdt.internal.ui.PackageExplorer.EmptyPackageFilter&quot; isEnabled=&quot;false&quot;/&gt;&#x0D;&#x0A;&lt;child filterId=&quot;org.eclipse.jdt.ui.PackageExplorer.ImportDeclarationFilter&quot; isEnabled=&quot;true&quot;/&gt;&#x0D;&#x0A;&lt;child filterId=&quot;org.eclipse.jdt.ui.PackageExplorer.FieldsFilter&quot; isEnabled=&quot;false&quot;/&gt;&#x0D;&#x0A;&lt;child filterId=&quot;org.eclipse.jdt.internal.ui.PackageExplorer.HideInnerClassFilesFilter&quot; isEnabled=&quot;true&quot;/&gt;&#x0D;&#x0A;&lt;child filterId=&quot;org.eclipse.jdt.ui.PackageExplorer.NonPublicFilter&quot; isEnabled=&quot;false&quot;/&gt;&#x0D;&#x0A;&lt;child filterId=&quot;org.eclipse.jdt.ui.PackageExplorer_patternFilterId_.*&quot; isEnabled=&quot;true&quot;/&gt;&#x0D;&#x0A;&lt;child filterId=&quot;org.eclipse.jdt.ui.PackageExplorer.EmptyLibraryContainerFilter&quot; isEnabled=&quot;true&quot;/&gt;&#x0D;&#x0A;&lt;child filterId=&quot;org.eclipse.jdt.ui.PackageExplorer.SyntheticMembersFilter&quot; isEnabled=&quot;true&quot;/&gt;&#x0D;&#x0A;&lt;/xmlDefinedFilters&gt;&#x0D;&#x0A;&lt;/customFilters&gt;&#x0D;&#x0A;&lt;/packageExplorer&gt;" key="memento"/>
</section>
<section name="JavaElementSearchActions">
</section>
</section>

View File

@@ -1,15 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<section name="Workbench">
<section name="ChooseWorkspaceDialogSettings">
<item value="185" key="DIALOG_Y_ORIGIN"/>
<item value="381" key="DIALOG_X_ORIGIN"/>
</section>
<section name="WORKBENCH_SETTINGS">
<list key="ENABLED_TRANSFERS">
</list>
</section>
<section name="ExternalProjectImportWizard">
<item value="false" key="WizardProjectsImportPage.STORE_ARCHIVE_SELECTED"/>
<item value="false" key="WizardProjectsImportPage.STORE_COPY_PROJECT_ID"/>
</section>
</section>

View File

@@ -1,17 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<section name="Workbench">
<section name="org.eclipse.ui.internal.QuickAccess">
<item value="1025" key="dialogWidth"/>
<item value="525" key="dialogHeight"/>
<list key="orderedProviders">
</list>
<list key="textArray">
</list>
<list key="orderedElements">
</list>
<list key="textEntries">
</list>
</section>
<section name="ImportExportAction">
</section>
</section>

View File

@@ -1,5 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<workingSetManager>
<workingSet aggregate="true" factoryID="org.eclipse.ui.internal.WorkingSetFactory" id="1379804109849_0" label="Window Working Set" name="Aggregate for window 1379804109848"/>
<workingSet aggregate="true" factoryID="org.eclipse.ui.internal.WorkingSetFactory" id="1379804153983_1" label="Window Working Set" name="Aggregate for window 1379804153983"/>
</workingSetManager>

View File

@@ -1 +0,0 @@
org.eclipse.core.runtime=1

View File

@@ -1,33 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<projectDescription>
<name>Limelight</name>
<comment></comment>
<projects>
</projects>
<buildSpec>
<buildCommand>
<name>com.android.ide.eclipse.adt.ResourceManagerBuilder</name>
<arguments>
</arguments>
</buildCommand>
<buildCommand>
<name>com.android.ide.eclipse.adt.PreCompilerBuilder</name>
<arguments>
</arguments>
</buildCommand>
<buildCommand>
<name>org.eclipse.jdt.core.javabuilder</name>
<arguments>
</arguments>
</buildCommand>
<buildCommand>
<name>com.android.ide.eclipse.adt.ApkBuilder</name>
<arguments>
</arguments>
</buildCommand>
</buildSpec>
<natures>
<nature>com.android.ide.eclipse.adt.AndroidNature</nature>
<nature>org.eclipse.jdt.core.javanature</nature>
</natures>
</projectDescription>

View File

@@ -1,11 +0,0 @@
eclipse.preferences.version=1
org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled
org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.6
org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve
org.eclipse.jdt.core.compiler.compliance=1.6
org.eclipse.jdt.core.compiler.debug.lineNumber=generate
org.eclipse.jdt.core.compiler.debug.localVariable=generate
org.eclipse.jdt.core.compiler.debug.sourceFile=generate
org.eclipse.jdt.core.compiler.problem.assertIdentifier=error
org.eclipse.jdt.core.compiler.problem.enumIdentifier=error
org.eclipse.jdt.core.compiler.source=1.6

View File

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

120
app/app.iml Normal file
View File

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

75
app/build.gradle Normal file
View File

@@ -0,0 +1,75 @@
import com.android.builder.model.ProductFlavor
import org.apache.tools.ant.taskdefs.condition.Os
apply plugin: 'com.android.application'
android {
compileSdkVersion 23
buildToolsVersion "23.0.1"
defaultConfig {
minSdkVersion 16
targetSdkVersion 23
versionName "3.1.10"
versionCode = 65
}
productFlavors {
root {
applicationId "com.limelight.root"
}
nonRoot {
applicationId "com.limelight"
}
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt'
}
}
sourceSets.main.jni.srcDirs = []
//noinspection GroovyAssignabilityCheck,GroovyAssignabilityCheck
task ndkBuild(type: Exec, description: 'Compile JNI source via NDK') {
Properties properties = new Properties()
properties.load(project.rootProject.file('local.properties').newDataInputStream())
def ndkDir = properties.getProperty('ndk.dir')
if (Os.isFamily(Os.FAMILY_WINDOWS)) {
commandLine "$ndkDir\\ndk-build.cmd",
'NDK_PROJECT_PATH=build/intermediates/ndk',
'NDK_LIBS_OUT=src/main/jniLibs',
'APP_BUILD_SCRIPT=src/main/jni/Android.mk',
'NDK_APPLICATION_MK=src/main/jni/Application.mk'
}
else {
commandLine "$ndkDir/ndk-build",
'NDK_PROJECT_PATH=build/intermediates/ndk',
'NDK_LIBS_OUT=src/main/jniLibs',
'APP_BUILD_SCRIPT=src/main/jni/Android.mk',
'NDK_APPLICATION_MK=src/main/jni/Application.mk'
}
}
tasks.withType(JavaCompile) {
compileTask -> compileTask.dependsOn ndkBuild
}
}
dependencies {
compile group: 'org.bouncycastle', name: 'bcprov-jdk15on', version: '1.52'
compile group: 'org.bouncycastle', name: 'bcpkix-jdk15on', version: '1.52'
compile group: 'com.squareup.okhttp', name: 'okhttp', version:'2.4.0'
compile group: 'com.squareup.okio', name:'okio', version:'1.5.0'
compile files('libs/jmdns-3.4.2.jar')
compile files('libs/limelight-common.jar')
compile files('libs/tinyrtsp.jar')
compile files('libs/jcodec-0.1.9-patched.jar')
}

Binary file not shown.

BIN
app/libs/jmdns-3.4.2.jar Normal file

Binary file not shown.

Binary file not shown.

View File

@@ -1,12 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.limelight"
android:versionCode="32"
android:versionName="2.5.4" >
<uses-sdk
android:minSdkVersion="16"
android:targetSdkVersion="19" />
package="com.limelight">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
@@ -16,67 +10,76 @@
<uses-feature android:name="android.hardware.touchscreen" android:required="false" />
<uses-feature android:name="android.hardware.wifi" android:required="false" />
<uses-feature android:name="android.hardware.gamepad" android:required="false" />
<application
android:allowBackup="true"
android:icon="@drawable/ic_launcher"
android:label="@string/app_name"
android:theme="@style/AppTheme" >
<!-- Samsung multi-window support -->
<uses-library android:name="com.sec.android.app.multiwindow" android:required="false" />
<meta-data android:name="com.sec.android.support.multiwindow" android:value="true" />
<!-- Launcher for traditional devices -->
<activity
android:name="com.limelight.PcView"
android:configChanges="mcc|mnc|locale|touchscreen|keyboard|keyboardHidden|navigation|screenLayout|fontScale|uiMode|orientation|screenSize|smallestScreenSize|layoutDirection"
android:label="@string/app_name" >
android:name=".PcView"
android:configChanges="mcc|mnc|locale|touchscreen|keyboard|keyboardHidden|navigation|screenLayout|fontScale|uiMode|orientation|screenSize|smallestScreenSize|layoutDirection">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
<category android:name="android.intent.category.MULTIWINDOW_LAUNCHER" />
<category android:name="tv.ouya.intent.category.APP" />
</intent-filter>
</activity>
<!-- Launcher for Android TV devices -->
<activity
android:name="com.limelight.AppView"
android:configChanges="mcc|mnc|locale|touchscreen|keyboard|keyboardHidden|navigation|screenLayout|fontScale|uiMode|orientation|screenSize|smallestScreenSize|layoutDirection"
android:label="App List" >
android:name=".PcViewTv"
android:logo="@drawable/atv_banner"
android:icon="@drawable/atv_banner"
android:configChanges="mcc|mnc|locale|touchscreen|keyboard|keyboardHidden|navigation|screenLayout|fontScale|uiMode|orientation|screenSize|smallestScreenSize|layoutDirection">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
</intent-filter>
</activity>
<activity
android:name=".AppView"
android:configChanges="mcc|mnc|locale|touchscreen|keyboard|keyboardHidden|navigation|screenLayout|fontScale|uiMode|orientation|screenSize|smallestScreenSize|layoutDirection" >
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value="com.limelight.PcView" />
</activity>
<activity
android:name="com.limelight.StreamSettings"
android:name=".preferences.StreamSettings"
android:label="Streaming Settings" >
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value="com.limelight.PcView" />
</activity>
<activity
android:name="com.limelight.AdvancedSettings"
android:label="Advanced Settings" >
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value="com.limelight.StreamSettings" />
</activity>
<activity
android:name="com.limelight.AddComputerManually"
android:name=".preferences.AddComputerManually"
android:label="Add Computer Manually" >
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value="com.limelight.PcView" />
</activity>
<activity
android:name="com.limelight.Game"
android:name=".Game"
android:screenOrientation="sensorLandscape"
android:configChanges="mcc|mnc|locale|touchscreen|keyboard|keyboardHidden|navigation|screenLayout|fontScale|uiMode|orientation|screenSize|smallestScreenSize|layoutDirection"
android:label="@string/title_activity_game"
android:parentActivityName="com.limelight.Connection"
android:theme="@style/FullscreenTheme" >
android:theme="@style/StreamTheme"
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.Connection" />
android:value="com.limelight.AppView" />
</activity>
<service
android:name="com.limelight.discovery.DiscoveryService"
android:name=".discovery.DiscoveryService"
android:label="mDNS PC Auto-Discovery Service" />
<service
android:name="com.limelight.computers.ComputerManagerService"
android:name=".computers.ComputerManagerService"
android:label="Computer Management Service" />
</application>

View File

@@ -0,0 +1,502 @@
package com.limelight;
import java.io.StringReader;
import java.util.List;
import java.util.Locale;
import java.util.UUID;
import com.limelight.computers.ComputerManagerListener;
import com.limelight.computers.ComputerManagerService;
import com.limelight.grid.AppGridAdapter;
import com.limelight.nvstream.http.ComputerDetails;
import com.limelight.nvstream.http.NvApp;
import com.limelight.nvstream.http.NvHTTP;
import com.limelight.preferences.PreferenceConfiguration;
import com.limelight.ui.AdapterFragment;
import com.limelight.ui.AdapterFragmentCallbacks;
import com.limelight.utils.CacheHelper;
import com.limelight.utils.Dialog;
import com.limelight.utils.ServerHelper;
import com.limelight.utils.SpinnerDialog;
import com.limelight.utils.UiHelper;
import android.app.Activity;
import android.app.Service;
import android.content.ComponentName;
import android.content.Intent;
import android.content.ServiceConnection;
import android.content.res.Configuration;
import android.os.Bundle;
import android.os.IBinder;
import android.view.ContextMenu;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.ContextMenu.ContextMenuInfo;
import android.widget.AbsListView;
import android.widget.AdapterView;
import android.widget.AdapterView.OnItemClickListener;
import android.widget.TextView;
import android.widget.Toast;
import android.widget.AdapterView.AdapterContextMenuInfo;
public class AppView extends Activity implements AdapterFragmentCallbacks {
private AppGridAdapter appGridAdapter;
private String uuidString;
private ComputerDetails computer;
private ComputerManagerService.ApplistPoller poller;
private SpinnerDialog blockingLoadSpinner;
private String lastRawApplist;
private int lastRunningAppId;
private boolean suspendGridUpdates;
private final static int START_OR_RESUME_ID = 1;
private final static int QUIT_ID = 2;
private final static int CANCEL_ID = 3;
private final static int START_WTIH_QUIT = 4;
public final static String NAME_EXTRA = "Name";
public final static String UUID_EXTRA = "UUID";
private ComputerManagerService.ComputerManagerBinder managerBinder;
private final ServiceConnection serviceConnection = new ServiceConnection() {
public void onServiceConnected(ComponentName className, IBinder binder) {
final ComputerManagerService.ComputerManagerBinder localBinder =
((ComputerManagerService.ComputerManagerBinder)binder);
// Wait in a separate thread to avoid stalling the UI
new Thread() {
@Override
public void run() {
// Wait for the binder to be ready
localBinder.waitForReady();
// Now make the binder visible
managerBinder = localBinder;
// Get the computer object
computer = managerBinder.getComputer(UUID.fromString(uuidString));
try {
appGridAdapter = new AppGridAdapter(AppView.this,
PreferenceConfiguration.readPreferences(AppView.this).listMode,
PreferenceConfiguration.readPreferences(AppView.this).smallIconMode,
computer, managerBinder.getUniqueId());
} catch (Exception e) {
e.printStackTrace();
finish();
return;
}
// Start updates
startComputerUpdates();
// Load the app grid with cached data (if possible)
populateAppGridWithCache();
getFragmentManager().beginTransaction()
.replace(R.id.appFragmentContainer, new AdapterFragment())
.commitAllowingStateLoss();
}
}.start();
}
public void onServiceDisconnected(ComponentName className) {
managerBinder = null;
}
};
private void startComputerUpdates() {
if (managerBinder == null) {
return;
}
managerBinder.startPolling(new ComputerManagerListener() {
@Override
public void notifyComputerUpdated(ComputerDetails details) {
// Do nothing if updates are suspended
if (suspendGridUpdates) {
return;
}
// Don't care about other computers
if (!details.uuid.toString().equalsIgnoreCase(uuidString)) {
return;
}
if (details.state == ComputerDetails.State.OFFLINE) {
// The PC is unreachable now
AppView.this.runOnUiThread(new Runnable() {
@Override
public void run() {
// Display a toast to the user and quit the activity
Toast.makeText(AppView.this, getResources().getText(R.string.lost_connection), Toast.LENGTH_SHORT).show();
finish();
}
});
return;
}
// App list is the same or empty
if (details.rawAppList == null || details.rawAppList.equals(lastRawApplist)) {
// Let's check if the running app ID changed
if (details.runningGameId != lastRunningAppId) {
// Update the currently running game using the app ID
lastRunningAppId = details.runningGameId;
updateUiWithServerinfo(details);
}
return;
}
lastRunningAppId = details.runningGameId;
lastRawApplist = details.rawAppList;
try {
updateUiWithAppList(NvHTTP.getAppListByReader(new StringReader(details.rawAppList)));
if (blockingLoadSpinner != null) {
blockingLoadSpinner.dismiss();
blockingLoadSpinner = null;
}
} catch (Exception ignored) {}
}
});
if (poller == null) {
poller = managerBinder.createAppListPoller(computer);
}
poller.start();
}
private void stopComputerUpdates() {
if (poller != null) {
poller.stop();
}
if (managerBinder != null) {
managerBinder.stopPolling();
}
if (appGridAdapter != null) {
appGridAdapter.cancelQueuedOperations();
}
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
String locale = PreferenceConfiguration.readPreferences(this).language;
if (!locale.equals(PreferenceConfiguration.DEFAULT_LANGUAGE)) {
Configuration config = new Configuration(getResources().getConfiguration());
config.locale = new Locale(locale);
getResources().updateConfiguration(config, getResources().getDisplayMetrics());
}
setContentView(R.layout.activity_app_view);
UiHelper.notifyNewRootView(this);
uuidString = getIntent().getStringExtra(UUID_EXTRA);
String labelText = getResources().getString(R.string.title_applist)+" "+getIntent().getStringExtra(NAME_EXTRA);
TextView label = (TextView) findViewById(R.id.appListText);
setTitle(labelText);
label.setText(labelText);
// Bind to the computer manager service
bindService(new Intent(this, ComputerManagerService.class), serviceConnection,
Service.BIND_AUTO_CREATE);
}
private void populateAppGridWithCache() {
try {
// Try to load from cache
lastRawApplist = CacheHelper.readInputStreamToString(CacheHelper.openCacheFileForInput(getCacheDir(), "applist", uuidString));
List<NvApp> applist = NvHTTP.getAppListByReader(new StringReader(lastRawApplist));
updateUiWithAppList(applist);
LimeLog.info("Loaded applist from cache");
} catch (Exception e) {
if (lastRawApplist != null) {
LimeLog.warning("Saved applist corrupted: "+lastRawApplist);
e.printStackTrace();
}
LimeLog.info("Loading applist from the network");
// We'll need to load from the network
loadAppsBlocking();
}
}
private void loadAppsBlocking() {
blockingLoadSpinner = SpinnerDialog.displayDialog(this, getResources().getString(R.string.applist_refresh_title),
getResources().getString(R.string.applist_refresh_msg), true);
}
@Override
protected void onDestroy() {
super.onDestroy();
SpinnerDialog.closeDialogs(this);
Dialog.closeDialogs();
if (managerBinder != null) {
unbindService(serviceConnection);
}
}
@Override
protected void onResume() {
super.onResume();
startComputerUpdates();
}
@Override
protected void onPause() {
super.onPause();
stopComputerUpdates();
}
private int getRunningAppId() {
int runningAppId = -1;
for (int i = 0; i < appGridAdapter.getCount(); i++) {
AppObject app = (AppObject) appGridAdapter.getItem(i);
if (app.app.getIsRunning()) {
runningAppId = app.app.getAppId();
break;
}
}
return runningAppId;
}
@Override
public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) {
super.onCreateContextMenu(menu, v, menuInfo);
AdapterContextMenuInfo info = (AdapterContextMenuInfo) menuInfo;
AppObject selectedApp = (AppObject) appGridAdapter.getItem(info.position);
int runningAppId = getRunningAppId();
if (runningAppId != -1) {
if (runningAppId == selectedApp.app.getAppId()) {
menu.add(Menu.NONE, START_OR_RESUME_ID, 1, getResources().getString(R.string.applist_menu_resume));
menu.add(Menu.NONE, QUIT_ID, 2, getResources().getString(R.string.applist_menu_quit));
}
else {
menu.add(Menu.NONE, START_WTIH_QUIT, 1, getResources().getString(R.string.applist_menu_quit_and_start));
menu.add(Menu.NONE, CANCEL_ID, 2, getResources().getString(R.string.applist_menu_cancel));
}
}
}
@Override
public void onContextMenuClosed(Menu menu) {
}
@Override
public boolean onContextItemSelected(MenuItem item) {
AdapterContextMenuInfo info = (AdapterContextMenuInfo) item.getMenuInfo();
final AppObject app = (AppObject) appGridAdapter.getItem(info.position);
switch (item.getItemId()) {
case START_WTIH_QUIT:
// Display a confirmation dialog first
UiHelper.displayQuitConfirmationDialog(this, new Runnable() {
@Override
public void run() {
ServerHelper.doStart(AppView.this, app.app, computer, managerBinder);
}
}, null);
return true;
case START_OR_RESUME_ID:
// Resume is the same as start for us
ServerHelper.doStart(AppView.this, app.app, computer, managerBinder);
return true;
case QUIT_ID:
// Display a confirmation dialog first
UiHelper.displayQuitConfirmationDialog(this, new Runnable() {
@Override
public void run() {
suspendGridUpdates = true;
ServerHelper.doQuit(AppView.this,
ServerHelper.getCurrentAddressFromComputer(computer),
app.app, managerBinder, new Runnable() {
@Override
public void run() {
// Trigger a poll immediately
suspendGridUpdates = false;
if (poller != null) {
poller.pollNow();
}
}
});
}
}, null);
return true;
case CANCEL_ID:
return true;
default:
return super.onContextItemSelected(item);
}
}
private void updateUiWithServerinfo(final ComputerDetails details) {
AppView.this.runOnUiThread(new Runnable() {
@Override
public void run() {
boolean updated = false;
// Look through our current app list to tag the running app
for (int i = 0; i < appGridAdapter.getCount(); i++) {
AppObject existingApp = (AppObject) appGridAdapter.getItem(i);
// There can only be one or zero apps running.
if (existingApp.app.getIsRunning() &&
existingApp.app.getAppId() == details.runningGameId) {
// This app was running and still is, so we're done now
return;
}
else if (existingApp.app.getAppId() == details.runningGameId) {
// This app wasn't running but now is
existingApp.app.setIsRunning(true);
updated = true;
}
else if (existingApp.app.getIsRunning()) {
// This app was running but now isn't
existingApp.app.setIsRunning(false);
updated = true;
}
else {
// This app wasn't running and still isn't
}
}
if (updated) {
appGridAdapter.notifyDataSetChanged();
}
}
});
}
private void updateUiWithAppList(final List<NvApp> appList) {
AppView.this.runOnUiThread(new Runnable() {
@Override
public void run() {
boolean updated = false;
// First handle app updates and additions
for (NvApp app : appList) {
boolean foundExistingApp = false;
// Try to update an existing app in the list first
for (int i = 0; i < appGridAdapter.getCount(); i++) {
AppObject existingApp = (AppObject) appGridAdapter.getItem(i);
if (existingApp.app.getAppId() == app.getAppId()) {
// Found the app; update its properties
if (existingApp.app.getIsRunning() != app.getIsRunning()) {
existingApp.app.setIsRunning(app.getIsRunning());
updated = true;
}
if (!existingApp.app.getAppName().equals(app.getAppName())) {
existingApp.app.setAppName(app.getAppName());
updated = true;
}
foundExistingApp = true;
break;
}
}
if (!foundExistingApp) {
// This app must be new
appGridAdapter.addApp(new AppObject(app));
updated = true;
}
}
// Next handle app removals
int i = 0;
while (i < appGridAdapter.getCount()) {
boolean foundExistingApp = false;
AppObject existingApp = (AppObject) appGridAdapter.getItem(i);
// Check if this app is in the latest list
for (NvApp app : appList) {
if (existingApp.app.getAppId() == app.getAppId()) {
foundExistingApp = true;
break;
}
}
// This app was removed in the latest app list
if (!foundExistingApp) {
appGridAdapter.removeApp(existingApp);
updated = true;
// Check this same index again because the item at i+1 is now at i after
// the removal
continue;
}
// Move on to the next item
i++;
}
if (updated) {
appGridAdapter.notifyDataSetChanged();
}
}
});
}
@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);
}
@Override
public void receiveAbsListView(AbsListView listView) {
listView.setAdapter(appGridAdapter);
listView.setOnItemClickListener(new OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> arg0, View arg1, int pos,
long id) {
AppObject app = (AppObject) appGridAdapter.getItem(pos);
// Only open the context menu if something is running, otherwise start it
if (getRunningAppId() != -1) {
openContextMenu(arg1);
} else {
ServerHelper.doStart(AppView.this, app.app, computer, managerBinder);
}
}
});
registerForContextMenu(listView);
listView.requestFocus();
}
public class AppObject {
public final NvApp app;
public AppObject(NvApp app) {
if (app == null) {
throw new IllegalArgumentException("app must not be null");
}
this.app = app;
}
@Override
public String toString() {
return app.getAppName();
}
}
}

View File

@@ -0,0 +1,897 @@
package com.limelight;
import com.limelight.LimelightBuildProps;
import com.limelight.binding.PlatformBinding;
import com.limelight.binding.input.ControllerHandler;
import com.limelight.binding.input.KeyboardTranslator;
import com.limelight.binding.input.TouchContext;
import com.limelight.binding.input.evdev.EvdevListener;
import com.limelight.binding.input.evdev.EvdevWatcher;
import com.limelight.binding.video.ConfigurableDecoderRenderer;
import com.limelight.nvstream.NvConnection;
import com.limelight.nvstream.NvConnectionListener;
import com.limelight.nvstream.StreamConfiguration;
import com.limelight.nvstream.av.video.VideoDecoderRenderer;
import com.limelight.nvstream.http.NvApp;
import com.limelight.nvstream.input.KeyboardPacket;
import com.limelight.nvstream.input.MouseButtonPacket;
import com.limelight.preferences.PreferenceConfiguration;
import com.limelight.ui.GameGestures;
import com.limelight.utils.Dialog;
import com.limelight.utils.SpinnerDialog;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.content.Context;
import android.content.res.Configuration;
import android.graphics.Point;
import android.hardware.input.InputManager;
import android.media.AudioManager;
import android.net.ConnectivityManager;
import android.net.wifi.WifiManager;
import android.os.Bundle;
import android.os.Handler;
import android.os.SystemClock;
import android.view.Display;
import android.view.InputDevice;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
import android.view.View;
import android.view.View.OnGenericMotionListener;
import android.view.View.OnSystemUiVisibilityChangeListener;
import android.view.View.OnTouchListener;
import android.view.ViewGroup;
import android.view.Window;
import android.view.WindowManager;
import android.view.inputmethod.InputMethodManager;
import android.widget.Toast;
import java.util.Locale;
public class Game extends Activity implements SurfaceHolder.Callback,
OnGenericMotionListener, OnTouchListener, NvConnectionListener, EvdevListener,
OnSystemUiVisibilityChangeListener, GameGestures
{
private int lastMouseX = Integer.MIN_VALUE;
private int lastMouseY = Integer.MIN_VALUE;
private int lastButtonState = 0;
// Only 2 touches are supported
private final TouchContext[] touchContextMap = new TouchContext[2];
private long threeFingerDownTime = 0;
private static final int THREE_FINGER_TAP_THRESHOLD = 300;
private ControllerHandler controllerHandler;
private KeyboardTranslator keybTranslator;
private PreferenceConfiguration prefConfig;
private final Point screenSize = new Point(0, 0);
private NvConnection conn;
private SpinnerDialog spinner;
private boolean displayedFailureDialog = false;
private boolean connecting = false;
private boolean connected = false;
private EvdevWatcher evdevWatcher;
private int modifierFlags = 0;
private boolean grabbedInput = true;
private boolean grabComboDown = false;
private ConfigurableDecoderRenderer decoderRenderer;
private WifiManager.WifiLock wifiLock;
private int drFlags = 0;
public static final String EXTRA_HOST = "Host";
public static final String EXTRA_APP_NAME = "AppName";
public static final String EXTRA_APP_ID = "AppId";
public static final String EXTRA_UNIQUEID = "UniqueId";
public static final String EXTRA_STREAMING_REMOTE = "Remote";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
String locale = PreferenceConfiguration.readPreferences(this).language;
if (!locale.equals(PreferenceConfiguration.DEFAULT_LANGUAGE)) {
Configuration config = new Configuration(getResources().getConfiguration());
config.locale = new Locale(locale);
getResources().updateConfiguration(config, getResources().getDisplayMetrics());
}
// We don't want a title bar
requestWindowFeature(Window.FEATURE_NO_TITLE);
// Full-screen and don't let the display go off
getWindow().addFlags(
WindowManager.LayoutParams.FLAG_FULLSCREEN |
WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
// If we're going to use immersive mode, we want to have
// the entire screen
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.KITKAT) {
getWindow().getDecorView().setSystemUiVisibility(
View.SYSTEM_UI_FLAG_LAYOUT_STABLE |
View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION |
View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN);
getWindow().addFlags(WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN);
}
// Listen for UI visibility events
getWindow().getDecorView().setOnSystemUiVisibilityChangeListener(this);
// Change volume button behavior
setVolumeControlStream(AudioManager.STREAM_MUSIC);
// Inflate the content
setContentView(R.layout.activity_game);
// Start the spinner
spinner = SpinnerDialog.displayDialog(this, getResources().getString(R.string.conn_establishing_title),
getResources().getString(R.string.conn_establishing_msg), true);
// Read the stream preferences
prefConfig = PreferenceConfiguration.readPreferences(this);
switch (prefConfig.decoder) {
case PreferenceConfiguration.FORCE_SOFTWARE_DECODER:
drFlags |= VideoDecoderRenderer.FLAG_FORCE_SOFTWARE_DECODING;
break;
case PreferenceConfiguration.AUTOSELECT_DECODER:
break;
case PreferenceConfiguration.FORCE_HARDWARE_DECODER:
drFlags |= VideoDecoderRenderer.FLAG_FORCE_HARDWARE_DECODING;
break;
}
if (prefConfig.stretchVideo) {
drFlags |= VideoDecoderRenderer.FLAG_FILL_SCREEN;
}
Display display = getWindowManager().getDefaultDisplay();
display.getSize(screenSize);
// Listen for events on the game surface
SurfaceView sv = (SurfaceView) findViewById(R.id.surfaceView);
sv.setOnGenericMotionListener(this);
sv.setOnTouchListener(this);
// Warn the user if they're on a metered connection
checkDataConnection();
// Make sure Wi-Fi is fully powered up
WifiManager wifiMgr = (WifiManager) getSystemService(Context.WIFI_SERVICE);
wifiLock = wifiMgr.createWifiLock(WifiManager.WIFI_MODE_FULL_HIGH_PERF, "Limelight");
wifiLock.setReferenceCounted(false);
wifiLock.acquire();
String host = Game.this.getIntent().getStringExtra(EXTRA_HOST);
String appName = Game.this.getIntent().getStringExtra(EXTRA_APP_NAME);
int appId = Game.this.getIntent().getIntExtra(EXTRA_APP_ID, StreamConfiguration.INVALID_APP_ID);
String uniqueId = Game.this.getIntent().getStringExtra(EXTRA_UNIQUEID);
boolean remote = Game.this.getIntent().getBooleanExtra(EXTRA_STREAMING_REMOTE, false);
if (appId == StreamConfiguration.INVALID_APP_ID) {
finish();
return;
}
decoderRenderer = new ConfigurableDecoderRenderer();
decoderRenderer.initializeWithFlags(drFlags);
StreamConfiguration config = new StreamConfiguration.Builder()
.setResolution(prefConfig.width, prefConfig.height)
.setRefreshRate(prefConfig.fps)
.setApp(new NvApp(appName, appId))
.setBitrate(prefConfig.bitrate * 1000)
.setEnableSops(prefConfig.enableSops)
.enableAdaptiveResolution((decoderRenderer.getCapabilities() &
VideoDecoderRenderer.CAPABILITY_ADAPTIVE_RESOLUTION) != 0)
.enableLocalAudioPlayback(prefConfig.playHostAudio)
.setMaxPacketSize(remote ? 1024 : 1292)
.setRemote(remote)
.build();
// Initialize the connection
conn = new NvConnection(host, uniqueId, Game.this, config, PlatformBinding.getCryptoProvider(this));
keybTranslator = new KeyboardTranslator(conn);
controllerHandler = new ControllerHandler(conn, this, prefConfig.multiController, prefConfig.deadzonePercentage);
InputManager inputManager = (InputManager) getSystemService(Context.INPUT_SERVICE);
inputManager.registerInputDeviceListener(controllerHandler, null);
SurfaceHolder sh = sv.getHolder();
if (prefConfig.stretchVideo || !decoderRenderer.isHardwareAccelerated()) {
// Set the surface to the size of the video
sh.setFixedSize(prefConfig.width, prefConfig.height);
}
// Initialize touch contexts
for (int i = 0; i < touchContextMap.length; i++) {
touchContextMap[i] = new TouchContext(conn, i,
((double)prefConfig.width / (double)screenSize.x),
((double)prefConfig.height / (double)screenSize.y));
}
if (LimelightBuildProps.ROOT_BUILD) {
// Start watching for raw input
evdevWatcher = new EvdevWatcher(this);
evdevWatcher.start();
}
// The connection will be started when the surface gets created
sh.addCallback(this);
}
private void resizeSurfaceWithAspectRatio(SurfaceView sv, double vidWidth, double vidHeight)
{
// Get the visible width of the activity
double visibleWidth = getWindow().getDecorView().getWidth();
ViewGroup.LayoutParams lp = sv.getLayoutParams();
// Calculate the new size of the SurfaceView
lp.width = (int) visibleWidth;
lp.height = (int) ((vidHeight / vidWidth) * visibleWidth);
// Apply the size change
sv.setLayoutParams(lp);
}
private void checkDataConnection()
{
ConnectivityManager mgr = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE);
if (mgr.isActiveNetworkMetered()) {
displayTransientMessage(getResources().getString(R.string.conn_metered));
}
}
@SuppressLint("InlinedApi")
private final Runnable hideSystemUi = new Runnable() {
@Override
public void run() {
// Use immersive mode on 4.4+ or standard low profile on previous builds
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.KITKAT) {
Game.this.getWindow().getDecorView().setSystemUiVisibility(
View.SYSTEM_UI_FLAG_LAYOUT_STABLE |
View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION |
View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN |
View.SYSTEM_UI_FLAG_HIDE_NAVIGATION |
View.SYSTEM_UI_FLAG_FULLSCREEN |
View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY);
}
else {
Game.this.getWindow().getDecorView().setSystemUiVisibility(
View.SYSTEM_UI_FLAG_FULLSCREEN |
View.SYSTEM_UI_FLAG_LOW_PROFILE);
}
}
};
private void hideSystemUi(int delay) {
Handler h = getWindow().getDecorView().getHandler();
if (h != null) {
h.removeCallbacks(hideSystemUi);
h.postDelayed(hideSystemUi, delay);
}
}
@Override
protected void onStop() {
super.onStop();
SpinnerDialog.closeDialogs(this);
Dialog.closeDialogs();
InputManager inputManager = (InputManager) getSystemService(Context.INPUT_SERVICE);
inputManager.unregisterInputDeviceListener(controllerHandler);
displayedFailureDialog = true;
stopConnection();
int averageEndToEndLat = decoderRenderer.getAverageEndToEndLatency();
int averageDecoderLat = decoderRenderer.getAverageDecoderLatency();
String message = null;
if (averageEndToEndLat > 0) {
message = getResources().getString(R.string.conn_client_latency)+" "+averageEndToEndLat+" ms";
if (averageDecoderLat > 0) {
message += " ("+getResources().getString(R.string.conn_client_latency_hw)+" "+averageDecoderLat+" ms)";
}
}
else if (averageDecoderLat > 0) {
message = getResources().getString(R.string.conn_hardware_latency)+" "+averageDecoderLat+" ms";
}
if (message != null) {
Toast.makeText(this, message, Toast.LENGTH_LONG).show();
}
finish();
}
@Override
protected void onDestroy() {
super.onDestroy();
wifiLock.release();
}
private final Runnable toggleGrab = new Runnable() {
@Override
public void run() {
if (evdevWatcher != null) {
if (grabbedInput) {
evdevWatcher.ungrabAll();
}
else {
evdevWatcher.regrabAll();
}
}
grabbedInput = !grabbedInput;
}
};
// Returns true if the key stroke was consumed
private boolean handleSpecialKeys(short translatedKey, boolean down) {
int modifierMask = 0;
// Mask off the high byte
translatedKey &= 0xff;
if (translatedKey == KeyboardTranslator.VK_CONTROL) {
modifierMask = KeyboardPacket.MODIFIER_CTRL;
}
else if (translatedKey == KeyboardTranslator.VK_SHIFT) {
modifierMask = KeyboardPacket.MODIFIER_SHIFT;
}
else if (translatedKey == KeyboardTranslator.VK_ALT) {
modifierMask = KeyboardPacket.MODIFIER_ALT;
}
if (down) {
this.modifierFlags |= modifierMask;
}
else {
this.modifierFlags &= ~modifierMask;
}
// Check if Ctrl+Shift+Z is pressed
if (translatedKey == KeyboardTranslator.VK_Z &&
(modifierFlags & (KeyboardPacket.MODIFIER_CTRL | KeyboardPacket.MODIFIER_SHIFT)) ==
(KeyboardPacket.MODIFIER_CTRL | KeyboardPacket.MODIFIER_SHIFT))
{
if (down) {
// Now that we've pressed the magic combo
// we'll wait for one of the keys to come up
grabComboDown = true;
}
else {
// Toggle the grab if Z comes up
Handler h = getWindow().getDecorView().getHandler();
if (h != null) {
h.postDelayed(toggleGrab, 250);
}
grabComboDown = false;
}
return true;
}
// Toggle the grab if control or shift comes up
else if (grabComboDown) {
Handler h = getWindow().getDecorView().getHandler();
if (h != null) {
h.postDelayed(toggleGrab, 250);
}
grabComboDown = false;
return true;
}
// Not a special combo
return false;
}
private static byte getModifierState(KeyEvent event) {
byte modifier = 0;
if (event.isShiftPressed()) {
modifier |= KeyboardPacket.MODIFIER_SHIFT;
}
if (event.isCtrlPressed()) {
modifier |= KeyboardPacket.MODIFIER_CTRL;
}
if (event.isAltPressed()) {
modifier |= KeyboardPacket.MODIFIER_ALT;
}
return modifier;
}
private byte getModifierState() {
return (byte) modifierFlags;
}
@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
// Pass-through virtual navigation keys
if ((event.getFlags() & KeyEvent.FLAG_VIRTUAL_HARD_KEY) != 0) {
return super.onKeyDown(keyCode, event);
}
// Try the controller handler first
boolean handled = controllerHandler.handleButtonDown(event);
if (!handled) {
// Try the keyboard handler
short translated = keybTranslator.translate(event.getKeyCode());
if (translated == 0) {
return super.onKeyDown(keyCode, event);
}
// Let this method take duplicate key down events
if (handleSpecialKeys(translated, true)) {
return true;
}
// Eat repeat down events
if (event.getRepeatCount() > 0) {
return true;
}
// Pass through keyboard input if we're not grabbing
if (!grabbedInput) {
return super.onKeyDown(keyCode, event);
}
keybTranslator.sendKeyDown(translated,
getModifierState(event));
}
return true;
}
@Override
public boolean onKeyUp(int keyCode, KeyEvent event) {
// Pass-through virtual navigation keys
if ((event.getFlags() & KeyEvent.FLAG_VIRTUAL_HARD_KEY) != 0) {
return super.onKeyUp(keyCode, event);
}
// Try the controller handler first
boolean handled = controllerHandler.handleButtonUp(event);
if (!handled) {
// Try the keyboard handler
short translated = keybTranslator.translate(event.getKeyCode());
if (translated == 0) {
return super.onKeyUp(keyCode, event);
}
if (handleSpecialKeys(translated, false)) {
return true;
}
// Pass through keyboard input if we're not grabbing
if (!grabbedInput) {
return super.onKeyUp(keyCode, event);
}
keybTranslator.sendKeyUp(translated,
getModifierState(event));
}
return true;
}
private TouchContext getTouchContext(int actionIndex)
{
if (actionIndex < touchContextMap.length) {
return touchContextMap[actionIndex];
}
else {
return null;
}
}
@Override
public void showKeyboard() {
LimeLog.info("Showing keyboard overlay");
InputMethodManager inputManager = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
inputManager.toggleSoftInput(InputMethodManager.SHOW_FORCED, InputMethodManager.HIDE_IMPLICIT_ONLY);
}
// Returns true if the event was consumed
private boolean handleMotionEvent(MotionEvent event) {
// Pass through keyboard input if we're not grabbing
if (!grabbedInput) {
return false;
}
if ((event.getSource() & InputDevice.SOURCE_CLASS_JOYSTICK) != 0) {
if (controllerHandler.handleMotionEvent(event)) {
return true;
}
}
else if ((event.getSource() & InputDevice.SOURCE_CLASS_POINTER) != 0)
{
// This case is for mice
if (event.getSource() == InputDevice.SOURCE_MOUSE)
{
int changedButtons = event.getButtonState() ^ lastButtonState;
if (event.getActionMasked() == MotionEvent.ACTION_SCROLL) {
// Send the vertical scroll packet
byte vScrollClicks = (byte) event.getAxisValue(MotionEvent.AXIS_VSCROLL);
conn.sendMouseScroll(vScrollClicks);
}
if ((changedButtons & MotionEvent.BUTTON_PRIMARY) != 0) {
if ((event.getButtonState() & MotionEvent.BUTTON_PRIMARY) != 0) {
conn.sendMouseButtonDown(MouseButtonPacket.BUTTON_LEFT);
}
else {
conn.sendMouseButtonUp(MouseButtonPacket.BUTTON_LEFT);
}
}
if ((changedButtons & MotionEvent.BUTTON_SECONDARY) != 0) {
if ((event.getButtonState() & MotionEvent.BUTTON_SECONDARY) != 0) {
conn.sendMouseButtonDown(MouseButtonPacket.BUTTON_RIGHT);
}
else {
conn.sendMouseButtonUp(MouseButtonPacket.BUTTON_RIGHT);
}
}
if ((changedButtons & MotionEvent.BUTTON_TERTIARY) != 0) {
if ((event.getButtonState() & MotionEvent.BUTTON_TERTIARY) != 0) {
conn.sendMouseButtonDown(MouseButtonPacket.BUTTON_MIDDLE);
}
else {
conn.sendMouseButtonUp(MouseButtonPacket.BUTTON_MIDDLE);
}
}
// First process the history
for (int i = 0; i < event.getHistorySize(); i++) {
updateMousePosition((int)event.getHistoricalX(i), (int)event.getHistoricalY(i));
}
// Now process the current values
updateMousePosition((int)event.getX(), (int)event.getY());
lastButtonState = event.getButtonState();
}
// This case is for touch-based input devices
else
{
int actionIndex = event.getActionIndex();
int eventX = (int)event.getX(actionIndex);
int eventY = (int)event.getY(actionIndex);
// Special handling for 3 finger gesture
if (event.getActionMasked() == MotionEvent.ACTION_POINTER_DOWN &&
event.getPointerCount() == 3) {
// Three fingers down
threeFingerDownTime = SystemClock.uptimeMillis();
// Cancel the first and second touches to avoid
// erroneous events
for (TouchContext aTouchContext : touchContextMap) {
aTouchContext.cancelTouch();
}
return true;
}
TouchContext context = getTouchContext(actionIndex);
if (context == null) {
return false;
}
switch (event.getActionMasked())
{
case MotionEvent.ACTION_POINTER_DOWN:
case MotionEvent.ACTION_DOWN:
context.touchDownEvent(eventX, eventY);
break;
case MotionEvent.ACTION_POINTER_UP:
case MotionEvent.ACTION_UP:
if (event.getPointerCount() == 1) {
// All fingers up
if (SystemClock.uptimeMillis() - threeFingerDownTime < THREE_FINGER_TAP_THRESHOLD) {
// This is a 3 finger tap to bring up the keyboard
showKeyboard();
return true;
}
}
context.touchUpEvent(eventX, eventY);
if (actionIndex == 0 && event.getPointerCount() > 1 && !context.isCancelled()) {
// The original secondary touch now becomes primary
context.touchDownEvent((int)event.getX(1), (int)event.getY(1));
}
break;
case MotionEvent.ACTION_MOVE:
// ACTION_MOVE is special because it always has actionIndex == 0
// We'll call the move handlers for all indexes manually
// First process the historical events
for (int i = 0; i < event.getHistorySize(); i++) {
for (TouchContext aTouchContextMap : touchContextMap) {
if (aTouchContextMap.getActionIndex() < event.getPointerCount())
{
aTouchContextMap.touchMoveEvent(
(int)event.getHistoricalX(aTouchContextMap.getActionIndex(), i),
(int)event.getHistoricalY(aTouchContextMap.getActionIndex(), i));
}
}
}
// Now process the current values
for (TouchContext aTouchContextMap : touchContextMap) {
if (aTouchContextMap.getActionIndex() < event.getPointerCount())
{
aTouchContextMap.touchMoveEvent(
(int)event.getX(aTouchContextMap.getActionIndex()),
(int)event.getY(aTouchContextMap.getActionIndex()));
}
}
break;
default:
return false;
}
}
// Handled a known source
return true;
}
// Unknown class
return false;
}
@Override
public boolean onTouchEvent(MotionEvent event) {
return handleMotionEvent(event) || super.onTouchEvent(event);
}
@Override
public boolean onGenericMotionEvent(MotionEvent event) {
return handleMotionEvent(event) || super.onGenericMotionEvent(event);
}
private void updateMousePosition(int eventX, int eventY) {
// Send a mouse move if we already have a mouse location
// and the mouse coordinates change
if (lastMouseX != Integer.MIN_VALUE &&
lastMouseY != Integer.MIN_VALUE &&
!(lastMouseX == eventX && lastMouseY == eventY))
{
int deltaX = eventX - lastMouseX;
int deltaY = eventY - lastMouseY;
// Scale the deltas if the device resolution is different
// than the stream resolution
deltaX = (int)Math.round((double)deltaX * ((double)prefConfig.width / (double)screenSize.x));
deltaY = (int)Math.round((double)deltaY * ((double)prefConfig.height / (double)screenSize.y));
conn.sendMouseMove((short)deltaX, (short)deltaY);
}
// Update pointer location for delta calculation next time
lastMouseX = eventX;
lastMouseY = eventY;
}
@Override
public boolean onGenericMotion(View v, MotionEvent event) {
return handleMotionEvent(event);
}
@SuppressLint("ClickableViewAccessibility")
@Override
public boolean onTouch(View v, MotionEvent event) {
return handleMotionEvent(event);
}
@Override
public void stageStarting(Stage stage) {
if (spinner != null) {
spinner.setMessage(getResources().getString(R.string.conn_starting)+" "+stage.getName());
}
}
@Override
public void stageComplete(Stage stage) {
}
private void stopConnection() {
if (connecting || connected) {
connecting = connected = false;
conn.stop();
}
// Close the Evdev watcher to allow use of captured input devices
if (evdevWatcher != null) {
evdevWatcher.shutdown();
evdevWatcher = null;
}
}
@Override
public void stageFailed(Stage stage) {
if (spinner != null) {
spinner.dismiss();
spinner = null;
}
if (!displayedFailureDialog) {
displayedFailureDialog = true;
stopConnection();
Dialog.displayDialog(this, getResources().getString(R.string.conn_error_title),
getResources().getString(R.string.conn_error_msg)+" "+stage.getName(), true);
}
}
@Override
public void connectionTerminated(Exception e) {
if (!displayedFailureDialog) {
displayedFailureDialog = true;
e.printStackTrace();
stopConnection();
Dialog.displayDialog(this, getResources().getString(R.string.conn_terminated_title),
getResources().getString(R.string.conn_terminated_msg), true);
}
}
@Override
public void connectionStarted() {
if (spinner != null) {
spinner.dismiss();
spinner = null;
}
connecting = false;
connected = true;
hideSystemUi(1000);
}
@Override
public void displayMessage(final String message) {
runOnUiThread(new Runnable() {
@Override
public void run() {
Toast.makeText(Game.this, message, Toast.LENGTH_LONG).show();
}
});
}
@Override
public void displayTransientMessage(final String message) {
if (!prefConfig.disableWarnings) {
runOnUiThread(new Runnable() {
@Override
public void run() {
Toast.makeText(Game.this, message, Toast.LENGTH_LONG).show();
}
});
}
}
@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
}
@Override
public void surfaceCreated(SurfaceHolder holder) {
if (!connected && !connecting) {
connecting = true;
// Resize the surface to match the aspect ratio of the video
// This must be done after the surface is created.
if (!prefConfig.stretchVideo && decoderRenderer.isHardwareAccelerated()) {
resizeSurfaceWithAspectRatio((SurfaceView) findViewById(R.id.surfaceView),
prefConfig.width, prefConfig.height);
}
conn.start(PlatformBinding.getDeviceName(), holder, drFlags,
PlatformBinding.getAudioRenderer(), decoderRenderer);
}
}
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
if (connected) {
stopConnection();
}
}
@Override
public void mouseMove(int deltaX, int deltaY) {
conn.sendMouseMove((short) deltaX, (short) deltaY);
}
@Override
public void mouseButtonEvent(int buttonId, boolean down) {
byte buttonIndex;
switch (buttonId)
{
case EvdevListener.BUTTON_LEFT:
buttonIndex = MouseButtonPacket.BUTTON_LEFT;
break;
case EvdevListener.BUTTON_MIDDLE:
buttonIndex = MouseButtonPacket.BUTTON_MIDDLE;
break;
case EvdevListener.BUTTON_RIGHT:
buttonIndex = MouseButtonPacket.BUTTON_RIGHT;
break;
default:
LimeLog.warning("Unhandled button: "+buttonId);
return;
}
if (down) {
conn.sendMouseButtonDown(buttonIndex);
}
else {
conn.sendMouseButtonUp(buttonIndex);
}
}
@Override
public void mouseScroll(byte amount) {
conn.sendMouseScroll(amount);
}
@Override
public void keyboardEvent(boolean buttonDown, short keyCode) {
short keyMap = keybTranslator.translate(keyCode);
if (keyMap != 0) {
if (handleSpecialKeys(keyMap, buttonDown)) {
return;
}
if (buttonDown) {
keybTranslator.sendKeyDown(keyMap, getModifierState());
}
else {
keybTranslator.sendKeyUp(keyMap, getModifierState());
}
}
}
@Override
public void onSystemUiVisibilityChange(int visibility) {
// Don't do anything if we're not connected
if (!connected) {
return;
}
// This flag is set for all devices
if ((visibility & View.SYSTEM_UI_FLAG_FULLSCREEN) == 0) {
hideSystemUi(2000);
}
// This flag is only set on 4.4+
else if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.KITKAT &&
(visibility & View.SYSTEM_UI_FLAG_HIDE_NAVIGATION) == 0) {
hideSystemUi(2000);
}
// This flag is only set before 4.4+
else if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.KITKAT &&
(visibility & View.SYSTEM_UI_FLAG_LOW_PROFILE) == 0) {
hideSystemUi(2000);
}
}
}

View File

@@ -0,0 +1,644 @@
package com.limelight;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.Locale;
import com.limelight.binding.PlatformBinding;
import com.limelight.binding.crypto.AndroidCryptoProvider;
import com.limelight.computers.ComputerManagerListener;
import com.limelight.computers.ComputerManagerService;
import com.limelight.grid.PcGridAdapter;
import com.limelight.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.http.PairingManager.PairState;
import com.limelight.nvstream.wol.WakeOnLanSender;
import com.limelight.preferences.AddComputerManually;
import com.limelight.preferences.PreferenceConfiguration;
import com.limelight.preferences.StreamSettings;
import com.limelight.ui.AdapterFragment;
import com.limelight.ui.AdapterFragmentCallbacks;
import com.limelight.utils.Dialog;
import com.limelight.utils.ServerHelper;
import com.limelight.utils.UiHelper;
import android.app.Activity;
import android.app.Service;
import android.content.ComponentName;
import android.content.Intent;
import android.content.ServiceConnection;
import android.content.res.Configuration;
import android.os.Bundle;
import android.os.IBinder;
import android.preference.PreferenceManager;
import android.view.ContextMenu;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.ContextMenu.ContextMenuInfo;
import android.view.View.OnClickListener;
import android.widget.AbsListView;
import android.widget.AdapterView;
import android.widget.AdapterView.OnItemClickListener;
import android.widget.ImageButton;
import android.widget.RelativeLayout;
import android.widget.Toast;
import android.widget.AdapterView.AdapterContextMenuInfo;
public class PcView extends Activity implements AdapterFragmentCallbacks {
private RelativeLayout noPcFoundLayout;
private PcGridAdapter pcGridAdapter;
private ComputerManagerService.ComputerManagerBinder managerBinder;
private boolean freezeUpdates, runningPolling;
private final ServiceConnection serviceConnection = new ServiceConnection() {
public void onServiceConnected(ComponentName className, IBinder binder) {
final ComputerManagerService.ComputerManagerBinder localBinder =
((ComputerManagerService.ComputerManagerBinder)binder);
// Wait in a separate thread to avoid stalling the UI
new Thread() {
@Override
public void run() {
// Wait for the binder to be ready
localBinder.waitForReady();
// Now make the binder visible
managerBinder = localBinder;
// Start updates
startComputerUpdates();
// Force a keypair to be generated early to avoid discovery delays
new AndroidCryptoProvider(PcView.this).getClientCertificate();
}
}.start();
}
public void onServiceDisconnected(ComponentName className) {
managerBinder = null;
}
};
public void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
// Reinitialize views just in case orientation changed
initializeViews();
}
private final static int APP_LIST_ID = 1;
private final static int PAIR_ID = 2;
private final static int UNPAIR_ID = 3;
private final static int WOL_ID = 4;
private final static int DELETE_ID = 5;
private final static int RESUME_ID = 6;
private final static int QUIT_ID = 7;
private void initializeViews() {
setContentView(R.layout.activity_pc_view);
UiHelper.notifyNewRootView(this);
// Set default preferences if we've never been run
PreferenceManager.setDefaultValues(this, R.xml.preferences, false);
// Setup the list view
ImageButton settingsButton = (ImageButton) findViewById(R.id.settingsButton);
ImageButton addComputerButton = (ImageButton) findViewById(R.id.manuallyAddPc);
settingsButton.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
startActivity(new Intent(PcView.this, StreamSettings.class));
}
});
addComputerButton.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
Intent i = new Intent(PcView.this, AddComputerManually.class);
startActivity(i);
}
});
getFragmentManager().beginTransaction()
.replace(R.id.pcFragmentContainer, new AdapterFragment())
.commitAllowingStateLoss();
noPcFoundLayout = (RelativeLayout) findViewById(R.id.no_pc_found_layout);
if (pcGridAdapter.getCount() == 0) {
noPcFoundLayout.setVisibility(View.VISIBLE);
}
else {
noPcFoundLayout.setVisibility(View.INVISIBLE);
}
pcGridAdapter.notifyDataSetChanged();
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
String locale = PreferenceConfiguration.readPreferences(this).language;
if (!locale.equals(PreferenceConfiguration.DEFAULT_LANGUAGE)) {
Configuration config = new Configuration(getResources().getConfiguration());
config.locale = new Locale(locale);
getResources().updateConfiguration(config, getResources().getDisplayMetrics());
}
// Bind to the computer manager service
bindService(new Intent(PcView.this, ComputerManagerService.class), serviceConnection,
Service.BIND_AUTO_CREATE);
pcGridAdapter = new PcGridAdapter(this,
PreferenceConfiguration.readPreferences(this).listMode,
PreferenceConfiguration.readPreferences(this).smallIconMode);
initializeViews();
}
private void startComputerUpdates() {
if (managerBinder != null) {
if (runningPolling) {
return;
}
freezeUpdates = false;
managerBinder.startPolling(new ComputerManagerListener() {
@Override
public void notifyComputerUpdated(final ComputerDetails details) {
if (!freezeUpdates) {
PcView.this.runOnUiThread(new Runnable() {
@Override
public void run() {
updateComputer(details);
}
});
}
}
});
runningPolling = true;
}
}
private void stopComputerUpdates(boolean wait) {
if (managerBinder != null) {
if (!runningPolling) {
return;
}
freezeUpdates = true;
managerBinder.stopPolling();
if (wait) {
managerBinder.waitForPollingStopped();
}
runningPolling = false;
}
}
@Override
public void onDestroy() {
super.onDestroy();
if (managerBinder != null) {
unbindService(serviceConnection);
}
}
@Override
protected void onResume() {
super.onResume();
startComputerUpdates();
}
@Override
protected void onPause() {
super.onPause();
stopComputerUpdates(false);
}
@Override
protected void onStop() {
super.onStop();
Dialog.closeDialogs();
}
@Override
public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) {
stopComputerUpdates(false);
// Call superclass
super.onCreateContextMenu(menu, v, menuInfo);
AdapterContextMenuInfo info = (AdapterContextMenuInfo) menuInfo;
ComputerObject computer = (ComputerObject) pcGridAdapter.getItem(info.position);
// Inflate the context menu
if (computer.details.reachability == ComputerDetails.Reachability.OFFLINE ||
computer.details.reachability == ComputerDetails.Reachability.UNKNOWN) {
menu.add(Menu.NONE, WOL_ID, 1, getResources().getString(R.string.pcview_menu_send_wol));
menu.add(Menu.NONE, DELETE_ID, 2, getResources().getString(R.string.pcview_menu_delete_pc));
}
else if (computer.details.pairState != PairState.PAIRED) {
menu.add(Menu.NONE, PAIR_ID, 1, getResources().getString(R.string.pcview_menu_pair_pc));
menu.add(Menu.NONE, DELETE_ID, 2, getResources().getString(R.string.pcview_menu_delete_pc));
}
else {
if (computer.details.runningGameId != 0) {
menu.add(Menu.NONE, RESUME_ID, 1, getResources().getString(R.string.applist_menu_resume));
menu.add(Menu.NONE, QUIT_ID, 2, getResources().getString(R.string.applist_menu_quit));
}
menu.add(Menu.NONE, APP_LIST_ID, 3, getResources().getString(R.string.pcview_menu_app_list));
// FIXME: We used to be able to unpair here but it's been broken since GFE 2.1.x, so I've replaced
// it with delete which actually work
menu.add(Menu.NONE, DELETE_ID, 4, getResources().getString(R.string.pcview_menu_delete_pc));
}
}
@Override
public void onContextMenuClosed(Menu menu) {
startComputerUpdates();
}
private void doPair(final ComputerDetails computer) {
if (computer.reachability == ComputerDetails.Reachability.OFFLINE) {
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;
}
Toast.makeText(PcView.this, getResources().getString(R.string.pairing), Toast.LENGTH_SHORT).show();
new Thread(new Runnable() {
@Override
public void run() {
NvHTTP httpConn;
String message;
boolean success = false;
try {
// Stop updates and wait while pairing
stopComputerUpdates(true);
InetAddress addr = null;
if (computer.reachability == ComputerDetails.Reachability.LOCAL) {
addr = computer.localIp;
}
else if (computer.reachability == ComputerDetails.Reachability.REMOTE) {
addr = computer.remoteIp;
}
else {
LimeLog.warning("Unknown reachability - using local IP");
addr = computer.localIp;
}
httpConn = new NvHTTP(addr,
managerBinder.getUniqueId(),
PlatformBinding.getDeviceName(),
PlatformBinding.getCryptoProvider(PcView.this));
if (httpConn.getPairState() == PairingManager.PairState.PAIRED) {
// Don't display any toast, but open the app list
message = null;
success = true;
}
else {
final String pinStr = PairingManager.generatePinString();
// 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);
PairingManager.PairState pairState = httpConn.pair(pinStr);
if (pairState == PairingManager.PairState.PIN_WRONG) {
message = getResources().getString(R.string.pair_incorrect_pin);
}
else if (pairState == PairingManager.PairState.FAILED) {
message = getResources().getString(R.string.pair_fail);
}
else if (pairState == PairingManager.PairState.PAIRED) {
// Just navigate to the app view without displaying a toast
message = null;
success = true;
}
else {
// Should be no other values
message = null;
}
}
} catch (UnknownHostException e) {
message = getResources().getString(R.string.error_unknown_host);
} catch (FileNotFoundException e) {
message = getResources().getString(R.string.error_404);
} catch (Exception e) {
e.printStackTrace();
message = e.getMessage();
}
Dialog.closeDialogs();
final String toastMessage = message;
final boolean toastSuccess = success;
runOnUiThread(new Runnable() {
@Override
public void run() {
if (toastMessage != null) {
Toast.makeText(PcView.this, toastMessage, Toast.LENGTH_LONG).show();
}
if (toastSuccess) {
// Open the app list after a successful pairing attemp
doAppList(computer);
}
}
});
// Start polling again
startComputerUpdates();
}
}).start();
}
private void doWakeOnLan(final ComputerDetails computer) {
if (computer.state == ComputerDetails.State.ONLINE) {
Toast.makeText(PcView.this, getResources().getString(R.string.wol_pc_online), Toast.LENGTH_SHORT).show();
return;
}
if (computer.macAddress == null) {
Toast.makeText(PcView.this, getResources().getString(R.string.wol_no_mac), Toast.LENGTH_SHORT).show();
return;
}
Toast.makeText(PcView.this, getResources().getString(R.string.wol_waking_pc), Toast.LENGTH_SHORT).show();
new Thread(new Runnable() {
@Override
public void run() {
String message;
try {
WakeOnLanSender.sendWolPacket(computer);
message = getResources().getString(R.string.wol_waking_msg);
} catch (IOException e) {
message = getResources().getString(R.string.wol_fail);
}
final String toastMessage = message;
runOnUiThread(new Runnable() {
@Override
public void run() {
Toast.makeText(PcView.this, toastMessage, Toast.LENGTH_LONG).show();
}
});
}
}).start();
}
private void doUnpair(final ComputerDetails computer) {
if (computer.reachability == ComputerDetails.Reachability.OFFLINE) {
Toast.makeText(PcView.this, getResources().getString(R.string.error_pc_offline), Toast.LENGTH_SHORT).show();
return;
}
if (managerBinder == null) {
Toast.makeText(PcView.this, getResources().getString(R.string.error_manager_not_running), Toast.LENGTH_LONG).show();
return;
}
Toast.makeText(PcView.this, getResources().getString(R.string.unpairing), Toast.LENGTH_SHORT).show();
new Thread(new Runnable() {
@Override
public void run() {
NvHTTP httpConn;
String message;
try {
InetAddress addr = null;
if (computer.reachability == ComputerDetails.Reachability.LOCAL) {
addr = computer.localIp;
}
else if (computer.reachability == ComputerDetails.Reachability.REMOTE) {
addr = computer.remoteIp;
}
else {
LimeLog.warning("Unknown reachability - using local IP");
addr = computer.localIp;
}
httpConn = new NvHTTP(addr,
managerBinder.getUniqueId(),
PlatformBinding.getDeviceName(),
PlatformBinding.getCryptoProvider(PcView.this));
if (httpConn.getPairState() == PairingManager.PairState.PAIRED) {
httpConn.unpair();
if (httpConn.getPairState() == PairingManager.PairState.NOT_PAIRED) {
message = getResources().getString(R.string.unpair_success);
}
else {
message = getResources().getString(R.string.unpair_fail);
}
}
else {
message = getResources().getString(R.string.unpair_error);
}
} catch (UnknownHostException e) {
message = getResources().getString(R.string.error_unknown_host);
} catch (FileNotFoundException e) {
message = getResources().getString(R.string.error_404);
} catch (Exception e) {
message = e.getMessage();
}
final String toastMessage = message;
runOnUiThread(new Runnable() {
@Override
public void run() {
Toast.makeText(PcView.this, toastMessage, Toast.LENGTH_LONG).show();
}
});
}
}).start();
}
private void doAppList(ComputerDetails computer) {
if (computer.reachability == ComputerDetails.Reachability.OFFLINE) {
Toast.makeText(PcView.this, getResources().getString(R.string.error_pc_offline), Toast.LENGTH_SHORT).show();
return;
}
if (managerBinder == null) {
Toast.makeText(PcView.this, getResources().getString(R.string.error_manager_not_running), Toast.LENGTH_LONG).show();
return;
}
Intent i = new Intent(this, AppView.class);
i.putExtra(AppView.NAME_EXTRA, computer.name);
i.putExtra(AppView.UUID_EXTRA, computer.uuid.toString());
startActivity(i);
}
@Override
public boolean onContextItemSelected(MenuItem item) {
AdapterContextMenuInfo info = (AdapterContextMenuInfo) item.getMenuInfo();
final ComputerObject computer = (ComputerObject) pcGridAdapter.getItem(info.position);
switch (item.getItemId()) {
case PAIR_ID:
doPair(computer.details);
return true;
case UNPAIR_ID:
doUnpair(computer.details);
return true;
case WOL_ID:
doWakeOnLan(computer.details);
return true;
case DELETE_ID:
if (managerBinder == null) {
Toast.makeText(PcView.this, getResources().getString(R.string.error_manager_not_running), Toast.LENGTH_LONG).show();
return true;
}
managerBinder.removeComputer(computer.details.name);
removeComputer(computer.details);
return true;
case APP_LIST_ID:
doAppList(computer.details);
return true;
case RESUME_ID:
if (managerBinder == null) {
Toast.makeText(PcView.this, getResources().getString(R.string.error_manager_not_running), Toast.LENGTH_LONG).show();
return true;
}
ServerHelper.doStart(this, new NvApp("app", computer.details.runningGameId), computer.details, managerBinder);
return true;
case QUIT_ID:
if (managerBinder == null) {
Toast.makeText(PcView.this, getResources().getString(R.string.error_manager_not_running), Toast.LENGTH_LONG).show();
return true;
}
// Display a confirmation dialog first
UiHelper.displayQuitConfirmationDialog(this, new Runnable() {
@Override
public void run() {
ServerHelper.doQuit(PcView.this,
ServerHelper.getCurrentAddressFromComputer(computer.details),
new NvApp("app", 0), managerBinder, null);
}
}, null);
return true;
default:
return super.onContextItemSelected(item);
}
}
private void removeComputer(ComputerDetails details) {
for (int i = 0; i < pcGridAdapter.getCount(); i++) {
ComputerObject computer = (ComputerObject) pcGridAdapter.getItem(i);
if (details.equals(computer.details)) {
pcGridAdapter.removeComputer(computer);
pcGridAdapter.notifyDataSetChanged();
if (pcGridAdapter.getCount() == 0) {
// Show the "Discovery in progress" view
noPcFoundLayout.setVisibility(View.VISIBLE);
}
break;
}
}
}
private void updateComputer(ComputerDetails details) {
ComputerObject existingEntry = null;
for (int i = 0; i < pcGridAdapter.getCount(); i++) {
ComputerObject computer = (ComputerObject) pcGridAdapter.getItem(i);
// Check if this is the same computer
if (details.uuid.equals(computer.details.uuid)) {
existingEntry = computer;
break;
}
}
if (existingEntry != null) {
// Replace the information in the existing entry
existingEntry.details = details;
}
else {
// Add a new entry
pcGridAdapter.addComputer(new ComputerObject(details));
// Remove the "Discovery in progress" view
noPcFoundLayout.setVisibility(View.INVISIBLE);
}
// Notify the view that the data has changed
pcGridAdapter.notifyDataSetChanged();
}
@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);
}
@Override
public void receiveAbsListView(AbsListView listView) {
listView.setAdapter(pcGridAdapter);
listView.setOnItemClickListener(new OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> arg0, View arg1, int pos,
long id) {
ComputerObject computer = (ComputerObject) pcGridAdapter.getItem(pos);
if (computer.details.reachability == ComputerDetails.Reachability.UNKNOWN ||
computer.details.reachability == ComputerDetails.Reachability.OFFLINE) {
// Open the context menu if a PC is offline or refreshing
openContextMenu(arg1);
} else if (computer.details.pairState != PairState.PAIRED) {
// Pair an unpaired machine by default
doPair(computer.details);
} else {
doAppList(computer.details);
}
}
});
registerForContextMenu(listView);
}
public class ComputerObject {
public ComputerDetails details;
public ComputerObject(ComputerDetails details) {
if (details == null) {
throw new IllegalArgumentException("details must not be null");
}
this.details = details;
}
@Override
public String toString() {
return details.name;
}
}
}

View File

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

View File

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

View File

@@ -0,0 +1,97 @@
package com.limelight.binding.audio;
import android.media.AudioFormat;
import android.media.AudioManager;
import android.media.AudioTrack;
import com.limelight.LimeLog;
import com.limelight.nvstream.av.audio.AudioRenderer;
public class AndroidAudioRenderer implements AudioRenderer {
private static final int FRAME_SIZE = 960;
private AudioTrack track;
@Override
public boolean streamInitialized(int channelCount, int sampleRate) {
int channelConfig;
int bufferSize;
switch (channelCount)
{
case 1:
channelConfig = AudioFormat.CHANNEL_OUT_MONO;
break;
case 2:
channelConfig = AudioFormat.CHANNEL_OUT_STEREO;
break;
default:
LimeLog.severe("Decoder returned unhandled channel count");
return false;
}
// We're not supposed to request less than the minimum
// buffer size for our buffer, but it appears that we can
// do this on many devices and it lowers audio latency.
// We'll try the small buffer size first and if it fails,
// use the recommended larger buffer size.
try {
// Buffer two frames of audio if possible
bufferSize = FRAME_SIZE * 2;
track = new AudioTrack(AudioManager.STREAM_MUSIC,
sampleRate,
channelConfig,
AudioFormat.ENCODING_PCM_16BIT,
bufferSize,
AudioTrack.MODE_STREAM);
track.play();
} catch (Exception e) {
// Try to release the AudioTrack if we got far enough
try {
if (track != null) {
track.release();
}
} catch (Exception ignored) {}
// Now try the larger buffer size
bufferSize = Math.max(AudioTrack.getMinBufferSize(sampleRate,
channelConfig,
AudioFormat.ENCODING_PCM_16BIT),
FRAME_SIZE * 2);
// Round to next frame
bufferSize = (((bufferSize + (FRAME_SIZE - 1)) / FRAME_SIZE) * FRAME_SIZE);
track = new AudioTrack(AudioManager.STREAM_MUSIC,
sampleRate,
channelConfig,
AudioFormat.ENCODING_PCM_16BIT,
bufferSize,
AudioTrack.MODE_STREAM);
track.play();
}
LimeLog.info("Audio track buffer size: "+bufferSize);
return true;
}
@Override
public void playDecodedAudio(byte[] audioData, int offset, int length) {
track.write(audioData, offset, length);
}
@Override
public void streamClosing() {
if (track != null) {
track.release();
}
}
@Override
public int getCapabilities() {
return 0;
}
}

View File

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

View File

@@ -0,0 +1,870 @@
package com.limelight.binding.input;
import android.hardware.input.InputManager;
import android.os.SystemClock;
import android.util.SparseArray;
import android.view.InputDevice;
import android.view.KeyEvent;
import android.view.MotionEvent;
import com.limelight.LimeLog;
import com.limelight.nvstream.NvConnection;
import com.limelight.nvstream.input.ControllerPacket;
import com.limelight.ui.GameGestures;
import com.limelight.utils.Vector2d;
public class ControllerHandler implements InputManager.InputDeviceListener {
private static final int MAXIMUM_BUMPER_UP_DELAY_MS = 100;
private static final int START_DOWN_TIME_KEYB_MS = 750;
private static final int MINIMUM_BUTTON_DOWN_TIME_MS = 25;
private static final int EMULATING_SPECIAL = 0x1;
private static final int EMULATING_SELECT = 0x2;
private static final int EMULATED_SPECIAL_UP_DELAY_MS = 100;
private static final int EMULATED_SELECT_UP_DELAY_MS = 30;
private final Vector2d inputVector = new Vector2d();
private final SparseArray<ControllerContext> contexts = new SparseArray<ControllerContext>();
private final NvConnection conn;
private final double stickDeadzone;
private final ControllerContext defaultContext = new ControllerContext();
private final GameGestures gestures;
private boolean hasGameController;
private final boolean multiControllerEnabled;
private short currentControllers;
public ControllerHandler(NvConnection conn, GameGestures gestures, boolean multiControllerEnabled, int deadzonePercentage) {
this.conn = conn;
this.gestures = gestures;
this.multiControllerEnabled = multiControllerEnabled;
// HACK: For now we're hardcoding a 10% deadzone. Some deadzone
// is required for controller batching support to work.
deadzonePercentage = 10;
int[] ids = InputDevice.getDeviceIds();
for (int id : ids) {
InputDevice dev = InputDevice.getDevice(id);
if ((dev.getSources() & InputDevice.SOURCE_JOYSTICK) != 0 ||
(dev.getSources() & InputDevice.SOURCE_GAMEPAD) != 0) {
// This looks like a gamepad, but we'll check X and Y to be sure
if (getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_X) != null &&
getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_Y) != null) {
// This is a gamepad
hasGameController = true;
}
}
}
// 1% is the lowest possible deadzone we support
if (deadzonePercentage <= 0) {
deadzonePercentage = 1;
}
this.stickDeadzone = (double)deadzonePercentage / 100.0;
// Initialize the default context for events with no device
defaultContext.leftStickXAxis = MotionEvent.AXIS_X;
defaultContext.leftStickYAxis = MotionEvent.AXIS_Y;
defaultContext.leftStickDeadzoneRadius = (float) stickDeadzone;
defaultContext.rightStickXAxis = MotionEvent.AXIS_Z;
defaultContext.rightStickYAxis = MotionEvent.AXIS_RZ;
defaultContext.rightStickDeadzoneRadius = (float) stickDeadzone;
defaultContext.leftTriggerAxis = MotionEvent.AXIS_BRAKE;
defaultContext.rightTriggerAxis = MotionEvent.AXIS_GAS;
defaultContext.controllerNumber = (short) 0;
}
private static InputDevice.MotionRange getMotionRangeForJoystickAxis(InputDevice dev, int axis) {
InputDevice.MotionRange range;
// First get the axis for SOURCE_JOYSTICK
range = dev.getMotionRange(axis, InputDevice.SOURCE_JOYSTICK);
if (range == null) {
// Now try the axis for SOURCE_GAMEPAD
range = dev.getMotionRange(axis, InputDevice.SOURCE_GAMEPAD);
}
return range;
}
@Override
public void onInputDeviceAdded(int deviceId) {
// Nothing happening here yet
}
@Override
public void onInputDeviceRemoved(int deviceId) {
ControllerContext context = contexts.get(deviceId);
if (context != null) {
LimeLog.info("Removed controller: "+context.name+" ("+deviceId+")");
releaseControllerNumber(context);
contexts.remove(deviceId);
}
}
@Override
public void onInputDeviceChanged(int deviceId) {
// Remove and re-add
onInputDeviceRemoved(deviceId);
onInputDeviceAdded(deviceId);
}
private void releaseControllerNumber(ControllerContext context) {
if (context.reservedControllerNumber) {
LimeLog.info("Controller number "+context.controllerNumber+" is now available");
currentControllers &= ~(1 << context.controllerNumber);
}
}
// Called before sending input but after we've determined that this
// is definitely a controller (not a keyboard, mouse, or something else)
private void assignControllerNumberIfNeeded(ControllerContext context) {
if (context.assignedControllerNumber) {
return;
}
LimeLog.info(context.name+" ("+context.id+") needs a controller number assigned");
if (context.name != null && context.name.contains("gpio-keys")) {
// This is the back button on Shield portable consoles
LimeLog.info("Built-in buttons hardcoded as controller 0");
context.controllerNumber = 0;
}
else if (multiControllerEnabled && context.hasJoystickAxes) {
context.controllerNumber = 0;
LimeLog.info("Reserving the next available controller number");
for (short i = 0; i < 4; i++) {
if ((currentControllers & (1 << i)) == 0) {
// Found an unused controller value
currentControllers |= (1 << i);
context.controllerNumber = i;
context.reservedControllerNumber = true;
break;
}
}
}
else {
LimeLog.info("Not reserving a controller number");
context.controllerNumber = 0;
}
LimeLog.info("Assigned as controller "+context.controllerNumber);
context.assignedControllerNumber = true;
}
private ControllerContext createContextForDevice(InputDevice dev) {
ControllerContext context = new ControllerContext();
String devName = dev.getName();
LimeLog.info("Creating controller context for device: "+devName);
context.name = devName;
context.id = dev.getId();
context.leftStickXAxis = MotionEvent.AXIS_X;
context.leftStickYAxis = MotionEvent.AXIS_Y;
if (getMotionRangeForJoystickAxis(dev, context.leftStickXAxis) != null &&
getMotionRangeForJoystickAxis(dev, context.leftStickYAxis) != null) {
// This is a gamepad
hasGameController = true;
context.hasJoystickAxes = true;
}
InputDevice.MotionRange leftTriggerRange = getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_LTRIGGER);
InputDevice.MotionRange rightTriggerRange = getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_RTRIGGER);
InputDevice.MotionRange brakeRange = getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_BRAKE);
InputDevice.MotionRange gasRange = getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_GAS);
if (leftTriggerRange != null && rightTriggerRange != null)
{
// Some controllers use LTRIGGER and RTRIGGER (like Ouya)
context.leftTriggerAxis = MotionEvent.AXIS_LTRIGGER;
context.rightTriggerAxis = MotionEvent.AXIS_RTRIGGER;
}
else if (brakeRange != null && gasRange != null)
{
// Others use GAS and BRAKE (like Moga)
context.leftTriggerAxis = MotionEvent.AXIS_BRAKE;
context.rightTriggerAxis = MotionEvent.AXIS_GAS;
}
else
{
InputDevice.MotionRange rxRange = getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_RX);
InputDevice.MotionRange ryRange = getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_RY);
if (rxRange != null && ryRange != null && devName != null) {
if (devName.contains("Xbox") || devName.contains("XBox") || devName.contains("X-Box")) {
// Xbox controllers use RX and RY for right stick
context.rightStickXAxis = MotionEvent.AXIS_RX;
context.rightStickYAxis = MotionEvent.AXIS_RY;
// Xbox controllers use Z and RZ for triggers
context.leftTriggerAxis = MotionEvent.AXIS_Z;
context.rightTriggerAxis = MotionEvent.AXIS_RZ;
context.triggersIdleNegative = true;
context.isXboxController = true;
}
else {
// DS4 controller uses RX and RY for triggers
context.leftTriggerAxis = MotionEvent.AXIS_RX;
context.rightTriggerAxis = MotionEvent.AXIS_RY;
context.triggersIdleNegative = true;
context.isDualShock4 = true;
}
}
}
if (context.rightStickXAxis == -1 && context.rightStickYAxis == -1) {
InputDevice.MotionRange zRange = getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_Z);
InputDevice.MotionRange rzRange = getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_RZ);
// Most other controllers use Z and RZ for the right stick
if (zRange != null && rzRange != null) {
context.rightStickXAxis = MotionEvent.AXIS_Z;
context.rightStickYAxis = MotionEvent.AXIS_RZ;
}
else {
InputDevice.MotionRange rxRange = getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_RX);
InputDevice.MotionRange ryRange = getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_RY);
// Try RX and RY now
if (rxRange != null && ryRange != null) {
context.rightStickXAxis = MotionEvent.AXIS_RX;
context.rightStickYAxis = MotionEvent.AXIS_RY;
}
}
}
// Some devices have "hats" for d-pads
InputDevice.MotionRange hatXRange = getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_HAT_X);
InputDevice.MotionRange hatYRange = getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_HAT_Y);
if (hatXRange != null && hatYRange != null) {
context.hatXAxis = MotionEvent.AXIS_HAT_X;
context.hatYAxis = MotionEvent.AXIS_HAT_Y;
}
if (context.leftStickXAxis != -1 && context.leftStickYAxis != -1) {
context.leftStickDeadzoneRadius = (float) stickDeadzone;
}
if (context.rightStickXAxis != -1 && context.rightStickYAxis != -1) {
context.rightStickDeadzoneRadius = (float) stickDeadzone;
}
if (context.leftTriggerAxis != -1 && context.rightTriggerAxis != -1) {
InputDevice.MotionRange ltRange = getMotionRangeForJoystickAxis(dev, context.leftTriggerAxis);
InputDevice.MotionRange rtRange = getMotionRangeForJoystickAxis(dev, context.rightTriggerAxis);
// It's important to have a valid deadzone so controller packet batching works properly
context.triggerDeadzone = Math.max(Math.abs(ltRange.getFlat()), Math.abs(rtRange.getFlat()));
// For triggers without (valid) deadzones, we'll use 13% (around XInput's default)
if (context.triggerDeadzone < 0.13f ||
context.triggerDeadzone > 0.30f)
{
context.triggerDeadzone = 0.13f;
}
}
// Ignore the back buttonn if a controller has both buttons
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.KITKAT) {
boolean[] hasSelectKey = dev.hasKeys(KeyEvent.KEYCODE_BUTTON_SELECT, KeyEvent.KEYCODE_BACK, 0);
if (hasSelectKey[0] && hasSelectKey[1]) {
LimeLog.info("Ignoring back button because select is present");
context.ignoreBack = true;
}
}
if (devName != null) {
// For the Nexus Player (and probably other ATV devices), we should
// use the back button as start since it doesn't have a start/menu button
// on the controller
if (devName.contains("ASUS Gamepad")) {
// We can only do this check on KitKat or higher, but it doesn't matter since ATV
// is Android 5.0 anyway
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.KITKAT) {
boolean[] hasStartKey = dev.hasKeys(KeyEvent.KEYCODE_BUTTON_START, KeyEvent.KEYCODE_MENU, 0);
if (!hasStartKey[0] && !hasStartKey[1]) {
context.backIsStart = true;
context.modeIsSelect = true;
}
}
// The ASUS Gamepad has triggers that sit far forward and are prone to false presses
// so we increase the deadzone on them to minimize this
context.triggerDeadzone = 0.30f;
}
// Classify this device as a remote by name
else if (devName.contains("Fire TV Remote") || devName.contains("Nexus Remote")) {
// It's only a remote if it doesn't any sticks
if (!context.hasJoystickAxes) {
context.ignoreBack = true;
}
}
// SHIELD controllers will use small stick deadzones
else if (devName.contains("SHIELD")) {
context.leftStickDeadzoneRadius = 0.07f;
context.rightStickDeadzoneRadius = 0.07f;
}
// Samsung's face buttons appear as a non-virtual button so we'll explicitly ignore
// back presses on this device
else if (devName.equals("sec_touchscreen")) {
context.ignoreBack = true;
}
// The Serval has a couple of unknown buttons that are start and select. It also has
// a back button which we want to ignore since there's already a select button.
else if (devName.contains("Razer Serval")) {
context.isServal = true;
context.ignoreBack = true;
}
}
LimeLog.info("Analog stick deadzone: "+context.leftStickDeadzoneRadius+" "+context.rightStickDeadzoneRadius);
LimeLog.info("Trigger deadzone: "+context.triggerDeadzone);
return context;
}
private ControllerContext getContextForDevice(InputDevice dev) {
// Unknown devices use the default context
if (dev == null) {
return defaultContext;
}
// Return the existing context if it exists
ControllerContext context = contexts.get(dev.getId());
if (context != null) {
return context;
}
// Otherwise create a new context
context = createContextForDevice(dev);
contexts.put(dev.getId(), context);
return context;
}
private void sendControllerInputPacket(ControllerContext context) {
assignControllerNumberIfNeeded(context);
conn.sendControllerInput(context.controllerNumber, context.inputMap,
context.leftTrigger, context.rightTrigger,
context.leftStickX, context.leftStickY,
context.rightStickX, context.rightStickY);
}
// Return a valid keycode, 0 to consume, or -1 to not consume the event
// Device MAY BE NULL
private int handleRemapping(ControllerContext context, KeyEvent event) {
// Don't capture the back button if configured
if (context.ignoreBack) {
if (event.getKeyCode() == KeyEvent.KEYCODE_BACK) {
return -1;
}
}
if (context.isDualShock4) {
switch (event.getKeyCode()) {
case KeyEvent.KEYCODE_BUTTON_Y:
return KeyEvent.KEYCODE_BUTTON_L1;
case KeyEvent.KEYCODE_BUTTON_Z:
return KeyEvent.KEYCODE_BUTTON_R1;
case KeyEvent.KEYCODE_BUTTON_C:
return KeyEvent.KEYCODE_BUTTON_B;
case KeyEvent.KEYCODE_BUTTON_X:
return KeyEvent.KEYCODE_BUTTON_Y;
case KeyEvent.KEYCODE_BUTTON_B:
return KeyEvent.KEYCODE_BUTTON_A;
case KeyEvent.KEYCODE_BUTTON_A:
return KeyEvent.KEYCODE_BUTTON_X;
case KeyEvent.KEYCODE_BUTTON_SELECT:
return KeyEvent.KEYCODE_BUTTON_THUMBL;
case KeyEvent.KEYCODE_BUTTON_START:
return KeyEvent.KEYCODE_BUTTON_THUMBR;
case KeyEvent.KEYCODE_BUTTON_L2:
return KeyEvent.KEYCODE_BUTTON_SELECT;
case KeyEvent.KEYCODE_BUTTON_R2:
return KeyEvent.KEYCODE_BUTTON_START;
// These are duplicate trigger events
case KeyEvent.KEYCODE_BUTTON_R1:
case KeyEvent.KEYCODE_BUTTON_L1:
return 0;
}
}
// If this is a Serval controller sending an unknown key code, it's probably
// the start and select buttons
else if (context.isServal && event.getKeyCode() == KeyEvent.KEYCODE_UNKNOWN) {
switch (event.getScanCode()) {
case 314:
return KeyEvent.KEYCODE_BUTTON_SELECT;
case 315:
return KeyEvent.KEYCODE_BUTTON_START;
}
}
if (context.hatXAxis != -1 && context.hatYAxis != -1) {
switch (event.getKeyCode()) {
// These are duplicate dpad events for hat input
case KeyEvent.KEYCODE_DPAD_LEFT:
case KeyEvent.KEYCODE_DPAD_RIGHT:
case KeyEvent.KEYCODE_DPAD_CENTER:
case KeyEvent.KEYCODE_DPAD_UP:
case KeyEvent.KEYCODE_DPAD_DOWN:
return 0;
}
}
else if (context.hatXAxis == -1 &&
context.hatYAxis == -1 &&
context.isXboxController &&
event.getKeyCode() == KeyEvent.KEYCODE_UNKNOWN) {
// If there's not a proper Xbox controller mapping, we'll translate the raw d-pad
// scan codes into proper key codes
switch (event.getScanCode())
{
case 704:
return KeyEvent.KEYCODE_DPAD_LEFT;
case 705:
return KeyEvent.KEYCODE_DPAD_RIGHT;
case 706:
return KeyEvent.KEYCODE_DPAD_UP;
case 707:
return KeyEvent.KEYCODE_DPAD_DOWN;
}
}
// Past here we can fixup the keycode and potentially trigger
// another special case so we need to remember what keycode we're using
int keyCode = event.getKeyCode();
// This is a hack for (at least) the "Tablet Remote" app
// which sends BACK with META_ALT_ON instead of KEYCODE_BUTTON_B
if (keyCode == KeyEvent.KEYCODE_BACK &&
!event.hasNoModifiers() &&
(event.getFlags() & KeyEvent.FLAG_SOFT_KEYBOARD) != 0)
{
keyCode = KeyEvent.KEYCODE_BUTTON_B;
}
if (keyCode == KeyEvent.KEYCODE_BUTTON_START ||
keyCode == KeyEvent.KEYCODE_MENU) {
// Ensure that we never use back as start if we have a real start
context.backIsStart = false;
}
else if (keyCode == KeyEvent.KEYCODE_BUTTON_SELECT) {
// Don't use mode as select if we have a select
context.modeIsSelect = false;
}
else if (context.backIsStart && keyCode == KeyEvent.KEYCODE_BACK) {
// Emulate the start button with back
return KeyEvent.KEYCODE_BUTTON_START;
}
else if (context.modeIsSelect && keyCode == KeyEvent.KEYCODE_BUTTON_MODE) {
// Emulate the select button with mode
return KeyEvent.KEYCODE_BUTTON_SELECT;
}
return keyCode;
}
private Vector2d populateCachedVector(float x, float y) {
// Reinitialize our cached Vector2d object
inputVector.initialize(x, y);
return inputVector;
}
private void handleDeadZone(Vector2d stickVector, float deadzoneRadius) {
if (stickVector.getMagnitude() <= deadzoneRadius) {
// Deadzone
stickVector.initialize(0, 0);
}
// We're not normalizing here because we let the computer handle the deadzones.
// Normalizing can make the deadzones larger than they should be after the computer also
// evaluates the deadzone.
}
private void handleAxisSet(ControllerContext context, float lsX, float lsY, float rsX,
float rsY, float lt, float rt, float hatX, float hatY) {
if (context.leftStickXAxis != -1 && context.leftStickYAxis != -1) {
Vector2d leftStickVector = populateCachedVector(lsX, lsY);
handleDeadZone(leftStickVector, context.leftStickDeadzoneRadius);
context.leftStickX = (short) (leftStickVector.getX() * 0x7FFE);
context.leftStickY = (short) (-leftStickVector.getY() * 0x7FFE);
}
if (context.rightStickXAxis != -1 && context.rightStickYAxis != -1) {
Vector2d rightStickVector = populateCachedVector(rsX, rsY);
handleDeadZone(rightStickVector, context.rightStickDeadzoneRadius);
context.rightStickX = (short) (rightStickVector.getX() * 0x7FFE);
context.rightStickY = (short) (-rightStickVector.getY() * 0x7FFE);
}
if (context.leftTriggerAxis != -1 && context.rightTriggerAxis != -1) {
if (context.triggersIdleNegative) {
lt = (lt + 1) / 2;
rt = (rt + 1) / 2;
}
if (lt <= context.triggerDeadzone) {
lt = 0;
}
if (rt <= context.triggerDeadzone) {
rt = 0;
}
context.leftTrigger = (byte)(lt * 0xFF);
context.rightTrigger = (byte)(rt * 0xFF);
}
if (context.hatXAxis != -1 && context.hatYAxis != -1) {
context.inputMap &= ~(ControllerPacket.LEFT_FLAG | ControllerPacket.RIGHT_FLAG);
if (hatX < -0.5) {
context.inputMap |= ControllerPacket.LEFT_FLAG;
}
else if (hatX > 0.5) {
context.inputMap |= ControllerPacket.RIGHT_FLAG;
}
context.inputMap &= ~(ControllerPacket.UP_FLAG | ControllerPacket.DOWN_FLAG);
if (hatY < -0.5) {
context.inputMap |= ControllerPacket.UP_FLAG;
}
else if (hatY > 0.5) {
context.inputMap |= ControllerPacket.DOWN_FLAG;
}
}
sendControllerInputPacket(context);
}
public boolean handleMotionEvent(MotionEvent event) {
ControllerContext context = getContextForDevice(event.getDevice());
float lsX = 0, lsY = 0, rsX = 0, rsY = 0, rt = 0, lt = 0, hatX = 0, hatY = 0;
// We purposefully ignore the historical values in the motion event as it makes
// the controller feel sluggish for some users.
if (context.leftStickXAxis != -1 && context.leftStickYAxis != -1) {
lsX = event.getAxisValue(context.leftStickXAxis);
lsY = event.getAxisValue(context.leftStickYAxis);
}
if (context.rightStickXAxis != -1 && context.rightStickYAxis != -1) {
rsX = event.getAxisValue(context.rightStickXAxis);
rsY = event.getAxisValue(context.rightStickYAxis);
}
if (context.leftTriggerAxis != -1 && context.rightTriggerAxis != -1) {
lt = event.getAxisValue(context.leftTriggerAxis);
rt = event.getAxisValue(context.rightTriggerAxis);
}
if (context.hatXAxis != -1 && context.hatYAxis != -1) {
hatX = event.getAxisValue(MotionEvent.AXIS_HAT_X);
hatY = event.getAxisValue(MotionEvent.AXIS_HAT_Y);
}
handleAxisSet(context, lsX, lsY, rsX, rsY, lt, rt, hatX, hatY);
return true;
}
public boolean handleButtonUp(KeyEvent event) {
ControllerContext context = getContextForDevice(event.getDevice());
int keyCode = handleRemapping(context, event);
if (keyCode == 0) {
return true;
}
// If the button hasn't been down long enough, sleep for a bit before sending the up event
// This allows "instant" button presses (like OUYA's virtual menu button) to work. This
// path should not be triggered during normal usage.
if (SystemClock.uptimeMillis() - event.getDownTime() < ControllerHandler.MINIMUM_BUTTON_DOWN_TIME_MS)
{
// Since our sleep time is so short (10 ms), it shouldn't cause a problem doing this in the
// UI thread.
try {
Thread.sleep(ControllerHandler.MINIMUM_BUTTON_DOWN_TIME_MS);
} catch (InterruptedException ignored) {}
}
switch (keyCode) {
case KeyEvent.KEYCODE_BUTTON_MODE:
context.inputMap &= ~ControllerPacket.SPECIAL_BUTTON_FLAG;
break;
case KeyEvent.KEYCODE_BUTTON_START:
case KeyEvent.KEYCODE_MENU:
if (SystemClock.uptimeMillis() - context.startDownTime > ControllerHandler.START_DOWN_TIME_KEYB_MS) {
// FIXME: The stock keyboard doesn't have controller focus so isn't usable. I'm not enabling this shortcut
// until we have a custom keyboard or some other fix
//gestures.showKeyboard();
}
context.inputMap &= ~ControllerPacket.PLAY_FLAG;
break;
case KeyEvent.KEYCODE_BACK:
case KeyEvent.KEYCODE_BUTTON_SELECT:
context.inputMap &= ~ControllerPacket.BACK_FLAG;
break;
case KeyEvent.KEYCODE_DPAD_LEFT:
context.inputMap &= ~ControllerPacket.LEFT_FLAG;
break;
case KeyEvent.KEYCODE_DPAD_RIGHT:
context.inputMap &= ~ControllerPacket.RIGHT_FLAG;
break;
case KeyEvent.KEYCODE_DPAD_UP:
context.inputMap &= ~ControllerPacket.UP_FLAG;
break;
case KeyEvent.KEYCODE_DPAD_DOWN:
context.inputMap &= ~ControllerPacket.DOWN_FLAG;
break;
case KeyEvent.KEYCODE_BUTTON_B:
context.inputMap &= ~ControllerPacket.B_FLAG;
break;
case KeyEvent.KEYCODE_DPAD_CENTER:
case KeyEvent.KEYCODE_BUTTON_A:
context.inputMap &= ~ControllerPacket.A_FLAG;
break;
case KeyEvent.KEYCODE_BUTTON_X:
context.inputMap &= ~ControllerPacket.X_FLAG;
break;
case KeyEvent.KEYCODE_BUTTON_Y:
context.inputMap &= ~ControllerPacket.Y_FLAG;
break;
case KeyEvent.KEYCODE_BUTTON_L1:
context.inputMap &= ~ControllerPacket.LB_FLAG;
context.lastLbUpTime = SystemClock.uptimeMillis();
break;
case KeyEvent.KEYCODE_BUTTON_R1:
context.inputMap &= ~ControllerPacket.RB_FLAG;
context.lastRbUpTime = SystemClock.uptimeMillis();
break;
case KeyEvent.KEYCODE_BUTTON_THUMBL:
context.inputMap &= ~ControllerPacket.LS_CLK_FLAG;
break;
case KeyEvent.KEYCODE_BUTTON_THUMBR:
context.inputMap &= ~ControllerPacket.RS_CLK_FLAG;
break;
case KeyEvent.KEYCODE_BUTTON_L2:
context.leftTrigger = 0;
break;
case KeyEvent.KEYCODE_BUTTON_R2:
context.rightTrigger = 0;
break;
default:
return false;
}
// Check if we're emulating the select button
if ((context.emulatingButtonFlags & ControllerHandler.EMULATING_SELECT) != 0)
{
// If either start or LB is up, select comes up too
if ((context.inputMap & ControllerPacket.PLAY_FLAG) == 0 ||
(context.inputMap & ControllerPacket.LB_FLAG) == 0)
{
context.inputMap &= ~ControllerPacket.BACK_FLAG;
context.emulatingButtonFlags &= ~ControllerHandler.EMULATING_SELECT;
try {
Thread.sleep(EMULATED_SELECT_UP_DELAY_MS);
} catch (InterruptedException ignored) {}
}
}
// Check if we're emulating the special button
if ((context.emulatingButtonFlags & ControllerHandler.EMULATING_SPECIAL) != 0)
{
// If either start or select and RB is up, the special button comes up too
if ((context.inputMap & ControllerPacket.PLAY_FLAG) == 0 ||
((context.inputMap & ControllerPacket.BACK_FLAG) == 0 &&
(context.inputMap & ControllerPacket.RB_FLAG) == 0))
{
context.inputMap &= ~ControllerPacket.SPECIAL_BUTTON_FLAG;
context.emulatingButtonFlags &= ~ControllerHandler.EMULATING_SPECIAL;
try {
Thread.sleep(EMULATED_SPECIAL_UP_DELAY_MS);
} catch (InterruptedException ignored) {}
}
}
sendControllerInputPacket(context);
return true;
}
public boolean handleButtonDown(KeyEvent event) {
ControllerContext context = getContextForDevice(event.getDevice());
int keyCode = handleRemapping(context, event);
if (keyCode == 0) {
return true;
}
switch (keyCode) {
case KeyEvent.KEYCODE_BUTTON_MODE:
context.inputMap |= ControllerPacket.SPECIAL_BUTTON_FLAG;
break;
case KeyEvent.KEYCODE_BUTTON_START:
case KeyEvent.KEYCODE_MENU:
if (event.getRepeatCount() == 0) {
context.startDownTime = SystemClock.uptimeMillis();
}
context.inputMap |= ControllerPacket.PLAY_FLAG;
break;
case KeyEvent.KEYCODE_BACK:
case KeyEvent.KEYCODE_BUTTON_SELECT:
context.inputMap |= ControllerPacket.BACK_FLAG;
break;
case KeyEvent.KEYCODE_DPAD_LEFT:
context.inputMap |= ControllerPacket.LEFT_FLAG;
break;
case KeyEvent.KEYCODE_DPAD_RIGHT:
context.inputMap |= ControllerPacket.RIGHT_FLAG;
break;
case KeyEvent.KEYCODE_DPAD_UP:
context.inputMap |= ControllerPacket.UP_FLAG;
break;
case KeyEvent.KEYCODE_DPAD_DOWN:
context.inputMap |= ControllerPacket.DOWN_FLAG;
break;
case KeyEvent.KEYCODE_BUTTON_B:
context.inputMap |= ControllerPacket.B_FLAG;
break;
case KeyEvent.KEYCODE_DPAD_CENTER:
case KeyEvent.KEYCODE_BUTTON_A:
context.inputMap |= ControllerPacket.A_FLAG;
break;
case KeyEvent.KEYCODE_BUTTON_X:
context.inputMap |= ControllerPacket.X_FLAG;
break;
case KeyEvent.KEYCODE_BUTTON_Y:
context.inputMap |= ControllerPacket.Y_FLAG;
break;
case KeyEvent.KEYCODE_BUTTON_L1:
context.inputMap |= ControllerPacket.LB_FLAG;
break;
case KeyEvent.KEYCODE_BUTTON_R1:
context.inputMap |= ControllerPacket.RB_FLAG;
break;
case KeyEvent.KEYCODE_BUTTON_THUMBL:
context.inputMap |= ControllerPacket.LS_CLK_FLAG;
break;
case KeyEvent.KEYCODE_BUTTON_THUMBR:
context.inputMap |= ControllerPacket.RS_CLK_FLAG;
break;
case KeyEvent.KEYCODE_BUTTON_L2:
context.leftTrigger = (byte)0xFF;
break;
case KeyEvent.KEYCODE_BUTTON_R2:
context.rightTrigger = (byte)0xFF;
break;
default:
return false;
}
// Start+LB acts like select for controllers with one button
if ((context.inputMap & ControllerPacket.PLAY_FLAG) != 0 &&
((context.inputMap & ControllerPacket.LB_FLAG) != 0 ||
SystemClock.uptimeMillis() - context.lastLbUpTime <= MAXIMUM_BUMPER_UP_DELAY_MS))
{
context.inputMap &= ~(ControllerPacket.PLAY_FLAG | ControllerPacket.LB_FLAG);
context.inputMap |= ControllerPacket.BACK_FLAG;
context.emulatingButtonFlags |= ControllerHandler.EMULATING_SELECT;
}
// We detect select+start or start+RB as the special button combo
if (((context.inputMap & ControllerPacket.RB_FLAG) != 0 ||
(SystemClock.uptimeMillis() - context.lastRbUpTime <= MAXIMUM_BUMPER_UP_DELAY_MS) ||
(context.inputMap & ControllerPacket.BACK_FLAG) != 0) &&
(context.inputMap & ControllerPacket.PLAY_FLAG) != 0)
{
context.inputMap &= ~(ControllerPacket.BACK_FLAG | ControllerPacket.PLAY_FLAG | ControllerPacket.RB_FLAG);
context.inputMap |= ControllerPacket.SPECIAL_BUTTON_FLAG;
context.emulatingButtonFlags |= ControllerHandler.EMULATING_SPECIAL;
}
// Send a new input packet if this is the first instance of a button down event
// or anytime if we're emulating a button
if (event.getRepeatCount() == 0 || context.emulatingButtonFlags != 0) {
sendControllerInputPacket(context);
}
return true;
}
class ControllerContext {
public String name;
public int id;
public int leftStickXAxis = -1;
public int leftStickYAxis = -1;
public float leftStickDeadzoneRadius;
public int rightStickXAxis = -1;
public int rightStickYAxis = -1;
public float rightStickDeadzoneRadius;
public int leftTriggerAxis = -1;
public int rightTriggerAxis = -1;
public boolean triggersIdleNegative;
public float triggerDeadzone;
public int hatXAxis = -1;
public int hatYAxis = -1;
public boolean isDualShock4;
public boolean isXboxController;
public boolean isServal;
public boolean backIsStart;
public boolean modeIsSelect;
public boolean ignoreBack;
public boolean hasJoystickAxes;
public boolean assignedControllerNumber;
public boolean reservedControllerNumber;
public short controllerNumber;
public short inputMap = 0x0000;
public byte leftTrigger = 0x00;
public byte rightTrigger = 0x00;
public short rightStickX = 0x0000;
public short rightStickY = 0x0000;
public short leftStickX = 0x0000;
public short leftStickY = 0x0000;
public int emulatingButtonFlags = 0;
// Used for OUYA bumper state tracking since they force all buttons
// up when the OUYA button goes down. We watch the last time we get
// a bumper up and compare that to our maximum delay when we receive
// a Start button press to see if we should activate one of our
// emulated button combos.
public long lastLbUpTime = 0;
public long lastRbUpTime = 0;
public long startDownTime = 0;
}
}

View File

@@ -15,7 +15,7 @@ public class KeyboardTranslator extends KeycodeTranslator {
/**
* GFE's prefix for every key code
*/
public static final short KEY_PREFIX = (short) 0x80;
private static final short KEY_PREFIX = (short) 0x80;
public static final int VK_0 = 48;
public static final int VK_9 = 57;
@@ -23,8 +23,8 @@ public class KeyboardTranslator extends KeycodeTranslator {
public static final int VK_Z = 90;
public static final int VK_ALT = 18;
public static final int VK_NUMPAD0 = 96;
public static final int VK_BACK_SLASH = 92;
public static final int VK_CAPS_LOCK = 20;
public static final int VK_BACK_SLASH = 92;
public static final int VK_CAPS_LOCK = 20;
public static final int VK_CLEAR = 12;
public static final int VK_COMMA = 44;
public static final int VK_CONTROL = 17;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,755 @@
package com.limelight.binding.video;
import java.nio.ByteBuffer;
import java.util.Locale;
import java.util.concurrent.locks.LockSupport;
import org.jcodec.codecs.h264.io.model.SeqParameterSet;
import org.jcodec.codecs.h264.io.model.VUIParameters;
import com.limelight.LimeLog;
import com.limelight.nvstream.av.ByteBufferDescriptor;
import com.limelight.nvstream.av.DecodeUnit;
import com.limelight.nvstream.av.video.VideoDecoderRenderer;
import com.limelight.nvstream.av.video.VideoDepacketizer;
import android.media.MediaCodec;
import android.media.MediaCodecInfo;
import android.media.MediaFormat;
import android.media.MediaCodec.BufferInfo;
import android.media.MediaCodec.CodecException;
import android.os.Build;
import android.view.SurfaceHolder;
public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer {
// Used on versions < 5.0
private ByteBuffer[] legacyInputBuffers;
private MediaCodec videoDecoder;
private Thread rendererThread;
private final boolean needsSpsBitstreamFixup, isExynos4;
private VideoDepacketizer depacketizer;
private final boolean adaptivePlayback, directSubmit;
private int initialWidth, initialHeight;
private boolean needsBaselineSpsHack;
private SeqParameterSet savedSps;
private long lastTimestampUs;
private long totalTimeMs;
private long decoderTimeMs;
private int totalFrames;
private String decoderName;
private int numSpsIn;
private int numPpsIn;
private int numIframeIn;
public MediaCodecDecoderRenderer() {
//dumpDecoders();
MediaCodecInfo decoder = MediaCodecHelper.findProbableSafeDecoder();
if (decoder == null) {
decoder = MediaCodecHelper.findFirstDecoder();
}
if (decoder == null) {
// This case is handled later in setup()
needsSpsBitstreamFixup = isExynos4 =
adaptivePlayback = directSubmit = false;
return;
}
decoderName = decoder.getName();
// Set decoder-specific attributes
directSubmit = MediaCodecHelper.decoderCanDirectSubmit(decoderName, decoder);
adaptivePlayback = MediaCodecHelper.decoderSupportsAdaptivePlayback(decoderName, decoder);
needsSpsBitstreamFixup = MediaCodecHelper.decoderNeedsSpsBitstreamRestrictions(decoderName, decoder);
needsBaselineSpsHack = MediaCodecHelper.decoderNeedsBaselineSpsHack(decoderName, decoder);
isExynos4 = MediaCodecHelper.isExynos4Device();
if (needsSpsBitstreamFixup) {
LimeLog.info("Decoder "+decoderName+" needs SPS bitstream restrictions fixup");
}
if (needsBaselineSpsHack) {
LimeLog.info("Decoder "+decoderName+" needs baseline SPS hack");
}
if (isExynos4) {
LimeLog.info("Decoder "+decoderName+" is on Exynos 4");
}
if (directSubmit) {
LimeLog.info("Decoder "+decoderName+" will use direct submit");
}
}
@Override
public boolean setup(int width, int height, int redrawRate, Object renderTarget, int drFlags) {
this.initialWidth = width;
this.initialHeight = height;
if (decoderName == null) {
LimeLog.severe("No available hardware decoder!");
return false;
}
// Codecs have been known to throw all sorts of crazy runtime exceptions
// due to implementation problems
try {
videoDecoder = MediaCodec.createByCodecName(decoderName);
} catch (Exception e) {
return false;
}
MediaFormat videoFormat = MediaFormat.createVideoFormat("video/avc", width, height);
// Adaptive playback can also be enabled by the whitelist on pre-KitKat devices
// so we don't fill these pre-KitKat
if (adaptivePlayback && Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
videoFormat.setInteger(MediaFormat.KEY_MAX_WIDTH, width);
videoFormat.setInteger(MediaFormat.KEY_MAX_HEIGHT, height);
}
videoDecoder.configure(videoFormat, ((SurfaceHolder)renderTarget).getSurface(), null, 0);
videoDecoder.setVideoScalingMode(MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT);
LimeLog.info("Using hardware decoding");
return true;
}
private void handleDecoderException(Exception e, ByteBuffer buf, int codecFlags) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
if (e instanceof CodecException) {
CodecException codecExc = (CodecException) e;
if (codecExc.isTransient()) {
// We'll let transient exceptions go
LimeLog.warning(codecExc.getDiagnosticInfo());
return;
}
LimeLog.severe(codecExc.getDiagnosticInfo());
}
}
// Only throw if this happens at the beginning of a stream
if (totalFrames < 60) {
if (buf != null || codecFlags != 0) {
throw new RendererException(this, e, buf, codecFlags);
}
else {
throw new RendererException(this, e);
}
}
}
private void startDirectSubmitRendererThread()
{
rendererThread = new Thread() {
@Override
public void run() {
BufferInfo info = new BufferInfo();
while (!isInterrupted()) {
try {
// Try to output a frame
int outIndex = videoDecoder.dequeueOutputBuffer(info, 50000);
if (outIndex >= 0) {
long presentationTimeUs = info.presentationTimeUs;
int lastIndex = outIndex;
// Get the last output buffer in the queue
while ((outIndex = videoDecoder.dequeueOutputBuffer(info, 0)) >= 0) {
videoDecoder.releaseOutputBuffer(lastIndex, false);
lastIndex = outIndex;
presentationTimeUs = info.presentationTimeUs;
}
// Render the last buffer
videoDecoder.releaseOutputBuffer(lastIndex, true);
// Add delta time to the totals (excluding probable outliers)
long delta = MediaCodecHelper.getMonotonicMillis() - (presentationTimeUs / 1000);
if (delta >= 0 && delta < 1000) {
decoderTimeMs += delta;
totalTimeMs += delta;
}
} else {
switch (outIndex) {
case MediaCodec.INFO_TRY_AGAIN_LATER:
break;
case MediaCodec.INFO_OUTPUT_FORMAT_CHANGED:
LimeLog.info("Output format changed");
LimeLog.info("New output Format: " + videoDecoder.getOutputFormat());
break;
default:
break;
}
}
} catch (Exception e) {
handleDecoderException(e, null, 0);
}
}
}
};
rendererThread.setName("Video - Renderer (MediaCodec)");
rendererThread.setPriority(Thread.NORM_PRIORITY + 2);
rendererThread.start();
}
private int dequeueInputBuffer(boolean wait, boolean infiniteWait) {
int index;
long startTime, queueTime;
startTime = MediaCodecHelper.getMonotonicMillis();
index = videoDecoder.dequeueInputBuffer(wait ? (infiniteWait ? -1 : 3000) : 0);
if (index < 0) {
return index;
}
queueTime = MediaCodecHelper.getMonotonicMillis();
if (queueTime - startTime >= 20) {
LimeLog.warning("Queue input buffer ran long: " + (queueTime - startTime) + " ms");
}
return index;
}
private void startLegacyRendererThread()
{
rendererThread = new Thread() {
@Override
public void run() {
BufferInfo info = new BufferInfo();
DecodeUnit du = null;
int inputIndex = -1;
long lastDuDequeueTime = 0;
while (!isInterrupted())
{
// In order to get as much data to the decoder as early as possible,
// try to submit up to 5 decode units at once without blocking.
if (inputIndex == -1 && du == null) {
try {
for (int i = 0; i < 5; i++) {
inputIndex = dequeueInputBuffer(false, false);
du = depacketizer.pollNextDecodeUnit();
if (du != null) {
lastDuDequeueTime = MediaCodecHelper.getMonotonicMillis();
notifyDuReceived(du);
}
// Stop if we can't get a DU or input buffer
if (du == null || inputIndex == -1) {
break;
}
submitDecodeUnit(du, inputIndex);
du = null;
inputIndex = -1;
}
} catch (Exception e) {
inputIndex = -1;
handleDecoderException(e, null, 0);
}
}
// Grab an input buffer if we don't have one already.
// This way we can have one ready hopefully by the time
// the depacketizer is done with this frame. It's important
// that this can timeout because it's possible that we could exhaust
// the decoder's input buffers and deadlocks because aren't pulling
// frames out of the other end.
if (inputIndex == -1) {
try {
// If we've got a DU waiting to be given to the decoder,
// wait a full 3 ms for an input buffer. Otherwise
// just see if we can get one immediately.
inputIndex = dequeueInputBuffer(du != null, false);
} catch (Exception e) {
inputIndex = -1;
handleDecoderException(e, null, 0);
}
}
// Grab a decode unit if we don't have one already
if (du == null) {
du = depacketizer.pollNextDecodeUnit();
if (du != null) {
lastDuDequeueTime = MediaCodecHelper.getMonotonicMillis();
notifyDuReceived(du);
}
}
// If we've got both a decode unit and an input buffer, we'll
// submit now. Otherwise, we wait until we have one.
if (du != null && inputIndex >= 0) {
long submissionTime = MediaCodecHelper.getMonotonicMillis();
if (submissionTime - lastDuDequeueTime >= 20) {
LimeLog.warning("Receiving an input buffer took too long: "+(submissionTime - lastDuDequeueTime)+" ms");
}
submitDecodeUnit(du, inputIndex);
// DU and input buffer have both been consumed
du = null;
inputIndex = -1;
}
// Try to output a frame
try {
int outIndex = videoDecoder.dequeueOutputBuffer(info, 0);
if (outIndex >= 0) {
long presentationTimeUs = info.presentationTimeUs;
int lastIndex = outIndex;
// Get the last output buffer in the queue
while ((outIndex = videoDecoder.dequeueOutputBuffer(info, 0)) >= 0) {
videoDecoder.releaseOutputBuffer(lastIndex, false);
lastIndex = outIndex;
presentationTimeUs = info.presentationTimeUs;
}
// Render the last buffer
videoDecoder.releaseOutputBuffer(lastIndex, true);
// Add delta time to the totals (excluding probable outliers)
long delta = MediaCodecHelper.getMonotonicMillis()-(presentationTimeUs/1000);
if (delta >= 0 && delta < 1000) {
decoderTimeMs += delta;
totalTimeMs += delta;
}
} else {
switch (outIndex) {
case MediaCodec.INFO_TRY_AGAIN_LATER:
// Getting an input buffer may already block
// so don't park if we still need to do that
if (inputIndex >= 0) {
LockSupport.parkNanos(1);
}
break;
case MediaCodec.INFO_OUTPUT_FORMAT_CHANGED:
LimeLog.info("Output format changed");
LimeLog.info("New output Format: " + videoDecoder.getOutputFormat());
break;
default:
break;
}
}
} catch (Exception e) {
handleDecoderException(e, null, 0);
}
}
}
};
rendererThread.setName("Video - Renderer (MediaCodec)");
rendererThread.setPriority(Thread.MAX_PRIORITY);
rendererThread.start();
}
@SuppressWarnings("deprecation")
@Override
public boolean start(VideoDepacketizer depacketizer) {
this.depacketizer = depacketizer;
// Start the decoder
videoDecoder.start();
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
legacyInputBuffers = videoDecoder.getInputBuffers();
}
if (directSubmit) {
startDirectSubmitRendererThread();
}
else {
startLegacyRendererThread();
}
return true;
}
@Override
public void stop() {
if (rendererThread != null) {
// Halt the rendering thread
rendererThread.interrupt();
try {
rendererThread.join();
} catch (InterruptedException ignored) { }
}
// Stop the decoder
videoDecoder.stop();
}
@Override
public void release() {
if (videoDecoder != null) {
videoDecoder.release();
}
}
private void queueInputBuffer(int inputBufferIndex, int offset, int length, long timestampUs, int codecFlags) {
// Try 25 times to submit the input buffer before throwing a real exception
int i;
Exception lastException = null;
for (i = 0; i < 25; i++) {
try {
videoDecoder.queueInputBuffer(inputBufferIndex,
offset, length,
timestampUs, codecFlags);
break;
} catch (Exception e) {
handleDecoderException(e, null, codecFlags);
lastException = e;
}
}
if (i == 25) {
throw new RendererException(this, lastException, null, codecFlags);
}
}
// Using the new getInputBuffer() API on Lollipop allows
// the framework to do some performance optimizations for us
private ByteBuffer getEmptyInputBuffer(int inputBufferIndex) {
ByteBuffer buf;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
buf = videoDecoder.getInputBuffer(inputBufferIndex);
}
else {
buf = legacyInputBuffers[inputBufferIndex];
// Clear old input data pre-Lollipop
buf.clear();
}
return buf;
}
@SuppressWarnings("deprecation")
private void submitDecodeUnit(DecodeUnit decodeUnit, int inputBufferIndex) {
long timestampUs = System.nanoTime() / 1000;
if (timestampUs <= lastTimestampUs) {
// We can't submit multiple buffers with the same timestamp
// so bump it up by one before queuing
timestampUs = lastTimestampUs + 1;
}
lastTimestampUs = timestampUs;
ByteBuffer buf = getEmptyInputBuffer(inputBufferIndex);
int codecFlags = 0;
int decodeUnitFlags = decodeUnit.getFlags();
if ((decodeUnitFlags & DecodeUnit.DU_FLAG_CODEC_CONFIG) != 0) {
codecFlags |= MediaCodec.BUFFER_FLAG_CODEC_CONFIG;
}
if ((decodeUnitFlags & DecodeUnit.DU_FLAG_SYNC_FRAME) != 0) {
codecFlags |= MediaCodec.BUFFER_FLAG_SYNC_FRAME;
numIframeIn++;
}
boolean needsSpsReplay = false;
if ((decodeUnitFlags & DecodeUnit.DU_FLAG_CODEC_CONFIG) != 0) {
ByteBufferDescriptor header = decodeUnit.getBufferHead();
if (header.data[header.offset+4] == 0x67) {
numSpsIn++;
ByteBuffer spsBuf = ByteBuffer.wrap(header.data);
// Skip to the start of the NALU data
spsBuf.position(header.offset+5);
SeqParameterSet sps = SeqParameterSet.read(spsBuf);
// Some decoders rely on H264 level to decide how many buffers are needed
// Since we only need one frame buffered, we'll set the level as low as we can
// for known resolution combinations
if (initialWidth == 1280 && initialHeight == 720) {
// Max 5 buffered frames at 1280x720x60
LimeLog.info("Patching level_idc to 32");
sps.level_idc = 32;
}
else if (initialWidth == 1920 && initialHeight == 1080) {
// Max 4 buffered frames at 1920x1080x64
LimeLog.info("Patching level_idc to 42");
sps.level_idc = 42;
}
else {
// Leave the profile alone (currently 5.0)
}
// TI OMAP4 requires a reference frame count of 1 to decode successfully. Exynos 4
// also requires this fixup.
//
// I'm doing this fixup for all devices because I haven't seen any devices that
// this causes issues for. At worst, it seems to do nothing and at best it fixes
// issues with video lag, hangs, and crashes.
LimeLog.info("Patching num_ref_frames in SPS");
sps.num_ref_frames = 1;
// GFE 2.5.11 changed the SPS to add additional extensions
// Some devices don't like these so we remove them here.
sps.vuiParams.video_signal_type_present_flag = false;
sps.vuiParams.colour_description_present_flag = false;
sps.vuiParams.chroma_loc_info_present_flag = false;
if (needsSpsBitstreamFixup || isExynos4) {
// The SPS that comes in the current H264 bytestream doesn't set bitstream_restriction_flag
// or max_dec_frame_buffering which increases decoding latency on Tegra.
// GFE 2.5.11 started sending bitstream restrictions
if (sps.vuiParams.bitstreamRestriction == null) {
LimeLog.info("Adding bitstream restrictions");
sps.vuiParams.bitstreamRestriction = new VUIParameters.BitstreamRestriction();
sps.vuiParams.bitstreamRestriction.motion_vectors_over_pic_boundaries_flag = true;
sps.vuiParams.bitstreamRestriction.log2_max_mv_length_horizontal = 16;
sps.vuiParams.bitstreamRestriction.log2_max_mv_length_vertical = 16;
sps.vuiParams.bitstreamRestriction.num_reorder_frames = 0;
}
else {
LimeLog.info("Patching bitstream restrictions");
}
// Some devices throw errors if max_dec_frame_buffering < num_ref_frames
sps.vuiParams.bitstreamRestriction.max_dec_frame_buffering = sps.num_ref_frames;
// These values are the defaults for the fields, but they are more aggressive
// than what GFE sends in 2.5.11, but it doesn't seem to cause picture problems.
sps.vuiParams.bitstreamRestriction.max_bytes_per_pic_denom = 2;
sps.vuiParams.bitstreamRestriction.max_bits_per_mb_denom = 1;
// log2_max_mv_length_horizontal and log2_max_mv_length_vertical are set to more
// conservative values by GFE 2.5.11. We'll let those values stand.
}
else {
// Devices that didn't/couldn't get bitstream restrictions before GFE 2.5.11
// will continue to not receive them now
sps.vuiParams.bitstreamRestriction = null;
}
// Set constraint flags 4 & 5 to make this Constrained High Profile
// which allows the decoder to assume there will be no B-frames and
// reduce delay and buffering accordingly.
//
// This profile is fairly new (standardized in H264 revision 2012-06) and
// it's known that at least some devices don't like these previously unused
// constraints being set. To minimize the chance of interfering with old devices,
// I'm only setting these on KitKat or higher. It's an arbitrary limitation and could
// change if it causes problems.
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
sps.constraint_set_4_flag = true;
sps.constraint_set_5_flag = true;
}
else {
// Force the constraints unset for < 4.4 (some may be set by default)
sps.constraint_set_4_flag = false;
sps.constraint_set_5_flag = false;
}
// If we need to hack this SPS to say we're baseline, do so now
if (needsBaselineSpsHack) {
LimeLog.info("Hacking SPS to baseline");
sps.profile_idc = 66;
savedSps = sps;
}
// Write the annex B header
buf.put(header.data, header.offset, 5);
// Write the modified SPS to the input buffer
sps.write(buf);
queueInputBuffer(inputBufferIndex,
0, buf.position(),
timestampUs, codecFlags);
depacketizer.freeDecodeUnit(decodeUnit);
return;
} else if (header.data[header.offset+4] == 0x68) {
numPpsIn++;
if (needsBaselineSpsHack) {
LimeLog.info("Saw PPS; disabling SPS hack");
needsBaselineSpsHack = false;
// Give the decoder the SPS again with the proper profile now
needsSpsReplay = true;
}
}
}
// Copy data from our buffer list into the input buffer
for (ByteBufferDescriptor desc = decodeUnit.getBufferHead();
desc != null; desc = desc.nextDescriptor) {
buf.put(desc.data, desc.offset, desc.length);
}
queueInputBuffer(inputBufferIndex,
0, decodeUnit.getDataLength(),
timestampUs, codecFlags);
depacketizer.freeDecodeUnit(decodeUnit);
if (needsSpsReplay) {
replaySps();
}
}
private void replaySps() {
int inputIndex = dequeueInputBuffer(true, true);
ByteBuffer inputBuffer = getEmptyInputBuffer(inputIndex);
// Write the Annex B header
inputBuffer.put(new byte[]{0x00, 0x00, 0x00, 0x01, 0x67});
// Switch the H264 profile back to high
savedSps.profile_idc = 100;
// Write the SPS data
savedSps.write(inputBuffer);
// No need for the SPS anymore
savedSps = null;
// Queue the new SPS
queueInputBuffer(inputIndex,
0, inputBuffer.position(),
System.nanoTime() / 1000,
MediaCodec.BUFFER_FLAG_CODEC_CONFIG);
LimeLog.info("SPS replay complete");
}
@Override
public int getCapabilities() {
int caps = 0;
caps |= adaptivePlayback ?
VideoDecoderRenderer.CAPABILITY_ADAPTIVE_RESOLUTION : 0;
caps |= directSubmit ?
VideoDecoderRenderer.CAPABILITY_DIRECT_SUBMIT : 0;
return caps;
}
@Override
public int getAverageDecoderLatency() {
if (totalFrames == 0) {
return 0;
}
return (int)(decoderTimeMs / totalFrames);
}
@Override
public int getAverageEndToEndLatency() {
if (totalFrames == 0) {
return 0;
}
return (int)(totalTimeMs / totalFrames);
}
@Override
public String getDecoderName() {
return decoderName;
}
private void notifyDuReceived(DecodeUnit du) {
long currentTime = MediaCodecHelper.getMonotonicMillis();
long delta = currentTime-du.getReceiveTimestamp();
if (delta >= 0 && delta < 1000) {
totalTimeMs += currentTime-du.getReceiveTimestamp();
totalFrames++;
}
}
@Override
public void directSubmitDecodeUnit(DecodeUnit du) {
int inputIndex;
notifyDuReceived(du);
for (;;) {
try {
inputIndex = dequeueInputBuffer(true, true);
break;
} catch (Exception e) {
handleDecoderException(e, null, 0);
}
}
if (inputIndex >= 0) {
submitDecodeUnit(du, inputIndex);
}
}
public class RendererException extends RuntimeException {
private static final long serialVersionUID = 8985937536997012406L;
private final Exception originalException;
private final MediaCodecDecoderRenderer renderer;
private ByteBuffer currentBuffer;
private int currentCodecFlags;
public RendererException(MediaCodecDecoderRenderer renderer, Exception e) {
this.renderer = renderer;
this.originalException = e;
}
public RendererException(MediaCodecDecoderRenderer renderer, Exception e, ByteBuffer currentBuffer, int currentCodecFlags) {
this.renderer = renderer;
this.originalException = e;
this.currentBuffer = currentBuffer;
this.currentCodecFlags = currentCodecFlags;
}
public String toString() {
String str = "";
str += "Decoder: "+renderer.decoderName+"\n";
str += "Initial video dimensions: "+renderer.initialWidth+"x"+renderer.initialHeight+"\n";
str += "In stats: "+renderer.numSpsIn+", "+renderer.numPpsIn+", "+renderer.numIframeIn+"\n";
str += "Total frames: "+renderer.totalFrames+"\n";
str += "Average end-to-end client latency: "+getAverageEndToEndLatency()+"ms\n";
str += "Average hardware decoder latency: "+getAverageDecoderLatency()+"ms\n";
if (currentBuffer != null) {
str += "Current buffer: ";
currentBuffer.flip();
while (currentBuffer.hasRemaining() && currentBuffer.position() < 10) {
str += String.format((Locale)null, "%02x ", currentBuffer.get());
}
str += "\n";
str += "Buffer codec flags: "+currentCodecFlags+"\n";
}
str += "Is Exynos 4: "+renderer.isExynos4+"\n";
str += "/proc/cpuinfo:\n";
try {
str += MediaCodecHelper.readCpuinfo();
} catch (Exception e) {
str += e.getMessage();
}
str += "Full decoder dump:\n";
try {
str += MediaCodecHelper.dumpDecoders();
} catch (Exception e) {
str += e.getMessage();
}
str += originalException.toString();
return str;
}
}
}

View File

@@ -0,0 +1,335 @@
package com.limelight.binding.video;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
import android.annotation.SuppressLint;
import android.annotation.TargetApi;
import android.media.MediaCodecInfo;
import android.media.MediaCodecList;
import android.media.MediaCodecInfo.CodecCapabilities;
import android.media.MediaCodecInfo.CodecProfileLevel;
import android.os.Build;
import com.limelight.LimeLog;
public class MediaCodecHelper {
private static final List<String> preferredDecoders;
private static final List<String> blacklistedDecoderPrefixes;
private static final List<String> spsFixupBitstreamFixupDecoderPrefixes;
private static final List<String> whitelistedAdaptiveResolutionPrefixes;
private static final List<String> baselineProfileHackPrefixes;
private static final List<String> directSubmitPrefixes;
static {
directSubmitPrefixes = new LinkedList<String>();
// These decoders have low enough input buffer latency that they
// can be directly invoked from the receive thread
directSubmitPrefixes.add("omx.qcom");
directSubmitPrefixes.add("omx.sec");
directSubmitPrefixes.add("omx.exynos");
directSubmitPrefixes.add("omx.intel");
directSubmitPrefixes.add("omx.brcm");
directSubmitPrefixes.add("omx.TI");
directSubmitPrefixes.add("omx.arc");
}
static {
preferredDecoders = new LinkedList<String>();
}
static {
blacklistedDecoderPrefixes = new LinkedList<String>();
// Software decoders that don't support H264 high profile
blacklistedDecoderPrefixes.add("omx.google");
blacklistedDecoderPrefixes.add("AVCDecoder");
}
static {
spsFixupBitstreamFixupDecoderPrefixes = new LinkedList<String>();
spsFixupBitstreamFixupDecoderPrefixes.add("omx.nvidia");
spsFixupBitstreamFixupDecoderPrefixes.add("omx.qcom");
spsFixupBitstreamFixupDecoderPrefixes.add("omx.brcm");
baselineProfileHackPrefixes = new LinkedList<String>();
baselineProfileHackPrefixes.add("omx.intel");
whitelistedAdaptiveResolutionPrefixes = new LinkedList<String>();
whitelistedAdaptiveResolutionPrefixes.add("omx.nvidia");
whitelistedAdaptiveResolutionPrefixes.add("omx.qcom");
whitelistedAdaptiveResolutionPrefixes.add("omx.sec");
whitelistedAdaptiveResolutionPrefixes.add("omx.TI");
}
private static boolean isDecoderInList(List<String> decoderList, String decoderName) {
for (String badPrefix : decoderList) {
if (decoderName.length() >= badPrefix.length()) {
String prefix = decoderName.substring(0, badPrefix.length());
if (prefix.equalsIgnoreCase(badPrefix)) {
return true;
}
}
}
return false;
}
public static long getMonotonicMillis() {
return System.nanoTime() / 1000000L;
}
@TargetApi(Build.VERSION_CODES.KITKAT)
public static boolean decoderSupportsAdaptivePlayback(String decoderName, MediaCodecInfo decoderInfo) {
/*
FIXME: Intel's decoder on Nexus Player forces the high latency path if adaptive playback is enabled
so we'll keep it off for now, since we don't know whether other devices also do the same
if (isDecoderInList(whitelistedAdaptiveResolutionPrefixes, decoderName)) {
LimeLog.info("Adaptive playback supported (whitelist)");
return true;
}
// Possibly enable adaptive playback on KitKat and above
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
try {
if (decoderInfo.getCapabilitiesForType("video/avc").
isFeatureSupported(CodecCapabilities.FEATURE_AdaptivePlayback))
{
// This will make getCapabilities() return that adaptive playback is supported
LimeLog.info("Adaptive playback supported (FEATURE_AdaptivePlayback)");
return true;
}
} catch (Exception e) {
// Tolerate buggy codecs
}
}*/
return false;
}
public static boolean decoderCanDirectSubmit(String decoderName, MediaCodecInfo decoderInfo) {
return isDecoderInList(directSubmitPrefixes, decoderName) && !isExynos4Device();
}
public static boolean decoderNeedsSpsBitstreamRestrictions(String decoderName, MediaCodecInfo decoderInfo) {
return isDecoderInList(spsFixupBitstreamFixupDecoderPrefixes, decoderName);
}
public static boolean decoderNeedsBaselineSpsHack(String decoderName, MediaCodecInfo decoderInfo) {
return isDecoderInList(baselineProfileHackPrefixes, decoderName);
}
@SuppressWarnings("deprecation")
@SuppressLint("NewApi")
private static LinkedList<MediaCodecInfo> getMediaCodecList() {
LinkedList<MediaCodecInfo> infoList = new LinkedList<MediaCodecInfo>();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
MediaCodecList mcl = new MediaCodecList(MediaCodecList.REGULAR_CODECS);
Collections.addAll(infoList, mcl.getCodecInfos());
}
else {
for (int i = 0; i < MediaCodecList.getCodecCount(); i++) {
infoList.add(MediaCodecList.getCodecInfoAt(i));
}
}
return infoList;
}
@SuppressWarnings("RedundantThrows")
public static String dumpDecoders() throws Exception {
String str = "";
for (MediaCodecInfo codecInfo : getMediaCodecList()) {
// Skip encoders
if (codecInfo.isEncoder()) {
continue;
}
str += "Decoder: "+codecInfo.getName()+"\n";
for (String type : codecInfo.getSupportedTypes()) {
str += "\t"+type+"\n";
CodecCapabilities caps = codecInfo.getCapabilitiesForType(type);
for (CodecProfileLevel profile : caps.profileLevels) {
str += "\t\t"+profile.profile+" "+profile.level+"\n";
}
}
}
return str;
}
private static MediaCodecInfo findPreferredDecoder() {
// This is a different algorithm than the other findXXXDecoder functions,
// because we want to evaluate the decoders in our list's order
// rather than MediaCodecList's order
for (String preferredDecoder : preferredDecoders) {
for (MediaCodecInfo codecInfo : getMediaCodecList()) {
// Skip encoders
if (codecInfo.isEncoder()) {
continue;
}
// Check for preferred decoders
if (preferredDecoder.equalsIgnoreCase(codecInfo.getName())) {
LimeLog.info("Preferred decoder choice is "+codecInfo.getName());
return codecInfo;
}
}
}
return null;
}
public static MediaCodecInfo findFirstDecoder() {
for (MediaCodecInfo codecInfo : getMediaCodecList()) {
// Skip encoders
if (codecInfo.isEncoder()) {
continue;
}
// Check for explicitly blacklisted decoders
if (isDecoderInList(blacklistedDecoderPrefixes, codecInfo.getName())) {
LimeLog.info("Skipping blacklisted decoder: "+codecInfo.getName());
continue;
}
// Find a decoder that supports H.264
for (String mime : codecInfo.getSupportedTypes()) {
if (mime.equalsIgnoreCase("video/avc")) {
LimeLog.info("First decoder choice is "+codecInfo.getName());
return codecInfo;
}
}
}
return null;
}
public static MediaCodecInfo findProbableSafeDecoder() {
// First look for a preferred decoder by name
MediaCodecInfo info = findPreferredDecoder();
if (info != null) {
return info;
}
// Now look for decoders we know are safe
try {
// If this function completes, it will determine if the decoder is safe
return findKnownSafeDecoder();
} catch (Exception e) {
// Some buggy devices seem to throw exceptions
// from getCapabilitiesForType() so we'll just assume
// they're okay and go with the first one we find
return findFirstDecoder();
}
}
// We declare this method as explicitly throwing Exception
// since some bad decoders can throw IllegalArgumentExceptions unexpectedly
// and we want to be sure all callers are handling this possibility
@SuppressWarnings("RedundantThrows")
private static MediaCodecInfo findKnownSafeDecoder() throws Exception {
for (MediaCodecInfo codecInfo : getMediaCodecList()) {
// Skip encoders
if (codecInfo.isEncoder()) {
continue;
}
// Check for explicitly blacklisted decoders
if (isDecoderInList(blacklistedDecoderPrefixes, codecInfo.getName())) {
LimeLog.info("Skipping blacklisted decoder: "+codecInfo.getName());
continue;
}
// Find a decoder that supports H.264 high profile
for (String mime : codecInfo.getSupportedTypes()) {
if (mime.equalsIgnoreCase("video/avc")) {
LimeLog.info("Examining decoder capabilities of "+codecInfo.getName());
CodecCapabilities caps = codecInfo.getCapabilitiesForType(mime);
for (CodecProfileLevel profile : caps.profileLevels) {
if (profile.profile == CodecProfileLevel.AVCProfileHigh) {
LimeLog.info("Decoder "+codecInfo.getName()+" supports high profile");
LimeLog.info("Selected decoder: "+codecInfo.getName());
return codecInfo;
}
}
LimeLog.info("Decoder "+codecInfo.getName()+" does NOT support high profile");
}
}
}
return null;
}
public static String readCpuinfo() throws Exception {
StringBuilder cpuInfo = new StringBuilder();
BufferedReader br = new BufferedReader(new FileReader(new File("/proc/cpuinfo")));
try {
for (;;) {
int ch = br.read();
if (ch == -1)
break;
cpuInfo.append((char)ch);
}
return cpuInfo.toString();
} finally {
br.close();
}
}
private static boolean stringContainsIgnoreCase(String string, String substring) {
return string.toLowerCase(Locale.ENGLISH).contains(substring.toLowerCase(Locale.ENGLISH));
}
public static boolean isExynos4Device() {
try {
// Try reading CPU info too look for
String cpuInfo = readCpuinfo();
// SMDK4xxx is Exynos 4
if (stringContainsIgnoreCase(cpuInfo, "SMDK4")) {
LimeLog.info("Found SMDK4 in /proc/cpuinfo");
return true;
}
// If we see "Exynos 4" also we'll count it
if (stringContainsIgnoreCase(cpuInfo, "Exynos 4")) {
LimeLog.info("Found Exynos 4 in /proc/cpuinfo");
return true;
}
} catch (Exception e) {
e.printStackTrace();
}
try {
File systemDir = new File("/sys/devices/system");
File[] files = systemDir.listFiles();
if (files != null) {
for (File f : files) {
if (stringContainsIgnoreCase(f.getName(), "exynos4")) {
LimeLog.info("Found exynos4 in /sys/devices/system");
return true;
}
}
}
} catch (Exception e) {
e.printStackTrace();
}
return false;
}
}

View File

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

View File

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

View File

@@ -0,0 +1,736 @@
package com.limelight.computers;
import java.io.IOException;
import java.io.OutputStream;
import java.io.StringReader;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.util.LinkedList;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicInteger;
import com.limelight.LimeLog;
import com.limelight.binding.PlatformBinding;
import com.limelight.discovery.DiscoveryService;
import com.limelight.nvstream.http.ComputerDetails;
import com.limelight.nvstream.http.NvApp;
import com.limelight.nvstream.http.NvHTTP;
import com.limelight.nvstream.mdns.MdnsComputer;
import com.limelight.nvstream.mdns.MdnsDiscoveryListener;
import com.limelight.utils.CacheHelper;
import android.app.Service;
import android.content.ComponentName;
import android.content.Intent;
import android.content.ServiceConnection;
import android.os.Binder;
import android.os.IBinder;
import org.xmlpull.v1.XmlPullParserException;
public class ComputerManagerService extends Service {
private static final int SERVERINFO_POLLING_PERIOD_MS = 3000;
private static final int APPLIST_POLLING_PERIOD_MS = 30000;
private static final int MDNS_QUERY_PERIOD_MS = 1000;
private static final int FAST_POLL_TIMEOUT = 500;
private static final int OFFLINE_POLL_TRIES = 3;
private final ComputerManagerBinder binder = new ComputerManagerBinder();
private ComputerDatabaseManager dbManager;
private final AtomicInteger dbRefCount = new AtomicInteger(0);
private IdentityManager idManager;
private final LinkedList<PollingTuple> pollingTuples = new LinkedList<PollingTuple>();
private ComputerManagerListener listener = null;
private final AtomicInteger activePolls = new AtomicInteger(0);
private boolean pollingActive = false;
private DiscoveryService.DiscoveryBinder discoveryBinder;
private final ServiceConnection discoveryServiceConnection = new ServiceConnection() {
public void onServiceConnected(ComponentName className, IBinder binder) {
synchronized (discoveryServiceConnection) {
DiscoveryService.DiscoveryBinder privateBinder = ((DiscoveryService.DiscoveryBinder)binder);
// Set us as the event listener
privateBinder.setListener(createDiscoveryListener());
// Signal a possible waiter that we're all setup
discoveryBinder = privateBinder;
discoveryServiceConnection.notifyAll();
}
}
public void onServiceDisconnected(ComponentName className) {
discoveryBinder = null;
}
};
// Returns true if the details object was modified
private boolean runPoll(ComputerDetails details, boolean newPc, int offlineCount) throws InterruptedException {
if (!getLocalDatabaseReference()) {
return false;
}
activePolls.incrementAndGet();
// Poll the machine
try {
if (!pollComputer(details)) {
if (!newPc && offlineCount < OFFLINE_POLL_TRIES) {
// Return without calling the listener
releaseLocalDatabaseReference();
return false;
}
details.state = ComputerDetails.State.OFFLINE;
details.reachability = ComputerDetails.Reachability.OFFLINE;
}
} catch (InterruptedException e) {
releaseLocalDatabaseReference();
throw e;
} finally {
activePolls.decrementAndGet();
}
// If it's online, update our persistent state
if (details.state == ComputerDetails.State.ONLINE) {
if (!newPc) {
// Check if it's in the database because it could have been
// removed after this was issued
if (dbManager.getComputerByName(details.name) == null) {
// It's gone
releaseLocalDatabaseReference();
return false;
}
}
dbManager.updateComputer(details);
}
// Don't call the listener if this is a failed lookup of a new PC
if ((!newPc || details.state == ComputerDetails.State.ONLINE) && listener != null) {
listener.notifyComputerUpdated(details);
}
releaseLocalDatabaseReference();
return true;
}
private Thread createPollingThread(final ComputerDetails details) {
Thread t = new Thread() {
@Override
public void run() {
int offlineCount = 0;
while (!isInterrupted() && pollingActive) {
try {
// Check if this poll has modified the details
if (!runPoll(details, false, offlineCount)) {
LimeLog.warning(details.name + " is offline (try " + offlineCount + ")");
offlineCount++;
}
else {
offlineCount = 0;
}
// Wait until the next polling interval
Thread.sleep(SERVERINFO_POLLING_PERIOD_MS / ((offlineCount > 0) ? 2 : 1));
} catch (InterruptedException e) {
break;
}
}
}
};
t.setName("Polling thread for "+details.localIp.getHostAddress());
return t;
}
public class ComputerManagerBinder extends Binder {
public void startPolling(ComputerManagerListener listener) {
// Polling is active
pollingActive = true;
// Set the listener
ComputerManagerService.this.listener = listener;
// Start mDNS autodiscovery too
discoveryBinder.startDiscovery(MDNS_QUERY_PERIOD_MS);
synchronized (pollingTuples) {
for (PollingTuple tuple : pollingTuples) {
// This polling thread might already be there
if (tuple.thread == null) {
// Report this computer initially
listener.notifyComputerUpdated(tuple.computer);
tuple.thread = createPollingThread(tuple.computer);
tuple.thread.start();
}
}
}
}
public void waitForReady() {
synchronized (discoveryServiceConnection) {
try {
while (discoveryBinder == null) {
// Wait for the bind notification
discoveryServiceConnection.wait(1000);
}
} catch (InterruptedException ignored) {
}
}
}
public void waitForPollingStopped() {
while (activePolls.get() != 0) {
try {
Thread.sleep(250);
} catch (InterruptedException ignored) {}
}
}
public boolean addComputerBlocking(InetAddress addr) {
return ComputerManagerService.this.addComputerBlocking(addr);
}
public void removeComputer(String name) {
ComputerManagerService.this.removeComputer(name);
}
public void stopPolling() {
// Just call the unbind handler to cleanup
ComputerManagerService.this.onUnbind(null);
}
public ApplistPoller createAppListPoller(ComputerDetails computer) {
return new ApplistPoller(computer);
}
public String getUniqueId() {
return idManager.getUniqueId();
}
public ComputerDetails getComputer(UUID uuid) {
synchronized (pollingTuples) {
for (PollingTuple tuple : pollingTuples) {
if (uuid.equals(tuple.computer.uuid)) {
return tuple.computer;
}
}
}
return null;
}
}
@Override
public boolean onUnbind(Intent intent) {
// Stop mDNS autodiscovery
discoveryBinder.stopDiscovery();
// Stop polling
pollingActive = false;
synchronized (pollingTuples) {
for (PollingTuple tuple : pollingTuples) {
if (tuple.thread != null) {
// Interrupt and remove the thread
tuple.thread.interrupt();
tuple.thread = null;
}
}
}
// Remove the listener
listener = null;
return false;
}
private MdnsDiscoveryListener createDiscoveryListener() {
return new MdnsDiscoveryListener() {
@Override
public void notifyComputerAdded(MdnsComputer computer) {
// Kick off a serverinfo poll on this machine
addComputerBlocking(computer.getAddress());
}
@Override
public void notifyComputerRemoved(MdnsComputer computer) {
// Nothing to do here
}
@Override
public void notifyDiscoveryFailure(Exception e) {
LimeLog.severe("mDNS discovery failed");
e.printStackTrace();
}
};
}
private void addTuple(ComputerDetails details) {
synchronized (pollingTuples) {
for (PollingTuple tuple : pollingTuples) {
// Check if this is the same computer
if (tuple.computer.uuid.equals(details.uuid)) {
// Update details anyway in case this machine has been re-added by IP
// after not being reachable by our existing information
tuple.computer.localIp = details.localIp;
tuple.computer.remoteIp = details.remoteIp;
// Start a polling thread if polling is active
if (pollingActive && tuple.thread == null) {
tuple.thread = createPollingThread(details);
tuple.thread.start();
}
// Found an entry so we're done
return;
}
}
// If we got here, we didn't find an entry
PollingTuple tuple = new PollingTuple(details, pollingActive ? createPollingThread(details) : null);
pollingTuples.add(tuple);
if (tuple.thread != null) {
tuple.thread.start();
}
}
}
public boolean addComputerBlocking(InetAddress addr) {
// Setup a placeholder
ComputerDetails fakeDetails = new ComputerDetails();
fakeDetails.localIp = addr;
fakeDetails.remoteIp = addr;
// Block while we try to fill the details
try {
runPoll(fakeDetails, true, 0);
} catch (InterruptedException e) {
return false;
}
// If the machine is reachable, it was successful
if (fakeDetails.state == ComputerDetails.State.ONLINE) {
LimeLog.info("New PC ("+fakeDetails.name+") is UUID "+fakeDetails.uuid);
// Start a polling thread for this machine
addTuple(fakeDetails);
return true;
}
else {
return false;
}
}
public void removeComputer(String name) {
if (!getLocalDatabaseReference()) {
return;
}
// Remove it from the database
dbManager.deleteComputer(name);
synchronized (pollingTuples) {
// Remove the computer from the computer list
for (PollingTuple tuple : pollingTuples) {
if (tuple.computer.name.equals(name)) {
if (tuple.thread != null) {
// Interrupt the thread on this entry
tuple.thread.interrupt();
}
pollingTuples.remove(tuple);
break;
}
}
}
releaseLocalDatabaseReference();
}
private boolean getLocalDatabaseReference() {
if (dbRefCount.get() == 0) {
return false;
}
dbRefCount.incrementAndGet();
return true;
}
private void releaseLocalDatabaseReference() {
if (dbRefCount.decrementAndGet() == 0) {
dbManager.close();
}
}
private ComputerDetails tryPollIp(ComputerDetails details, InetAddress ipAddr) {
// Fast poll this address first to determine if we can connect at the TCP layer
if (!fastPollIp(ipAddr)) {
return null;
}
try {
NvHTTP http = new NvHTTP(ipAddr, idManager.getUniqueId(),
null, PlatformBinding.getCryptoProvider(ComputerManagerService.this));
ComputerDetails newDetails = http.getComputerDetails();
// Check if this is the PC we expected
if (details.uuid != null && newDetails.uuid != null &&
!details.uuid.equals(newDetails.uuid)) {
// We got the wrong PC!
LimeLog.info("Polling returned the wrong PC!");
return null;
}
return newDetails;
} catch (Exception e) {
return null;
}
}
// Just try to establish a TCP connection to speculatively detect a running
// GFE server
private boolean fastPollIp(InetAddress addr) {
Socket s = new Socket();
try {
s.connect(new InetSocketAddress(addr, NvHTTP.HTTPS_PORT), FAST_POLL_TIMEOUT);
s.close();
return true;
} catch (IOException e) {
return false;
}
}
private void startFastPollThread(final InetAddress addr, final boolean[] info) {
Thread t = new Thread() {
@Override
public void run() {
boolean pollRes = fastPollIp(addr);
synchronized (info) {
info[0] = true; // Done
info[1] = pollRes; // Polling result
info.notify();
}
}
};
t.setName("Fast Poll - "+addr.getHostAddress());
t.start();
}
private ComputerDetails.Reachability fastPollPc(final InetAddress local, final InetAddress remote) throws InterruptedException {
final boolean[] remoteInfo = new boolean[2];
final boolean[] localInfo = new boolean[2];
startFastPollThread(local, localInfo);
startFastPollThread(remote, remoteInfo);
// Check local first
synchronized (localInfo) {
while (!localInfo[0]) {
localInfo.wait(500);
}
if (localInfo[1]) {
return ComputerDetails.Reachability.LOCAL;
}
}
// Now remote
synchronized (remoteInfo) {
while (!remoteInfo[0]) {
remoteInfo.wait(500);
}
if (remoteInfo[1]) {
return ComputerDetails.Reachability.REMOTE;
}
}
return ComputerDetails.Reachability.OFFLINE;
}
private ReachabilityTuple pollForReachability(ComputerDetails details) throws InterruptedException {
ComputerDetails polledDetails;
ComputerDetails.Reachability reachability;
// If the local address is routable across the Internet,
// always consider this PC remote to be conservative
if (details.localIp.equals(details.remoteIp)) {
reachability = ComputerDetails.Reachability.REMOTE;
}
else {
// Do a TCP-level connection to the HTTP server to see if it's listening
LimeLog.info("Starting fast poll for "+details.name+" ("+details.localIp+", "+details.remoteIp+")");
reachability = fastPollPc(details.localIp, details.remoteIp);
LimeLog.info("Fast poll for "+details.name+" returned "+reachability.toString());
// If no connection could be established to either IP address, there's nothing we can do
if (reachability == ComputerDetails.Reachability.OFFLINE) {
return null;
}
}
boolean localFirst = (reachability == ComputerDetails.Reachability.LOCAL);
if (localFirst) {
polledDetails = tryPollIp(details, details.localIp);
}
else {
polledDetails = tryPollIp(details, details.remoteIp);
}
InetAddress reachableAddr = null;
if (polledDetails == null && !details.localIp.equals(details.remoteIp)) {
// Failed, so let's try the fallback
if (!localFirst) {
polledDetails = tryPollIp(details, details.localIp);
}
else {
polledDetails = tryPollIp(details, details.remoteIp);
}
if (polledDetails != null) {
// The fallback poll worked
reachableAddr = !localFirst ? details.localIp : details.remoteIp;
}
}
else if (polledDetails != null) {
reachableAddr = localFirst ? details.localIp : details.remoteIp;
}
if (reachableAddr == null) {
return null;
}
if (polledDetails.remoteIp.equals(reachableAddr)) {
polledDetails.reachability = ComputerDetails.Reachability.REMOTE;
}
else if (polledDetails.localIp.equals(reachableAddr)) {
polledDetails.reachability = ComputerDetails.Reachability.LOCAL;
}
else {
polledDetails.reachability = ComputerDetails.Reachability.UNKNOWN;
}
return new ReachabilityTuple(polledDetails, reachableAddr);
}
private boolean pollComputer(ComputerDetails details) throws InterruptedException {
ReachabilityTuple initialReachTuple = pollForReachability(details);
if (initialReachTuple == null) {
return false;
}
if (initialReachTuple.computer.reachability == ComputerDetails.Reachability.UNKNOWN) {
// Neither IP address reported in the serverinfo response was the one we used.
// Poll again to see if we can contact this machine on either of its reported addresses.
ReachabilityTuple confirmationReachTuple = pollForReachability(initialReachTuple.computer);
if (confirmationReachTuple == null) {
// Neither of those seem to work, so we'll hold onto the address that did work
initialReachTuple.computer.localIp = initialReachTuple.reachableAddress;
initialReachTuple.computer.reachability = ComputerDetails.Reachability.LOCAL;
}
else {
// We got it on one of the returned addresses; replace the original reach tuple
// with the new one
initialReachTuple = confirmationReachTuple;
}
}
// Save the old MAC address
String savedMacAddress = details.macAddress;
// If we got here, it's reachable
details.update(initialReachTuple.computer);
// If the new MAC address is empty, restore the old one (workaround for GFE bug)
if (details.macAddress.equals("00:00:00:00:00:00") && savedMacAddress != null) {
LimeLog.info("MAC address was empty; using existing value: "+savedMacAddress);
details.macAddress = savedMacAddress;
}
return true;
}
@Override
public void onCreate() {
// Bind to the discovery service
bindService(new Intent(this, DiscoveryService.class),
discoveryServiceConnection, Service.BIND_AUTO_CREATE);
// Lookup or generate this device's UID
idManager = new IdentityManager(this);
// Initialize the DB
dbManager = new ComputerDatabaseManager(this);
dbRefCount.set(1);
// Grab known machines into our computer list
if (!getLocalDatabaseReference()) {
return;
}
for (ComputerDetails computer : dbManager.getAllComputers()) {
// Add tuples for each computer
addTuple(computer);
}
releaseLocalDatabaseReference();
}
@Override
public void onDestroy() {
if (discoveryBinder != null) {
// Unbind from the discovery service
unbindService(discoveryServiceConnection);
}
// FIXME: Should await termination here but we have timeout issues in HttpURLConnection
// Remove the initial DB reference
releaseLocalDatabaseReference();
}
@Override
public IBinder onBind(Intent intent) {
return binder;
}
public class ApplistPoller {
private Thread thread;
private final ComputerDetails computer;
private final Object pollEvent = new Object();
public ApplistPoller(ComputerDetails computer) {
this.computer = computer;
}
public void pollNow() {
synchronized (pollEvent) {
pollEvent.notify();
}
}
private boolean waitPollingDelay() {
try {
synchronized (pollEvent) {
pollEvent.wait(APPLIST_POLLING_PERIOD_MS);
}
} catch (InterruptedException e) {
return false;
}
return thread != null && !thread.isInterrupted();
}
public void start() {
thread = new Thread() {
@Override
public void run() {
do {
InetAddress selectedAddr;
// Can't poll if it's not online
if (computer.state != ComputerDetails.State.ONLINE) {
if (listener != null) {
listener.notifyComputerUpdated(computer);
}
continue;
}
// Can't poll if there's no UUID yet
if (computer.uuid == null) {
continue;
}
if (computer.reachability == ComputerDetails.Reachability.LOCAL) {
selectedAddr = computer.localIp;
}
else {
selectedAddr = computer.remoteIp;
}
NvHTTP http = new NvHTTP(selectedAddr, idManager.getUniqueId(),
null, PlatformBinding.getCryptoProvider(ComputerManagerService.this));
try {
// Query the app list from the server
String appList = http.getAppListRaw();
List<NvApp> list = NvHTTP.getAppListByReader(new StringReader(appList));
if (appList != null && !appList.isEmpty() && !list.isEmpty()) {
// Open the cache file
OutputStream cacheOut = null;
try {
cacheOut = CacheHelper.openCacheFileForOutput(getCacheDir(), "applist", computer.uuid.toString());
CacheHelper.writeStringToOutputStream(cacheOut, appList);
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (cacheOut != null) {
cacheOut.close();
}
} catch (IOException ignored) {}
}
// Update the computer
computer.rawAppList = appList;
// Notify that the app list has been updated
// and ensure that the thread is still active
if (listener != null && thread != null) {
listener.notifyComputerUpdated(computer);
}
}
else {
LimeLog.warning("Empty app list received from "+computer.uuid);
}
} catch (IOException e) {
e.printStackTrace();
} catch (XmlPullParserException e) {
e.printStackTrace();
}
} while (waitPollingDelay());
}
};
thread.start();
}
public void stop() {
if (thread != null) {
thread.interrupt();
// Don't join here because we might be blocked on network I/O
thread = null;
}
}
}
}
class PollingTuple {
public Thread thread;
public final ComputerDetails computer;
public PollingTuple(ComputerDetails computer, Thread thread) {
this.computer = computer;
this.thread = thread;
}
}
class ReachabilityTuple {
public final InetAddress reachableAddress;
public final ComputerDetails computer;
public ReachabilityTuple(ComputerDetails computer, InetAddress reachableAddress) {
this.computer = computer;
this.reachableAddress = reachableAddress;
}
}

View File

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

View File

@@ -0,0 +1,90 @@
package com.limelight.discovery;
import java.util.List;
import com.limelight.nvstream.mdns.MdnsComputer;
import com.limelight.nvstream.mdns.MdnsDiscoveryAgent;
import com.limelight.nvstream.mdns.MdnsDiscoveryListener;
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.IBinder;
public class DiscoveryService extends Service {
private MdnsDiscoveryAgent discoveryAgent;
private MdnsDiscoveryListener boundListener;
private MulticastLock multicastLock;
public class DiscoveryBinder extends Binder {
public void setListener(MdnsDiscoveryListener listener) {
boundListener = listener;
}
public void startDiscovery(int queryIntervalMs) {
multicastLock.acquire();
discoveryAgent.startDiscovery(queryIntervalMs);
}
public void stopDiscovery() {
discoveryAgent.stopDiscovery();
multicastLock.release();
}
public List<MdnsComputer> getComputerSet() {
return discoveryAgent.getComputerSet();
}
}
@Override
public void onCreate() {
WifiManager wifiMgr = (WifiManager) getSystemService(Context.WIFI_SERVICE);
multicastLock = wifiMgr.createMulticastLock("Limelight mDNS");
multicastLock.setReferenceCounted(false);
discoveryAgent = new MdnsDiscoveryAgent(new MdnsDiscoveryListener() {
@Override
public void notifyComputerAdded(MdnsComputer computer) {
if (boundListener != null) {
boundListener.notifyComputerAdded(computer);
}
}
@Override
public void notifyComputerRemoved(MdnsComputer computer) {
if (boundListener != null) {
boundListener.notifyComputerRemoved(computer);
}
}
@Override
public void notifyDiscoveryFailure(Exception e) {
if (boundListener != null) {
boundListener.notifyDiscoveryFailure(e);
}
}
});
}
private final DiscoveryBinder binder = new DiscoveryBinder();
@Override
public IBinder onBind(Intent intent) {
return binder;
}
@Override
public boolean onUnbind(Intent intent) {
// Stop any discovery session
discoveryAgent.stopDiscovery();
multicastLock.release();
// Unbind the listener
boundListener = null;
return false;
}
}

View File

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

View File

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

View File

@@ -0,0 +1,75 @@
package com.limelight.grid;
import android.content.Context;
import android.widget.ImageView;
import android.widget.TextView;
import com.limelight.PcView;
import com.limelight.R;
import com.limelight.nvstream.http.ComputerDetails;
import java.util.Collections;
import java.util.Comparator;
public class PcGridAdapter extends GenericGridAdapter<PcView.ComputerObject> {
public PcGridAdapter(Context context, boolean listMode, boolean small) {
super(context, listMode ? R.layout.simple_row : (small ? R.layout.pc_grid_item_small : R.layout.pc_grid_item), R.drawable.computer);
}
public void addComputer(PcView.ComputerObject computer) {
itemList.add(computer);
sortList();
}
private void sortList() {
Collections.sort(itemList, new Comparator<PcView.ComputerObject>() {
@Override
public int compare(PcView.ComputerObject lhs, PcView.ComputerObject rhs) {
return lhs.details.name.compareTo(rhs.details.name);
}
});
}
public boolean removeComputer(PcView.ComputerObject computer) {
return itemList.remove(computer);
}
@Override
public boolean populateImageView(ImageView imgView, PcView.ComputerObject obj) {
if (obj.details.reachability != ComputerDetails.Reachability.OFFLINE) {
imgView.setAlpha(1.0f);
}
else {
imgView.setAlpha(0.4f);
}
// Return false to use the default drawable
return false;
}
@Override
public boolean populateTextView(TextView txtView, PcView.ComputerObject obj) {
if (obj.details.reachability != ComputerDetails.Reachability.OFFLINE) {
txtView.setAlpha(1.0f);
}
else {
txtView.setAlpha(0.4f);
}
// Return false to use the computer's toString method
return false;
}
@Override
public boolean populateOverlayView(ImageView overlayView, PcView.ComputerObject obj) {
if (obj.details.reachability == ComputerDetails.Reachability.UNKNOWN) {
// Still refreshing this PC so display the overlay
overlayView.setImageResource(R.drawable.image_loading);
return true;
}
// No overlay
return false;
}
}

View File

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

View File

@@ -0,0 +1,83 @@
package com.limelight.grid.assets;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import com.limelight.LimeLog;
import com.limelight.utils.CacheHelper;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
public class DiskAssetLoader {
// 5 MB
private final long MAX_ASSET_SIZE = 5 * 1024 * 1024;
private final File cacheDir;
public DiskAssetLoader(File cacheDir) {
this.cacheDir = cacheDir;
}
public boolean checkCacheExists(CachedAppAssetLoader.LoaderTuple tuple) {
return CacheHelper.cacheFileExists(cacheDir, "boxart", tuple.computer.uuid.toString(), tuple.app.getAppId() + ".png");
}
public Bitmap loadBitmapFromCache(CachedAppAssetLoader.LoaderTuple tuple, int sampleSize) {
InputStream in = null;
Bitmap bmp = null;
try {
// Make sure the cached asset doesn't exceed the maximum size
if (CacheHelper.getFileSize(cacheDir, "boxart", tuple.computer.uuid.toString(), tuple.app.getAppId() + ".png") > MAX_ASSET_SIZE) {
LimeLog.warning("Removing cached tuple exceeding size threshold: "+tuple);
CacheHelper.deleteCacheFile(cacheDir, "boxart", tuple.computer.uuid.toString(), tuple.app.getAppId() + ".png");
return null;
}
in = CacheHelper.openCacheFileForInput(cacheDir, "boxart", tuple.computer.uuid.toString(), tuple.app.getAppId() + ".png");
BitmapFactory.Options options = new BitmapFactory.Options();
options.inSampleSize = sampleSize;
options.inPreferredConfig = Bitmap.Config.RGB_565;
bmp = BitmapFactory.decodeStream(in, null, options);
} catch (IOException ignored) {
} finally {
if (in != null) {
try {
in.close();
} catch (IOException ignored) {}
}
}
if (bmp != null) {
LimeLog.info("Disk cache hit for tuple: "+tuple);
}
return bmp;
}
public void populateCacheWithStream(CachedAppAssetLoader.LoaderTuple tuple, InputStream input) {
OutputStream out = null;
boolean success = false;
try {
out = CacheHelper.openCacheFileForOutput(cacheDir, "boxart", tuple.computer.uuid.toString(), tuple.app.getAppId() + ".png");
CacheHelper.writeInputStreamToOutputStream(input, out, MAX_ASSET_SIZE);
success = true;
} catch (IOException e) {
e.printStackTrace();
} finally {
if (out != null) {
try {
out.close();
} catch (IOException ignored) {}
}
if (!success) {
LimeLog.warning("Unable to populate cache with tuple: "+tuple);
CacheHelper.deleteCacheFile(cacheDir, "boxart", tuple.computer.uuid.toString(), tuple.app.getAppId() + ".png");
}
}
}
}

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,181 @@
package com.limelight.preferences;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.Locale;
import java.util.concurrent.LinkedBlockingQueue;
import com.limelight.computers.ComputerManagerService;
import com.limelight.R;
import com.limelight.utils.Dialog;
import com.limelight.utils.SpinnerDialog;
import com.limelight.utils.UiHelper;
import android.app.Activity;
import android.app.Service;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.content.res.Configuration;
import android.os.Bundle;
import android.os.IBinder;
import android.view.KeyEvent;
import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputMethodManager;
import android.widget.TextView;
import android.widget.Toast;
public class AddComputerManually extends Activity {
private TextView hostText;
private ComputerManagerService.ComputerManagerBinder managerBinder;
private final LinkedBlockingQueue<String> computersToAdd = new LinkedBlockingQueue<String>();
private Thread addThread;
private final ServiceConnection serviceConnection = new ServiceConnection() {
public void onServiceConnected(ComponentName className, final IBinder binder) {
managerBinder = ((ComputerManagerService.ComputerManagerBinder)binder);
startAddThread();
}
public void onServiceDisconnected(ComponentName className) {
joinAddThread();
managerBinder = null;
}
};
private void doAddPc(String host) {
String msg;
boolean finish = false;
SpinnerDialog dialog = SpinnerDialog.displayDialog(this, getResources().getString(R.string.title_add_pc),
getResources().getString(R.string.msg_add_pc), false);
try {
InetAddress addr = InetAddress.getByName(host);
if (!managerBinder.addComputerBlocking(addr)){
msg = getResources().getString(R.string.addpc_fail);
}
else {
msg = getResources().getString(R.string.addpc_success);
finish = true;
}
} catch (UnknownHostException e) {
msg = getResources().getString(R.string.addpc_unknown_host);
}
dialog.dismiss();
final boolean toastFinish = finish;
final String toastMsg = msg;
AddComputerManually.this.runOnUiThread(new Runnable() {
@Override
public void run() {
Toast.makeText(AddComputerManually.this, toastMsg, Toast.LENGTH_LONG).show();
if (toastFinish && !isFinishing()) {
// Close the activity
AddComputerManually.this.finish();
}
}
});
}
private void startAddThread() {
addThread = new Thread() {
@Override
public void run() {
while (!isInterrupted()) {
String computer;
try {
computer = computersToAdd.take();
} catch (InterruptedException e) {
return;
}
doAddPc(computer);
}
}
};
addThread.setName("UI - AddComputerManually");
addThread.start();
}
private void joinAddThread() {
if (addThread != null) {
addThread.interrupt();
try {
addThread.join();
} catch (InterruptedException ignored) {}
addThread = null;
}
}
@Override
protected void onStop() {
super.onStop();
Dialog.closeDialogs();
SpinnerDialog.closeDialogs(this);
}
@Override
protected void onDestroy() {
super.onDestroy();
if (managerBinder != null) {
joinAddThread();
unbindService(serviceConnection);
}
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
String locale = PreferenceConfiguration.readPreferences(this).language;
if (!locale.equals(PreferenceConfiguration.DEFAULT_LANGUAGE)) {
Configuration config = new Configuration(getResources().getConfiguration());
config.locale = new Locale(locale);
getResources().updateConfiguration(config, getResources().getDisplayMetrics());
}
setContentView(R.layout.activity_add_computer_manually);
UiHelper.notifyNewRootView(this);
this.hostText = (TextView) findViewById(R.id.hostTextView);
hostText.setImeOptions(EditorInfo.IME_ACTION_DONE);
hostText.setOnEditorActionListener(new TextView.OnEditorActionListener() {
@Override
public boolean onEditorAction(TextView textView, int actionId, KeyEvent keyEvent) {
if (actionId == EditorInfo.IME_ACTION_DONE ||
(keyEvent != null &&
keyEvent.getAction() == KeyEvent.ACTION_DOWN &&
keyEvent.getKeyCode() == KeyEvent.KEYCODE_ENTER)) {
if (hostText.getText().length() == 0) {
Toast.makeText(AddComputerManually.this, getResources().getString(R.string.addpc_enter_ip), Toast.LENGTH_LONG).show();
return true;
}
computersToAdd.add(hostText.getText().toString().trim());
}
else if (actionId == EditorInfo.IME_ACTION_PREVIOUS) {
// This is how the Fire TV dismisses the keyboard
InputMethodManager imm = (InputMethodManager)getSystemService(Context.INPUT_METHOD_SERVICE);
imm.hideSoftInputFromWindow(hostText.getWindowToken(), 0);
return false;
}
return false;
}
});
// Bind to the ComputerManager service
bindService(new Intent(AddComputerManually.this,
ComputerManagerService.class), serviceConnection, Service.BIND_AUTO_CREATE);
}
}

View File

@@ -0,0 +1,182 @@
package com.limelight.preferences;
import android.content.Context;
import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.os.Build;
import android.preference.PreferenceManager;
public class PreferenceConfiguration {
static final String RES_FPS_PREF_STRING = "list_resolution_fps";
private static final String DECODER_PREF_STRING = "list_decoders";
static final String BITRATE_PREF_STRING = "seekbar_bitrate";
private static final String STRETCH_PREF_STRING = "checkbox_stretch_video";
private static final String SOPS_PREF_STRING = "checkbox_enable_sops";
private static final String DISABLE_TOASTS_PREF_STRING = "checkbox_disable_warnings";
private static final String HOST_AUDIO_PREF_STRING = "checkbox_host_audio";
private static final String DEADZONE_PREF_STRING = "seekbar_deadzone";
private static final String LANGUAGE_PREF_STRING = "list_languages";
private static final String LIST_MODE_PREF_STRING = "checkbox_list_mode";
private static final String SMALL_ICONS_PREF_STRING = "checkbox_small_icon_mode";
private static final String MULTI_CONTROLLER_PREF_STRING = "checkbox_multi_controller";
private static final int BITRATE_DEFAULT_720_30 = 5;
private static final int BITRATE_DEFAULT_720_60 = 10;
private static final int BITRATE_DEFAULT_1080_30 = 10;
private static final int BITRATE_DEFAULT_1080_60 = 20;
private static final String DEFAULT_RES_FPS = "720p60";
private static final String DEFAULT_DECODER = "auto";
private static final int DEFAULT_BITRATE = BITRATE_DEFAULT_720_60;
private static final boolean DEFAULT_STRETCH = false;
private static final boolean DEFAULT_SOPS = true;
private static final boolean DEFAULT_DISABLE_TOASTS = false;
private static final boolean DEFAULT_HOST_AUDIO = false;
private static final int DEFAULT_DEADZONE = 15;
public static final String DEFAULT_LANGUAGE = "default";
private static final boolean DEFAULT_LIST_MODE = false;
private static final boolean DEFAULT_MULTI_CONTROLLER = true;
public static final int FORCE_HARDWARE_DECODER = -1;
public static final int AUTOSELECT_DECODER = 0;
public static final int FORCE_SOFTWARE_DECODER = 1;
public int width, height, fps;
public int bitrate;
public int decoder;
public int deadzonePercentage;
public boolean stretchVideo, enableSops, playHostAudio, disableWarnings;
public String language;
public boolean listMode, smallIconMode, multiController;
public static int getDefaultBitrate(String resFpsString) {
if (resFpsString.equals("720p30")) {
return BITRATE_DEFAULT_720_30;
}
else if (resFpsString.equals("720p60")) {
return BITRATE_DEFAULT_720_60;
}
else if (resFpsString.equals("1080p30")) {
return BITRATE_DEFAULT_1080_30;
}
else if (resFpsString.equals("1080p60")) {
return BITRATE_DEFAULT_1080_60;
}
else {
// Should never get here
return DEFAULT_BITRATE;
}
}
public static boolean getDefaultSmallMode(Context context) {
PackageManager manager = context.getPackageManager();
if (manager != null) {
// TVs shouldn't use small mode by default
if (manager.hasSystemFeature(PackageManager.FEATURE_TELEVISION)) {
return false;
}
// API 21 uses LEANBACK instead of TELEVISION
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP_MR1) {
if (manager.hasSystemFeature(PackageManager.FEATURE_LEANBACK)) {
return false;
}
}
}
// Use small mode on anything smaller than a 7" tablet
return context.getResources().getConfiguration().smallestScreenWidthDp < 600;
}
public static int getDefaultBitrate(Context context) {
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
String str = prefs.getString(RES_FPS_PREF_STRING, DEFAULT_RES_FPS);
if (str.equals("720p30")) {
return BITRATE_DEFAULT_720_30;
}
else if (str.equals("720p60")) {
return BITRATE_DEFAULT_720_60;
}
else if (str.equals("1080p30")) {
return BITRATE_DEFAULT_1080_30;
}
else if (str.equals("1080p60")) {
return BITRATE_DEFAULT_1080_60;
}
else {
// Should never get here
return DEFAULT_BITRATE;
}
}
private static int getDecoderValue(Context context) {
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
String str = prefs.getString(DECODER_PREF_STRING, DEFAULT_DECODER);
if (str.equals("auto")) {
return AUTOSELECT_DECODER;
}
else if (str.equals("software")) {
return FORCE_SOFTWARE_DECODER;
}
else if (str.equals("hardware")) {
return FORCE_HARDWARE_DECODER;
}
else {
// Should never get here
return AUTOSELECT_DECODER;
}
}
public static PreferenceConfiguration readPreferences(Context context) {
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
PreferenceConfiguration config = new PreferenceConfiguration();
config.bitrate = prefs.getInt(BITRATE_PREF_STRING, getDefaultBitrate(context));
String str = prefs.getString(RES_FPS_PREF_STRING, DEFAULT_RES_FPS);
if (str.equals("720p30")) {
config.width = 1280;
config.height = 720;
config.fps = 30;
}
else if (str.equals("720p60")) {
config.width = 1280;
config.height = 720;
config.fps = 60;
}
else if (str.equals("1080p30")) {
config.width = 1920;
config.height = 1080;
config.fps = 30;
}
else if (str.equals("1080p60")) {
config.width = 1920;
config.height = 1080;
config.fps = 60;
}
else {
// Should never get here
config.width = 1280;
config.height = 720;
config.fps = 60;
}
config.decoder = getDecoderValue(context);
config.deadzonePercentage = prefs.getInt(DEADZONE_PREF_STRING, DEFAULT_DEADZONE);
config.language = prefs.getString(LANGUAGE_PREF_STRING, DEFAULT_LANGUAGE);
// Checkbox preferences
config.disableWarnings = prefs.getBoolean(DISABLE_TOASTS_PREF_STRING, DEFAULT_DISABLE_TOASTS);
config.enableSops = prefs.getBoolean(SOPS_PREF_STRING, DEFAULT_SOPS);
config.stretchVideo = prefs.getBoolean(STRETCH_PREF_STRING, DEFAULT_STRETCH);
config.playHostAudio = prefs.getBoolean(HOST_AUDIO_PREF_STRING, DEFAULT_HOST_AUDIO);
config.listMode = prefs.getBoolean(LIST_MODE_PREF_STRING, DEFAULT_LIST_MODE);
config.smallIconMode = prefs.getBoolean(SMALL_ICONS_PREF_STRING, getDefaultSmallMode(context));
config.multiController = prefs.getBoolean(MULTI_CONTROLLER_PREF_STRING, DEFAULT_MULTI_CONTROLLER);
return config;
}
}

View File

@@ -0,0 +1,162 @@
package com.limelight.preferences;
import android.app.AlertDialog;
import android.content.Context;
import android.os.Bundle;
import android.preference.DialogPreference;
import android.util.AttributeSet;
import android.view.Gravity;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
import android.widget.LinearLayout;
import android.widget.SeekBar;
import android.widget.TextView;
// Based on a Stack Overflow example: http://stackoverflow.com/questions/1974193/slider-on-my-preferencescreen
public class SeekBarPreference extends DialogPreference
{
private static final String SCHEMA_URL = "http://schemas.android.com/apk/res/android";
private SeekBar seekBar;
private TextView valueText;
private final Context context;
private final String dialogMessage;
private final String suffix;
private final int defaultValue;
private final int maxValue;
private final int minValue;
private int currentValue;
public SeekBarPreference(Context context, AttributeSet attrs) {
super(context, attrs);
this.context = context;
// Read the message from XML
int dialogMessageId = attrs.getAttributeResourceValue(SCHEMA_URL, "dialogMessage", 0);
if (dialogMessageId == 0) {
dialogMessage = attrs.getAttributeValue(SCHEMA_URL, "dialogMessage");
}
else {
dialogMessage = context.getString(dialogMessageId);
}
// Get the suffix for the number displayed in the dialog
int suffixId = attrs.getAttributeResourceValue(SCHEMA_URL, "text", 0);
if (suffixId == 0) {
suffix = attrs.getAttributeValue(SCHEMA_URL, "text");
}
else {
suffix = context.getString(suffixId);
}
// Get default, min, and max seekbar values
defaultValue = attrs.getAttributeIntValue(SCHEMA_URL, "defaultValue", PreferenceConfiguration.getDefaultBitrate(context));
maxValue = attrs.getAttributeIntValue(SCHEMA_URL, "max", 100);
minValue = 1;
}
@Override
protected View onCreateDialogView() {
LinearLayout.LayoutParams params;
LinearLayout layout = new LinearLayout(context);
layout.setOrientation(LinearLayout.VERTICAL);
layout.setPadding(6, 6, 6, 6);
TextView splashText = new TextView(context);
splashText.setPadding(30, 10, 30, 10);
if (dialogMessage != null) {
splashText.setText(dialogMessage);
}
layout.addView(splashText);
valueText = new TextView(context);
valueText.setGravity(Gravity.CENTER_HORIZONTAL);
valueText.setTextSize(32);
params = new LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.WRAP_CONTENT);
layout.addView(valueText, params);
seekBar = new SeekBar(context);
seekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
@Override
public void onProgressChanged(SeekBar seekBar, int value, boolean b) {
if (value < minValue) {
seekBar.setProgress(minValue);
return;
}
String t = String.valueOf(value);
valueText.setText(suffix == null ? t : t.concat(suffix.length() > 1 ? " "+suffix : suffix));
}
@Override
public void onStartTrackingTouch(SeekBar seekBar) {}
@Override
public void onStopTrackingTouch(SeekBar seekBar) {}
});
layout.addView(seekBar, new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT));
if (shouldPersist()) {
currentValue = getPersistedInt(defaultValue);
}
seekBar.setMax(maxValue);
seekBar.setProgress(currentValue);
return layout;
}
@Override
protected void onBindDialogView(View v) {
super.onBindDialogView(v);
seekBar.setMax(maxValue);
seekBar.setProgress(currentValue);
}
@Override
protected void onSetInitialValue(boolean restore, Object defaultValue)
{
super.onSetInitialValue(restore, defaultValue);
if (restore) {
currentValue = shouldPersist() ? getPersistedInt(this.defaultValue) : 0;
}
else {
currentValue = (Integer) defaultValue;
}
}
public void setProgress(int progress) {
this.currentValue = progress;
if (seekBar != null) {
seekBar.setProgress(progress);
}
}
public int getProgress() {
return currentValue;
}
@Override
public void showDialog(Bundle state) {
super.showDialog(state);
Button positiveButton = ((AlertDialog) getDialog()).getButton(AlertDialog.BUTTON_POSITIVE);
positiveButton.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View view) {
if (shouldPersist()) {
currentValue = seekBar.getProgress();
persistInt(seekBar.getProgress());
callChangeListener(seekBar.getProgress());
}
getDialog().dismiss();
}
});
}
}

View File

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

View File

@@ -0,0 +1,85 @@
package com.limelight.preferences;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.res.Configuration;
import android.os.Bundle;
import android.app.Activity;
import android.preference.Preference;
import android.preference.PreferenceFragment;
import android.preference.PreferenceManager;
import com.limelight.PcView;
import com.limelight.R;
import com.limelight.utils.UiHelper;
import java.util.Locale;
public class StreamSettings extends Activity {
private PreferenceConfiguration previousPrefs;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
previousPrefs = PreferenceConfiguration.readPreferences(this);
if (!previousPrefs.language.equals(PreferenceConfiguration.DEFAULT_LANGUAGE)) {
Configuration config = new Configuration(getResources().getConfiguration());
config.locale = new Locale(previousPrefs.language);
getResources().updateConfiguration(config, getResources().getDisplayMetrics());
}
setContentView(R.layout.activity_stream_settings);
getFragmentManager().beginTransaction().replace(
R.id.stream_settings, new SettingsFragment()
).commit();
UiHelper.notifyNewRootView(this);
}
@Override
public void onBackPressed() {
finish();
// Check for changes that require a UI reload to take effect
PreferenceConfiguration newPrefs = PreferenceConfiguration.readPreferences(this);
if (newPrefs.listMode != previousPrefs.listMode ||
newPrefs.smallIconMode != previousPrefs.smallIconMode ||
!newPrefs.language.equals(previousPrefs.language)) {
// Restart the PC view to apply UI changes
Intent intent = new Intent(this, PcView.class);
intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK | Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(intent, null);
}
}
public static class SettingsFragment extends PreferenceFragment {
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
addPreferencesFromResource(R.xml.preferences);
// Add a listener to the FPS and resolution preference
// so the bitrate can be auto-adjusted
Preference pref = findPreference(PreferenceConfiguration.RES_FPS_PREF_STRING);
pref.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
@Override
public boolean onPreferenceChange(Preference preference, Object newValue) {
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(SettingsFragment.this.getActivity());
String valueStr = (String) newValue;
// Write the new bitrate value
prefs.edit()
.putInt(PreferenceConfiguration.BITRATE_PREF_STRING,
PreferenceConfiguration.getDefaultBitrate(valueStr))
.apply();
// Allow the original preference change to take place
return true;
}
});
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,77 @@
package com.limelight.utils;
import java.util.ArrayList;
import android.app.Activity;
import android.app.AlertDialog;
import android.content.DialogInterface;
public class Dialog implements Runnable {
private final String title;
private final String message;
private final Activity activity;
private final boolean endAfterDismiss;
private AlertDialog alert;
private static final ArrayList<Dialog> rundownDialogs = new ArrayList<Dialog>();
private Dialog(Activity activity, String title, String message, boolean endAfterDismiss)
{
this.activity = activity;
this.title = title;
this.message = message;
this.endAfterDismiss = endAfterDismiss;
}
public static void closeDialogs()
{
synchronized (rundownDialogs) {
for (Dialog d : rundownDialogs) {
if (d.alert.isShowing()) {
d.alert.dismiss();
}
}
rundownDialogs.clear();
}
}
public static void displayDialog(Activity activity, String title, String message, boolean endAfterDismiss)
{
activity.runOnUiThread(new Dialog(activity, title, message, endAfterDismiss));
}
@Override
public void run() {
// If we're dying, don't bother creating a dialog
if (activity.isFinishing())
return;
alert = new AlertDialog.Builder(activity).create();
alert.setTitle(title);
alert.setMessage(message);
alert.setCancelable(false);
alert.setCanceledOnTouchOutside(false);
alert.setButton(AlertDialog.BUTTON_NEUTRAL, "OK", new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int which) {
synchronized (rundownDialogs) {
rundownDialogs.remove(Dialog.this);
alert.dismiss();
}
if (endAfterDismiss) {
activity.finish();
}
}
});
synchronized (rundownDialogs) {
rundownDialogs.add(this);
alert.show();
}
}
}

View File

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

View File

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

View File

@@ -0,0 +1,62 @@
package com.limelight.utils;
import android.app.Activity;
import android.app.AlertDialog;
import android.app.UiModeManager;
import android.content.Context;
import android.content.DialogInterface;
import android.content.res.Configuration;
import android.view.View;
import com.limelight.R;
public class UiHelper {
// Values from https://developer.android.com/training/tv/start/layouts.html
private static final int TV_VERTICAL_PADDING_DP = 27;
private static final int TV_HORIZONTAL_PADDING_DP = 48;
public static void notifyNewRootView(Activity activity)
{
View rootView = activity.findViewById(android.R.id.content);
UiModeManager modeMgr = (UiModeManager) activity.getSystemService(Context.UI_MODE_SERVICE);
if (modeMgr.getCurrentModeType() == Configuration.UI_MODE_TYPE_TELEVISION)
{
// Increase view padding on TVs
float scale = activity.getResources().getDisplayMetrics().density;
int verticalPaddingPixels = (int) (TV_VERTICAL_PADDING_DP*scale + 0.5f);
int horizontalPaddingPixels = (int) (TV_HORIZONTAL_PADDING_DP*scale + 0.5f);
rootView.setPadding(horizontalPaddingPixels, verticalPaddingPixels,
horizontalPaddingPixels, verticalPaddingPixels);
}
}
public static void displayQuitConfirmationDialog(Activity parent, final Runnable onYes, final Runnable onNo) {
DialogInterface.OnClickListener dialogClickListener = new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
switch (which){
case DialogInterface.BUTTON_POSITIVE:
if (onYes != null) {
onYes.run();
}
break;
case DialogInterface.BUTTON_NEGATIVE:
if (onNo != null) {
onNo.run();
}
break;
}
}
};
AlertDialog.Builder builder = new AlertDialog.Builder(parent);
builder.setMessage(parent.getResources().getString(R.string.applist_quit_confirmation))
.setPositiveButton(parent.getResources().getString(R.string.yes), dialogClickListener)
.setNegativeButton(parent.getResources().getString(R.string.no), dialogClickListener)
.show();
}
}

View File

@@ -8,5 +8,6 @@ LOCAL_PATH := $(MY_LOCAL_PATH)
include $(CLEAR_VARS)
LOCAL_MODULE := evdev_reader
LOCAL_SRC_FILES := evdev_reader.c
LOCAL_LDLIBS := -llog
include $(BUILD_SHARED_LIBRARY)

View File

@@ -0,0 +1,118 @@
#include <stdlib.h>
#include <jni.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <linux/input.h>
#include <unistd.h>
#include <poll.h>
#include <errno.h>
#include <android/log.h>
JNIEXPORT jint JNICALL
Java_com_limelight_binding_input_evdev_EvdevReader_open(JNIEnv *env, jobject this, jstring absolutePath) {
const char *path;
path = (*env)->GetStringUTFChars(env, absolutePath, NULL);
return open(path, O_RDWR);
}
JNIEXPORT jboolean JNICALL
Java_com_limelight_binding_input_evdev_EvdevReader_grab(JNIEnv *env, jobject this, jint fd) {
return ioctl(fd, EVIOCGRAB, 1) == 0;
}
JNIEXPORT jboolean JNICALL
Java_com_limelight_binding_input_evdev_EvdevReader_ungrab(JNIEnv *env, jobject this, jint fd) {
return ioctl(fd, EVIOCGRAB, 0) == 0;
}
// has*() and friends are based on Android's EventHub.cpp
#define test_bit(bit, array) (array[bit/8] & (1<<(bit%8)))
JNIEXPORT jboolean JNICALL
Java_com_limelight_binding_input_evdev_EvdevReader_hasRelAxis(JNIEnv *env, jobject this, jint fd, jshort axis) {
unsigned char relBitmask[(REL_MAX + 1) / 8];
ioctl(fd, EVIOCGBIT(EV_REL, sizeof(relBitmask)), relBitmask);
return test_bit(axis, relBitmask);
}
JNIEXPORT jboolean JNICALL
Java_com_limelight_binding_input_evdev_EvdevReader_hasAbsAxis(JNIEnv *env, jobject this, jint fd, jshort axis) {
unsigned char absBitmask[(ABS_MAX + 1) / 8];
ioctl(fd, EVIOCGBIT(EV_ABS, sizeof(absBitmask)), absBitmask);
return test_bit(axis, absBitmask);
}
JNIEXPORT jboolean JNICALL
Java_com_limelight_binding_input_evdev_EvdevReader_hasKey(JNIEnv *env, jobject this, jint fd, jshort key) {
unsigned char keyBitmask[(KEY_MAX + 1) / 8];
ioctl(fd, EVIOCGBIT(EV_KEY, sizeof(keyBitmask)), keyBitmask);
return test_bit(key, keyBitmask);
}
JNIEXPORT jint JNICALL
Java_com_limelight_binding_input_evdev_EvdevReader_read(JNIEnv *env, jobject this, jint fd, jbyteArray buffer) {
jint ret;
jbyte *data;
int pollres;
struct pollfd pollinfo;
data = (*env)->GetByteArrayElements(env, buffer, NULL);
if (data == NULL) {
__android_log_print(ANDROID_LOG_ERROR, "EvdevReader",
"Failed to get byte array");
return -1;
}
do
{
// Unwait every 250 ms to return to caller if the fd is closed
pollinfo.fd = fd;
pollinfo.events = POLLIN;
pollinfo.revents = 0;
pollres = poll(&pollinfo, 1, 250);
}
while (pollres == 0);
if (pollres > 0 && (pollinfo.revents & POLLIN)) {
// We'll have data available now
ret = read(fd, data, sizeof(struct input_event));
if (ret < 0) {
__android_log_print(ANDROID_LOG_ERROR, "EvdevReader",
"read() failed: %d", errno);
}
}
else {
// There must have been a failure
ret = -1;
if (pollres < 0) {
__android_log_print(ANDROID_LOG_ERROR, "EvdevReader",
"poll() failed: %d", errno);
}
else {
__android_log_print(ANDROID_LOG_ERROR, "EvdevReader",
"Unexpected revents: %d", pollinfo.revents);
}
}
(*env)->ReleaseByteArrayElements(env, buffer, data, 0);
return ret;
}
JNIEXPORT jint JNICALL
Java_com_limelight_binding_input_evdev_EvdevReader_close(JNIEnv *env, jobject this, jint fd) {
return close(fd);
}

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