Compare commits

...

225 Commits

Author SHA1 Message Date
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
88 changed files with 7629 additions and 5041 deletions
+2
View File
@@ -8,6 +8,8 @@ whether in your own home or over the internet.
[Limelight-pc](https://github.com/limelight-stream/limelight-pc) is also currently in development for Windows, OS X and Linux. Versions for [iOS](https://github.com/limelight-stream/limelight-ios) and [Windows and Windows Phone](https://github.com/limelight-stream/limelight-windows) are also in development. [Limelight-pc](https://github.com/limelight-stream/limelight-pc) is also currently in development for Windows, OS X and Linux. Versions for [iOS](https://github.com/limelight-stream/limelight-ios) and [Windows and Windows Phone](https://github.com/limelight-stream/limelight-windows) are also in development.
Check our [wiki](https://github.com/limelight-stream/limelight-android/wiki) for more detailed information or a troubleshooting guide.
##Features ##Features
* Streams any of your games from your PC to your Android device * Streams any of your games from your PC to your Android device
+14 -8
View File
@@ -9,11 +9,12 @@
<facet type="android" name="Android"> <facet type="android" name="Android">
<configuration> <configuration>
<option name="SELECTED_BUILD_VARIANT" value="nonRootDebug" /> <option name="SELECTED_BUILD_VARIANT" value="nonRootDebug" />
<option name="SELECTED_TEST_ARTIFACT" value="_android_test_" />
<option name="ASSEMBLE_TASK_NAME" value="assembleNonRootDebug" /> <option name="ASSEMBLE_TASK_NAME" value="assembleNonRootDebug" />
<option name="COMPILE_JAVA_TASK_NAME" value="compileNonRootDebugSources" /> <option name="COMPILE_JAVA_TASK_NAME" value="compileNonRootDebugSources" />
<option name="ASSEMBLE_TEST_TASK_NAME" value="assembleNonRootDebugTest" /> <option name="ASSEMBLE_TEST_TASK_NAME" value="assembleNonRootDebugAndroidTest" />
<option name="SOURCE_GEN_TASK_NAME" value="generateNonRootDebugSources" /> <option name="SOURCE_GEN_TASK_NAME" value="generateNonRootDebugSources" />
<option name="TEST_SOURCE_GEN_TASK_NAME" value="generateNonRootDebugTestSources" /> <option name="TEST_SOURCE_GEN_TASK_NAME" value="generateNonRootDebugAndroidTestSources" />
<option name="ALLOW_USER_CONFIGURATION" value="false" /> <option name="ALLOW_USER_CONFIGURATION" value="false" />
<option name="MANIFEST_FILE_RELATIVE_PATH" value="/src/main/AndroidManifest.xml" /> <option name="MANIFEST_FILE_RELATIVE_PATH" value="/src/main/AndroidManifest.xml" />
<option name="RES_FOLDER_RELATIVE_PATH" value="/src/main/res" /> <option name="RES_FOLDER_RELATIVE_PATH" value="/src/main/res" />
@@ -24,6 +25,7 @@
</component> </component>
<component name="NewModuleRootManager" inherit-compiler-output="false"> <component name="NewModuleRootManager" inherit-compiler-output="false">
<output url="file://$MODULE_DIR$/build/intermediates/classes/nonRoot/debug" /> <output url="file://$MODULE_DIR$/build/intermediates/classes/nonRoot/debug" />
<output-test url="file://$MODULE_DIR$/build/intermediates/classes/androidTest/nonRoot/debug" />
<exclude-output /> <exclude-output />
<content url="file://$MODULE_DIR$"> <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/r/nonRoot/debug" isTestSource="false" generated="true" />
@@ -31,6 +33,7 @@
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/buildConfig/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/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/rs/nonRoot/debug" type="java-resource" />
<sourceFolder url="file://$MODULE_DIR$/build/generated/res/generated/nonRoot/debug" type="java-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/nonRootDebug/res" type="java-resource" /> <sourceFolder url="file://$MODULE_DIR$/src/nonRootDebug/res" type="java-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/nonRootDebug/resources" 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/assets" type="java-resource" />
@@ -38,11 +41,12 @@
<sourceFolder url="file://$MODULE_DIR$/src/nonRootDebug/java" 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/jni" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/src/nonRootDebug/rs" isTestSource="false" /> <sourceFolder url="file://$MODULE_DIR$/src/nonRootDebug/rs" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/r/test/nonRoot/debug" isTestSource="true" generated="true" /> <sourceFolder url="file://$MODULE_DIR$/build/generated/source/r/androidTest/nonRoot/debug" isTestSource="true" generated="true" />
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/aidl/test/nonRoot/debug" isTestSource="true" generated="true" /> <sourceFolder url="file://$MODULE_DIR$/build/generated/source/aidl/androidTest/nonRoot/debug" isTestSource="true" generated="true" />
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/buildConfig/test/nonRoot/debug" isTestSource="true" generated="true" /> <sourceFolder url="file://$MODULE_DIR$/build/generated/source/buildConfig/androidTest/nonRoot/debug" isTestSource="true" generated="true" />
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/rs/test/nonRoot/debug" isTestSource="true" generated="true" /> <sourceFolder url="file://$MODULE_DIR$/build/generated/source/rs/androidTest/nonRoot/debug" isTestSource="true" generated="true" />
<sourceFolder url="file://$MODULE_DIR$/build/generated/res/rs/test/nonRoot/debug" type="java-test-resource" /> <sourceFolder url="file://$MODULE_DIR$/build/generated/res/rs/androidTest/nonRoot/debug" type="java-test-resource" />
<sourceFolder url="file://$MODULE_DIR$/build/generated/res/generated/androidTest/nonRoot/debug" type="java-test-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/nonRoot/res" type="java-resource" /> <sourceFolder url="file://$MODULE_DIR$/src/nonRoot/res" type="java-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/nonRoot/resources" 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/assets" type="java-resource" />
@@ -103,10 +107,12 @@
<orderEntry type="sourceFolder" forTests="false" /> <orderEntry type="sourceFolder" forTests="false" />
<orderEntry type="library" exported="" name="bcprov-jdk15on-1.51" level="project" /> <orderEntry type="library" exported="" name="bcprov-jdk15on-1.51" level="project" />
<orderEntry type="library" exported="" name="jmdns-fixed" level="project" /> <orderEntry type="library" exported="" name="jmdns-fixed" level="project" />
<orderEntry type="library" exported="" name="jcodec-0.1.6-3" level="project" />
<orderEntry type="library" exported="" name="bcpkix-jdk15on-1.51" level="project" /> <orderEntry type="library" exported="" name="bcpkix-jdk15on-1.51" level="project" />
<orderEntry type="library" exported="" name="tinyrtsp" level="project" /> <orderEntry type="library" exported="" name="tinyrtsp" level="project" />
<orderEntry type="library" exported="" name="limelight-common" level="project" /> <orderEntry type="library" exported="" name="limelight-common" level="project" />
<orderEntry type="library" exported="" name="okhttp-2.2.0" level="project" />
<orderEntry type="library" exported="" name="jcodec-0.1.9" level="project" />
<orderEntry type="library" exported="" name="okio-1.2.0" level="project" />
</component> </component>
</module> </module>
+10 -5
View File
@@ -5,14 +5,14 @@ apply plugin: 'com.android.application'
android { android {
compileSdkVersion 21 compileSdkVersion 21
buildToolsVersion "21.0.2" buildToolsVersion "21.1.2"
defaultConfig { defaultConfig {
minSdkVersion 16 minSdkVersion 16
targetSdkVersion 21 targetSdkVersion 21
versionName "2.9" versionName "3.1.2"
versionCode = 38 versionCode = 56
} }
productFlavors { productFlavors {
@@ -27,7 +27,7 @@ android {
buildTypes { buildTypes {
release { release {
runProguard false minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt' proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt'
} }
} }
@@ -62,9 +62,14 @@ android {
} }
dependencies { dependencies {
compile group: 'org.jcodec', name: 'jcodec', version: '0.1.6-3' compile group: 'org.jcodec', name: 'jcodec', version: '0.1.9'
compile group: 'org.bouncycastle', name: 'bcprov-jdk15on', version: '1.51' compile group: 'org.bouncycastle', name: 'bcprov-jdk15on', version: '1.51'
compile group: 'org.bouncycastle', name: 'bcpkix-jdk15on', version: '1.51' compile group: 'org.bouncycastle', name: 'bcpkix-jdk15on', version: '1.51'
compile group: 'com.squareup.okhttp', name: 'okhttp', version:'2.2.0'
compile group: 'com.squareup.okio', name:'okio', version:'1.2.0'
compile files('libs/jmdns-fixed.jar') compile files('libs/jmdns-fixed.jar')
compile files('libs/limelight-common.jar') compile files('libs/limelight-common.jar')
compile files('libs/tinyrtsp.jar') compile files('libs/tinyrtsp.jar')
Binary file not shown.
+2 -1
View File
@@ -64,10 +64,11 @@
<activity <activity
android:name=".Game" android:name=".Game"
android:screenOrientation="sensorLandscape" android:screenOrientation="sensorLandscape"
android:theme="@style/StreamTheme"
android:configChanges="mcc|mnc|locale|touchscreen|keyboard|keyboardHidden|navigation|screenLayout|fontScale|uiMode|orientation|screenSize|smallestScreenSize|layoutDirection" > android:configChanges="mcc|mnc|locale|touchscreen|keyboard|keyboardHidden|navigation|screenLayout|fontScale|uiMode|orientation|screenSize|smallestScreenSize|layoutDirection" >
<meta-data <meta-data
android:name="android.support.PARENT_ACTIVITY" android:name="android.support.PARENT_ACTIVITY"
android:value="com.limelight.Connection" /> android:value="com.limelight.AppView" />
</activity> </activity>
<service <service
android:name=".discovery.DiscoveryService" android:name=".discovery.DiscoveryService"
+467 -228
View File
@@ -1,295 +1,534 @@
package com.limelight; package com.limelight;
import java.io.FileNotFoundException; import java.io.FileNotFoundException;
import java.io.IOException; import java.io.StringReader;
import java.net.InetAddress; import java.net.InetAddress;
import java.net.UnknownHostException; import java.net.UnknownHostException;
import java.util.List; import java.util.List;
import java.util.Locale;
import org.xmlpull.v1.XmlPullParserException; import java.util.UUID;
import com.limelight.binding.PlatformBinding; import com.limelight.binding.PlatformBinding;
import com.limelight.computers.ComputerManagerListener;
import com.limelight.computers.ComputerManagerService;
import com.limelight.grid.AppGridAdapter;
import com.limelight.nvstream.http.ComputerDetails;
import com.limelight.nvstream.http.GfeHttpResponseException; import com.limelight.nvstream.http.GfeHttpResponseException;
import com.limelight.nvstream.http.NvApp; import com.limelight.nvstream.http.NvApp;
import com.limelight.nvstream.http.NvHTTP; import com.limelight.nvstream.http.NvHTTP;
import com.limelight.R; 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.Dialog;
import com.limelight.utils.SpinnerDialog; import com.limelight.utils.SpinnerDialog;
import com.limelight.utils.UiHelper;
import android.app.Activity; import android.app.Activity;
import android.app.AlertDialog;
import android.app.Service;
import android.content.ComponentName;
import android.content.DialogInterface;
import android.content.Intent; import android.content.Intent;
import android.content.ServiceConnection;
import android.content.res.Configuration;
import android.os.Bundle; import android.os.Bundle;
import android.os.IBinder;
import android.view.ContextMenu; import android.view.ContextMenu;
import android.view.Menu; import android.view.Menu;
import android.view.MenuItem; import android.view.MenuItem;
import android.view.View; import android.view.View;
import android.view.ContextMenu.ContextMenuInfo; import android.view.ContextMenu.ContextMenuInfo;
import android.widget.AbsListView;
import android.widget.AdapterView; import android.widget.AdapterView;
import android.widget.AdapterView.OnItemClickListener; import android.widget.AdapterView.OnItemClickListener;
import android.widget.ArrayAdapter;
import android.widget.ListView;
import android.widget.TextView; import android.widget.TextView;
import android.widget.Toast; import android.widget.Toast;
import android.widget.AdapterView.AdapterContextMenuInfo; import android.widget.AdapterView.AdapterContextMenuInfo;
public class AppView extends Activity { public class AppView extends Activity implements AdapterFragmentCallbacks {
private ListView appList; private AppGridAdapter appGridAdapter;
private ArrayAdapter<AppObject> appListAdapter; private String uuidString;
private InetAddress ipAddress;
private String uniqueId;
private boolean remote;
private final static int RESUME_ID = 1; private ComputerDetails computer;
private final static int QUIT_ID = 2; private ComputerManagerService.ApplistPoller poller;
private final static int CANCEL_ID = 3; private SpinnerDialog blockingLoadSpinner;
private String lastRawApplist;
public final static String ADDRESS_EXTRA = "Address"; private final static int START_OR_RESUME_ID = 1;
public final static String UNIQUEID_EXTRA = "UniqueId"; private final static int QUIT_ID = 2;
public final static String NAME_EXTRA = "Name"; private final static int CANCEL_ID = 3;
public final static String REMOTE_EXTRA = "Remote"; private final static int START_WTIH_QUIT = 4;
@Override public final static String NAME_EXTRA = "Name";
protected void onCreate(Bundle savedInstanceState) { public final static String UUID_EXTRA = "UUID";
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_app_view);
byte[] address = getIntent().getByteArrayExtra(ADDRESS_EXTRA); private ComputerManagerService.ComputerManagerBinder managerBinder;
uniqueId = getIntent().getStringExtra(UNIQUEID_EXTRA); private final ServiceConnection serviceConnection = new ServiceConnection() {
remote = getIntent().getBooleanExtra(REMOTE_EXTRA, false); public void onServiceConnected(ComponentName className, IBinder binder) {
if (address == null || uniqueId == null) { final ComputerManagerService.ComputerManagerBinder localBinder =
return; ((ComputerManagerService.ComputerManagerBinder)binder);
}
String labelText = "App List for "+getIntent().getStringExtra(NAME_EXTRA); // Wait in a separate thread to avoid stalling the UI
TextView label = (TextView) findViewById(R.id.appListText); new Thread() {
setTitle(labelText); @Override
label.setText(labelText); public void run() {
// Wait for the binder to be ready
localBinder.waitForReady();
try { // Now make the binder visible
ipAddress = InetAddress.getByAddress(address); managerBinder = localBinder;
} catch (UnknownHostException e) {
return;
}
// Setup the list view // Get the computer object
appList = (ListView)findViewById(R.id.pcListView); computer = managerBinder.getComputer(UUID.fromString(uuidString));
appListAdapter = new ArrayAdapter<AppObject>(this, R.layout.simplerow, R.id.rowTextView);
appListAdapter.setNotifyOnChange(false);
appList.setAdapter(appListAdapter);
appList.setItemsCanFocus(true);
appList.setOnItemClickListener(new OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> arg0, View arg1, int pos,
long id) {
AppObject app = appListAdapter.getItem(pos);
if (app == null || app.app == null) {
return;
}
// Only open the context menu if something is running, otherwise start it try {
if (getRunningAppId() != -1) { appGridAdapter = new AppGridAdapter(AppView.this,
openContextMenu(arg1); PreferenceConfiguration.readPreferences(AppView.this).listMode,
} PreferenceConfiguration.readPreferences(AppView.this).smallIconMode,
else { computer, managerBinder.getUniqueId());
doStart(app.app); } catch (Exception e) {
} e.printStackTrace();
} finish();
}); return;
registerForContextMenu(appList); }
}
@Override // Start updates
protected void onDestroy() { startComputerUpdates();
super.onDestroy();
SpinnerDialog.closeDialogs(this); // Load the app grid with cached data (if possible)
Dialog.closeDialogs(); populateAppGridWithCache();
}
@Override getFragmentManager().beginTransaction()
protected void onResume() { .replace(R.id.appFragmentContainer, new AdapterFragment())
super.onResume(); .commitAllowingStateLoss();
}
updateAppList(); }.start();
}
private int getRunningAppId() {
int runningAppId = -1;
for (int i = 0; i < appListAdapter.getCount(); i++) {
AppObject app = appListAdapter.getItem(i);
if (app.app == null) {
continue;
}
if (app.app.getIsRunning()) {
runningAppId = app.app.getAppId();
break;
}
}
return runningAppId;
}
@Override
public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) {
super.onCreateContextMenu(menu, v, menuInfo);
AdapterContextMenuInfo info = (AdapterContextMenuInfo) menuInfo;
AppObject selectedApp = appListAdapter.getItem(info.position);
if (selectedApp == null || selectedApp.app == null) {
return;
} }
int runningAppId = getRunningAppId(); public void onServiceDisconnected(ComponentName className) {
if (runningAppId != -1) { managerBinder = null;
if (runningAppId == selectedApp.app.getAppId()) { }
menu.add(Menu.NONE, RESUME_ID, 1, "Resume Session"); };
menu.add(Menu.NONE, QUIT_ID, 2, "Quit Session");
} private InetAddress getAddress() {
else { return computer.reachability == ComputerDetails.Reachability.LOCAL ?
menu.add(Menu.NONE, RESUME_ID, 1, "Quit Current Game and Start"); computer.localIp : computer.remoteIp;
menu.add(Menu.NONE, CANCEL_ID, 2, "Cancel"); }
}
private void startComputerUpdates() {
if (managerBinder == null) {
return;
}
managerBinder.startPolling(new ComputerManagerListener() {
@Override
public void notifyComputerUpdated(ComputerDetails details) {
// 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; nothing to do
if (details.rawAppList == null || details.rawAppList.equals(lastRawApplist)) {
return;
}
try {
lastRawApplist = details.rawAppList;
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 @Override
public void onContextMenuClosed(Menu menu) { 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) {
}
private void displayQuitConfirmationDialog(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(this);
builder.setMessage(getResources().getString(R.string.applist_quit_confirmation))
.setPositiveButton(getResources().getString(R.string.yes), dialogClickListener)
.setNegativeButton(getResources().getString(R.string.no), dialogClickListener)
.show();
}
@Override @Override
public boolean onContextItemSelected(MenuItem item) { public boolean onContextItemSelected(MenuItem item) {
AdapterContextMenuInfo info = (AdapterContextMenuInfo) item.getMenuInfo(); AdapterContextMenuInfo info = (AdapterContextMenuInfo) item.getMenuInfo();
AppObject app = appListAdapter.getItem(info.position); final AppObject app = (AppObject) appGridAdapter.getItem(info.position);
switch (item.getItemId()) switch (item.getItemId()) {
{ case START_WTIH_QUIT:
case RESUME_ID: // Display a confirmation dialog first
// Resume is the same as start for us displayQuitConfirmationDialog(new Runnable() {
doStart(app.app); @Override
return true; public void run() {
doStart(app.app);
}
}, null);
return true;
case QUIT_ID: case START_OR_RESUME_ID:
doQuit(app.app); // Resume is the same as start for us
return true; doStart(app.app);
return true;
case CANCEL_ID: case QUIT_ID:
return true; // Display a confirmation dialog first
displayQuitConfirmationDialog(new Runnable() {
@Override
public void run() {
doQuit(app.app);
}
}, null);
return true;
default: case CANCEL_ID:
return super.onContextItemSelected(item); return true;
default:
return super.onContextItemSelected(item);
} }
} }
private static String generateString(NvApp app) { private void updateUiWithAppList(final List<NvApp> appList) {
StringBuilder str = new StringBuilder(); AppView.this.runOnUiThread(new Runnable() {
str.append(app.getAppName()); @Override
if (app.getIsRunning()) { public void run() {
str.append(" - Running"); boolean updated = false;
}
return str.toString(); // 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();
}
}
});
} }
private void addListPlaceholder() { private void doStart(NvApp app) {
appListAdapter.add(new AppObject("No apps found. Try rescanning for games in GeForce Experience.", null)); Intent intent = new Intent(this, 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);
startActivity(intent);
} }
private void updateAppList() { private void doQuit(final NvApp app) {
final SpinnerDialog spinner = SpinnerDialog.displayDialog(this, "App List", "Refreshing app list...", true); Toast.makeText(AppView.this, getResources().getString(R.string.applist_quit_app)+" "+app.getAppName()+"...", Toast.LENGTH_SHORT).show();
new Thread() { new Thread(new Runnable() {
@Override @Override
public void run() { public void run() {
NvHTTP httpConn = new NvHTTP(ipAddress, uniqueId, null, PlatformBinding.getCryptoProvider(AppView.this)); NvHTTP httpConn;
String message;
try {
httpConn = new NvHTTP(getAddress(),
managerBinder.getUniqueId(), null, PlatformBinding.getCryptoProvider(AppView.this));
if (httpConn.quitApp()) {
message = getResources().getString(R.string.applist_quit_success) + " " + app.getAppName();
} else {
message = 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 = getResources().getString(R.string.error_unknown_host);
} catch (FileNotFoundException e) {
message = getResources().getString(R.string.error_404);
} catch (Exception e) {
message = e.getMessage();
} finally {
// Trigger a poll immediately
if (poller != null) {
poller.pollNow();
}
}
try { final String toastMessage = message;
final List<NvApp> appList = httpConn.getAppList(); runOnUiThread(new Runnable() {
@Override
AppView.this.runOnUiThread(new Runnable() { public void run() {
@Override Toast.makeText(AppView.this, toastMessage, Toast.LENGTH_LONG).show();
public void run() { }
appListAdapter.clear(); });
if (appList.isEmpty()) { }
addListPlaceholder(); }).start();
}
else {
for (NvApp app : appList) {
appListAdapter.add(new AppObject(generateString(app), app));
}
}
appListAdapter.notifyDataSetChanged();
}
});
// Success case
return;
} catch (GfeHttpResponseException ignored) {
} catch (IOException ignored) {
} catch (XmlPullParserException ignored) {
} finally {
spinner.dismiss();
}
Dialog.displayDialog(AppView.this, "Error", "Failed to get app list", true);
}
}.start();
} }
private void doStart(NvApp app) { @Override
Intent intent = new Intent(this, Game.class); public int getAdapterFragmentLayoutId() {
intent.putExtra(Game.EXTRA_HOST, ipAddress.getHostAddress()); return PreferenceConfiguration.readPreferences(this).listMode ?
intent.putExtra(Game.EXTRA_APP, app.getAppName()); R.layout.list_view : (PreferenceConfiguration.readPreferences(AppView.this).smallIconMode ?
intent.putExtra(Game.EXTRA_UNIQUEID, uniqueId); R.layout.app_grid_view_small : R.layout.app_grid_view);
intent.putExtra(Game.EXTRA_STREAMING_REMOTE, remote); }
startActivity(intent);
}
private void doQuit(final NvApp app) { @Override
Toast.makeText(AppView.this, "Quitting "+app.getAppName()+"...", Toast.LENGTH_SHORT).show(); public void receiveAbsListView(AbsListView listView) {
new Thread(new Runnable() { listView.setAdapter(appGridAdapter);
@Override listView.setOnItemClickListener(new OnItemClickListener() {
public void run() { @Override
NvHTTP httpConn; public void onItemClick(AdapterView<?> arg0, View arg1, int pos,
String message; long id) {
try { AppObject app = (AppObject) appGridAdapter.getItem(pos);
httpConn = new NvHTTP(ipAddress, uniqueId, null, PlatformBinding.getCryptoProvider(AppView.this));
if (httpConn.quitApp()) {
message = "Successfully quit "+app.getAppName();
}
else {
message = "Failed to quit "+app.getAppName();
}
updateAppList();
} catch (UnknownHostException e) {
message = "Failed to resolve host";
} catch (FileNotFoundException e) {
message = "GFE returned an HTTP 404 error. Make sure your PC is running a supported GPU. Using remote desktop software can also cause this error. "
+ "Try rebooting your machine or reinstalling GFE.";
} catch (Exception e) {
message = e.getMessage();
}
final String toastMessage = message; // Only open the context menu if something is running, otherwise start it
runOnUiThread(new Runnable() { if (getRunningAppId() != -1) {
@Override openContextMenu(arg1);
public void run() { } else {
Toast.makeText(AppView.this, toastMessage, Toast.LENGTH_LONG).show(); doStart(app.app);
} }
}); }
} });
}).start(); registerForContextMenu(listView);
} listView.requestFocus();
}
public class AppObject { public class AppObject {
public String text; public final NvApp app;
public NvApp app;
public AppObject(String text, NvApp app) { public AppObject(NvApp app) {
this.text = text; if (app == null) {
this.app = app; throw new IllegalArgumentException("app must not be null");
} }
this.app = app;
}
@Override @Override
public String toString() { public String toString() {
return text; return app.getAppName();
} }
} }
} }
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -8,17 +8,17 @@ import com.limelight.nvstream.av.audio.AudioRenderer;
import com.limelight.nvstream.http.LimelightCryptoProvider; import com.limelight.nvstream.http.LimelightCryptoProvider;
public class PlatformBinding { public class PlatformBinding {
public static String getDeviceName() { public static String getDeviceName() {
String deviceName = android.os.Build.MODEL; String deviceName = android.os.Build.MODEL;
deviceName = deviceName.replace(" ", ""); deviceName = deviceName.replace(" ", "");
return deviceName; return deviceName;
} }
public static AudioRenderer getAudioRenderer() { public static AudioRenderer getAudioRenderer() {
return new AndroidAudioRenderer(); return new AndroidAudioRenderer();
} }
public static LimelightCryptoProvider getCryptoProvider(Context c) { public static LimelightCryptoProvider getCryptoProvider(Context c) {
return new AndroidCryptoProvider(c); return new AndroidCryptoProvider(c);
} }
} }
@@ -9,62 +9,89 @@ import com.limelight.nvstream.av.audio.AudioRenderer;
public class AndroidAudioRenderer implements AudioRenderer { public class AndroidAudioRenderer implements AudioRenderer {
public static final int FRAME_SIZE = 960; private static final int FRAME_SIZE = 960;
private AudioTrack track; private AudioTrack track;
@Override @Override
public boolean streamInitialized(int channelCount, int sampleRate) { public boolean streamInitialized(int channelCount, int sampleRate) {
int channelConfig; int channelConfig;
int bufferSize; int bufferSize;
switch (channelCount) switch (channelCount)
{ {
case 1: case 1:
channelConfig = AudioFormat.CHANNEL_OUT_MONO; channelConfig = AudioFormat.CHANNEL_OUT_MONO;
break; break;
case 2: case 2:
channelConfig = AudioFormat.CHANNEL_OUT_STEREO; channelConfig = AudioFormat.CHANNEL_OUT_STEREO;
break; break;
default: default:
LimeLog.severe("Decoder returned unhandled channel count"); LimeLog.severe("Decoder returned unhandled channel count");
return false; return false;
} }
bufferSize = Math.max(AudioTrack.getMinBufferSize(sampleRate, // We're not supposed to request less than the minimum
channelConfig, // buffer size for our buffer, but it appears that we can
AudioFormat.ENCODING_PCM_16BIT), // do this on many devices and it lowers audio latency.
FRAME_SIZE * 2); // 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;
// Round to next frame track = new AudioTrack(AudioManager.STREAM_MUSIC,
bufferSize = (((bufferSize + (FRAME_SIZE - 1)) / FRAME_SIZE) * FRAME_SIZE); 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) {}
LimeLog.info("Audio track buffer size: "+bufferSize); // Now try the larger buffer size
track = new AudioTrack(AudioManager.STREAM_MUSIC, bufferSize = Math.max(AudioTrack.getMinBufferSize(sampleRate,
sampleRate, channelConfig,
channelConfig, AudioFormat.ENCODING_PCM_16BIT),
AudioFormat.ENCODING_PCM_16BIT, FRAME_SIZE * 2);
bufferSize,
AudioTrack.MODE_STREAM);
track.play(); // Round to next frame
return true; bufferSize = (((bufferSize + (FRAME_SIZE - 1)) / FRAME_SIZE) * FRAME_SIZE);
}
@Override track = new AudioTrack(AudioManager.STREAM_MUSIC,
public void playDecodedAudio(byte[] audioData, int offset, int length) { sampleRate,
track.write(audioData, offset, length); channelConfig,
} AudioFormat.ENCODING_PCM_16BIT,
bufferSize,
AudioTrack.MODE_STREAM);
track.play();
}
@Override LimeLog.info("Audio track buffer size: "+bufferSize);
public void streamClosing() {
if (track != null) {
track.release();
}
}
@Override return true;
public int getCapabilities() { }
return 0;
} @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;
}
} }
@@ -45,239 +45,239 @@ import com.limelight.nvstream.http.LimelightCryptoProvider;
public class AndroidCryptoProvider implements LimelightCryptoProvider { public class AndroidCryptoProvider implements LimelightCryptoProvider {
private File certFile; private final File certFile;
private File keyFile; private final File keyFile;
private X509Certificate cert; private X509Certificate cert;
private RSAPrivateKey key; private RSAPrivateKey key;
private byte[] pemCertBytes; private byte[] pemCertBytes;
private static final Object globalCryptoLock = new Object(); private static final Object globalCryptoLock = new Object();
static { static {
// Install the Bouncy Castle provider // Install the Bouncy Castle provider
Security.addProvider(new BouncyCastleProvider()); Security.addProvider(new BouncyCastleProvider());
} }
public AndroidCryptoProvider(Context c) { public AndroidCryptoProvider(Context c) {
String dataPath = c.getFilesDir().getAbsolutePath(); String dataPath = c.getFilesDir().getAbsolutePath();
certFile = new File(dataPath + File.separator + "client.crt"); certFile = new File(dataPath + File.separator + "client.crt");
keyFile = new File(dataPath + File.separator + "client.key"); keyFile = new File(dataPath + File.separator + "client.key");
} }
private byte[] loadFileToBytes(File f) { private byte[] loadFileToBytes(File f) {
if (!f.exists()) { if (!f.exists()) {
return null; return null;
} }
try { try {
FileInputStream fin = new FileInputStream(f); FileInputStream fin = new FileInputStream(f);
byte[] fileData = new byte[(int) f.length()]; byte[] fileData = new byte[(int) f.length()];
if (fin.read(fileData) != f.length()) { if (fin.read(fileData) != f.length()) {
// Failed to read // Failed to read
fileData = null; fileData = null;
} }
fin.close(); fin.close();
return fileData; return fileData;
} catch (IOException e) { } catch (IOException e) {
return null; return null;
} }
} }
private boolean loadCertKeyPair() { private boolean loadCertKeyPair() {
byte[] certBytes = loadFileToBytes(certFile); byte[] certBytes = loadFileToBytes(certFile);
byte[] keyBytes = loadFileToBytes(keyFile); byte[] keyBytes = loadFileToBytes(keyFile);
// If either file was missing, we definitely can't succeed // If either file was missing, we definitely can't succeed
if (certBytes == null || keyBytes == null) { if (certBytes == null || keyBytes == null) {
LimeLog.info("Missing cert or key; need to generate a new one"); LimeLog.info("Missing cert or key; need to generate a new one");
return false; return false;
} }
try { try {
CertificateFactory certFactory = CertificateFactory.getInstance("X.509", "BC"); CertificateFactory certFactory = CertificateFactory.getInstance("X.509", "BC");
cert = (X509Certificate) certFactory.generateCertificate(new ByteArrayInputStream(certBytes)); cert = (X509Certificate) certFactory.generateCertificate(new ByteArrayInputStream(certBytes));
pemCertBytes = certBytes; pemCertBytes = certBytes;
KeyFactory keyFactory = KeyFactory.getInstance("RSA", "BC"); KeyFactory keyFactory = KeyFactory.getInstance("RSA", "BC");
key = (RSAPrivateKey) keyFactory.generatePrivate(new PKCS8EncodedKeySpec(keyBytes)); key = (RSAPrivateKey) keyFactory.generatePrivate(new PKCS8EncodedKeySpec(keyBytes));
} catch (CertificateException e) { } catch (CertificateException e) {
// May happen if the cert is corrupt // May happen if the cert is corrupt
LimeLog.warning("Corrupted certificate"); LimeLog.warning("Corrupted certificate");
return false; return false;
} catch (NoSuchAlgorithmException e) { } catch (NoSuchAlgorithmException e) {
// Should never happen // Should never happen
e.printStackTrace(); e.printStackTrace();
return false; return false;
} catch (InvalidKeySpecException e) { } catch (InvalidKeySpecException e) {
// May happen if the key is corrupt // May happen if the key is corrupt
LimeLog.warning("Corrupted key"); LimeLog.warning("Corrupted key");
return false; return false;
} catch (NoSuchProviderException e) { } catch (NoSuchProviderException e) {
// Should never happen // Should never happen
e.printStackTrace(); e.printStackTrace();
return false; return false;
} }
return true; return true;
} }
@SuppressLint("TrulyRandom") @SuppressLint("TrulyRandom")
private boolean generateCertKeyPair() { private boolean generateCertKeyPair() {
byte[] snBytes = new byte[8]; byte[] snBytes = new byte[8];
new SecureRandom().nextBytes(snBytes); new SecureRandom().nextBytes(snBytes);
KeyPair keyPair; KeyPair keyPair;
try { try {
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA", "BC"); KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA", "BC");
keyPairGenerator.initialize(2048); keyPairGenerator.initialize(2048);
keyPair = keyPairGenerator.generateKeyPair(); keyPair = keyPairGenerator.generateKeyPair();
} catch (NoSuchAlgorithmException e1) { } catch (NoSuchAlgorithmException e1) {
// Should never happen // Should never happen
e1.printStackTrace(); e1.printStackTrace();
return false; return false;
} catch (NoSuchProviderException e) { } catch (NoSuchProviderException e) {
// Should never happen // Should never happen
e.printStackTrace(); e.printStackTrace();
return false; return false;
} }
Date now = new Date(); Date now = new Date();
// Expires in 20 years // Expires in 20 years
Calendar calendar = Calendar.getInstance(); Calendar calendar = Calendar.getInstance();
calendar.setTime(now); calendar.setTime(now);
calendar.add(Calendar.YEAR, 20); calendar.add(Calendar.YEAR, 20);
Date expirationDate = calendar.getTime(); Date expirationDate = calendar.getTime();
BigInteger serial = new BigInteger(snBytes).abs(); BigInteger serial = new BigInteger(snBytes).abs();
X500NameBuilder nameBuilder = new X500NameBuilder(BCStyle.INSTANCE); X500NameBuilder nameBuilder = new X500NameBuilder(BCStyle.INSTANCE);
nameBuilder.addRDN(BCStyle.CN, "NVIDIA GameStream Client"); nameBuilder.addRDN(BCStyle.CN, "NVIDIA GameStream Client");
X500Name name = nameBuilder.build(); X500Name name = nameBuilder.build();
X509v3CertificateBuilder certBuilder = new X509v3CertificateBuilder(name, serial, now, expirationDate, Locale.ENGLISH, name, X509v3CertificateBuilder certBuilder = new X509v3CertificateBuilder(name, serial, now, expirationDate, Locale.ENGLISH, name,
SubjectPublicKeyInfo.getInstance(keyPair.getPublic().getEncoded())); SubjectPublicKeyInfo.getInstance(keyPair.getPublic().getEncoded()));
try { try {
ContentSigner sigGen = new JcaContentSignerBuilder("SHA1withRSA").setProvider(BouncyCastleProvider.PROVIDER_NAME).build(keyPair.getPrivate()); ContentSigner sigGen = new JcaContentSignerBuilder("SHA1withRSA").setProvider(BouncyCastleProvider.PROVIDER_NAME).build(keyPair.getPrivate());
cert = new JcaX509CertificateConverter().setProvider(BouncyCastleProvider.PROVIDER_NAME).getCertificate(certBuilder.build(sigGen)); cert = new JcaX509CertificateConverter().setProvider(BouncyCastleProvider.PROVIDER_NAME).getCertificate(certBuilder.build(sigGen));
key = (RSAPrivateKey) keyPair.getPrivate(); key = (RSAPrivateKey) keyPair.getPrivate();
} catch (Exception e) { } catch (Exception e) {
// Nothing should go wrong here // Nothing should go wrong here
e.printStackTrace(); e.printStackTrace();
return false; return false;
} }
LimeLog.info("Generated a new key pair"); LimeLog.info("Generated a new key pair");
// Save the resulting pair // Save the resulting pair
saveCertKeyPair(); saveCertKeyPair();
return true; return true;
} }
private void saveCertKeyPair() { private void saveCertKeyPair() {
try { try {
FileOutputStream certOut = new FileOutputStream(certFile); FileOutputStream certOut = new FileOutputStream(certFile);
FileOutputStream keyOut = new FileOutputStream(keyFile); FileOutputStream keyOut = new FileOutputStream(keyFile);
// Write the certificate in OpenSSL PEM format (important for the server) // Write the certificate in OpenSSL PEM format (important for the server)
StringWriter strWriter = new StringWriter(); StringWriter strWriter = new StringWriter();
JcaPEMWriter pemWriter = new JcaPEMWriter(strWriter); JcaPEMWriter pemWriter = new JcaPEMWriter(strWriter);
pemWriter.writeObject(cert); pemWriter.writeObject(cert);
pemWriter.close(); pemWriter.close();
// Line endings MUST be UNIX for the PC to accept the cert properly // Line endings MUST be UNIX for the PC to accept the cert properly
OutputStreamWriter certWriter = new OutputStreamWriter(certOut); OutputStreamWriter certWriter = new OutputStreamWriter(certOut);
String pemStr = strWriter.getBuffer().toString(); String pemStr = strWriter.getBuffer().toString();
for (int i = 0; i < pemStr.length(); i++) { for (int i = 0; i < pemStr.length(); i++) {
char c = pemStr.charAt(i); char c = pemStr.charAt(i);
if (c != '\r') if (c != '\r')
certWriter.append(c); certWriter.append(c);
} }
certWriter.close(); certWriter.close();
// Write the private out in PKCS8 format // Write the private out in PKCS8 format
keyOut.write(key.getEncoded()); keyOut.write(key.getEncoded());
certOut.close(); certOut.close();
keyOut.close(); keyOut.close();
LimeLog.info("Saved generated key pair to disk"); LimeLog.info("Saved generated key pair to disk");
} catch (IOException e) { } catch (IOException e) {
// This isn't good because it means we'll have // This isn't good because it means we'll have
// to re-pair next time // to re-pair next time
e.printStackTrace(); e.printStackTrace();
} }
} }
public X509Certificate getClientCertificate() { public X509Certificate getClientCertificate() {
// Use a lock here to ensure only one guy will be generating or loading // Use a lock here to ensure only one guy will be generating or loading
// the certificate and key at a time // the certificate and key at a time
synchronized (globalCryptoLock) { synchronized (globalCryptoLock) {
// Return a loaded cert if we have one // Return a loaded cert if we have one
if (cert != null) { if (cert != null) {
return cert; return cert;
} }
// No loaded cert yet, let's see if we have one on disk // No loaded cert yet, let's see if we have one on disk
if (loadCertKeyPair()) { if (loadCertKeyPair()) {
// Got one // Got one
return cert; return cert;
} }
// Try to generate a new key pair // Try to generate a new key pair
if (!generateCertKeyPair()) { if (!generateCertKeyPair()) {
// Failed // Failed
return null; return null;
} }
// Load the generated pair // Load the generated pair
loadCertKeyPair(); loadCertKeyPair();
return cert; return cert;
} }
} }
public RSAPrivateKey getClientPrivateKey() { public RSAPrivateKey getClientPrivateKey() {
// Use a lock here to ensure only one guy will be generating or loading // Use a lock here to ensure only one guy will be generating or loading
// the certificate and key at a time // the certificate and key at a time
synchronized (globalCryptoLock) { synchronized (globalCryptoLock) {
// Return a loaded key if we have one // Return a loaded key if we have one
if (key != null) { if (key != null) {
return key; return key;
} }
// No loaded key yet, let's see if we have one on disk // No loaded key yet, let's see if we have one on disk
if (loadCertKeyPair()) { if (loadCertKeyPair()) {
// Got one // Got one
return key; return key;
} }
// Try to generate a new key pair // Try to generate a new key pair
if (!generateCertKeyPair()) { if (!generateCertKeyPair()) {
// Failed // Failed
return null; return null;
} }
// Load the generated pair // Load the generated pair
loadCertKeyPair(); loadCertKeyPair();
return key; return key;
} }
} }
public byte[] getPemEncodedClientCertificate() { public byte[] getPemEncodedClientCertificate() {
synchronized (globalCryptoLock) { synchronized (globalCryptoLock) {
// Call our helper function to do the cert loading/generation for us // Call our helper function to do the cert loading/generation for us
getClientCertificate(); getClientCertificate();
// Return a cached value if we have it // Return a cached value if we have it
return pemCertBytes; return pemCertBytes;
} }
} }
@Override @Override
public String encodeBase64String(byte[] data) { public String encodeBase64String(byte[] data) {
return Base64.encodeToString(data, Base64.NO_WRAP); return Base64.encodeToString(data, Base64.NO_WRAP);
} }
} }
File diff suppressed because it is too large Load Diff
@@ -15,7 +15,7 @@ public class KeyboardTranslator extends KeycodeTranslator {
/** /**
* GFE's prefix for every key code * 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_0 = 48;
public static final int VK_9 = 57; 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_Z = 90;
public static final int VK_ALT = 18; public static final int VK_ALT = 18;
public static final int VK_NUMPAD0 = 96; public static final int VK_NUMPAD0 = 96;
public static final int VK_BACK_SLASH = 92; public static final int VK_BACK_SLASH = 92;
public static final int VK_CAPS_LOCK = 20; public static final int VK_CAPS_LOCK = 20;
public static final int VK_CLEAR = 12; public static final int VK_CLEAR = 12;
public static final int VK_COMMA = 44; public static final int VK_COMMA = 44;
public static final int VK_CONTROL = 17; public static final int VK_CONTROL = 17;
@@ -4,90 +4,117 @@ import com.limelight.nvstream.NvConnection;
import com.limelight.nvstream.input.MouseButtonPacket; import com.limelight.nvstream.input.MouseButtonPacket;
public class TouchContext { public class TouchContext {
private int lastTouchX = 0; private int lastTouchX = 0;
private int lastTouchY = 0; private int lastTouchY = 0;
private int originalTouchX = 0; private int originalTouchX = 0;
private int originalTouchY = 0; private int originalTouchY = 0;
private long originalTouchTime = 0; private long originalTouchTime = 0;
private boolean cancelled;
private NvConnection conn; private final NvConnection conn;
private int actionIndex; 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_MOVEMENT_THRESHOLD = 10;
private static final int TAP_TIME_THRESHOLD = 250; private static final int TAP_TIME_THRESHOLD = 250;
public TouchContext(NvConnection conn, int actionIndex) public TouchContext(NvConnection conn, int actionIndex, double xFactor, double yFactor)
{ {
this.conn = conn; this.conn = conn;
this.actionIndex = actionIndex; this.actionIndex = actionIndex;
} this.xFactor = xFactor;
this.yFactor = yFactor;
}
private boolean isTap() public int getActionIndex()
{ {
int xDelta = Math.abs(lastTouchX - originalTouchX); return actionIndex;
int yDelta = Math.abs(lastTouchY - originalTouchY); }
long timeDelta = System.currentTimeMillis() - originalTouchTime;
return xDelta <= TAP_MOVEMENT_THRESHOLD && private boolean isTap()
yDelta <= TAP_MOVEMENT_THRESHOLD && {
timeDelta <= TAP_TIME_THRESHOLD; int xDelta = Math.abs(lastTouchX - originalTouchX);
} int yDelta = Math.abs(lastTouchY - originalTouchY);
long timeDelta = System.currentTimeMillis() - originalTouchTime;
private byte getMouseButtonIndex() return xDelta <= TAP_MOVEMENT_THRESHOLD &&
{ yDelta <= TAP_MOVEMENT_THRESHOLD &&
if (actionIndex == 1) { timeDelta <= TAP_TIME_THRESHOLD;
return MouseButtonPacket.BUTTON_RIGHT; }
}
else {
return MouseButtonPacket.BUTTON_LEFT;
}
}
public boolean touchDownEvent(int eventX, int eventY) private byte getMouseButtonIndex()
{ {
originalTouchX = lastTouchX = eventX; if (actionIndex == 1) {
originalTouchY = lastTouchY = eventY; return MouseButtonPacket.BUTTON_RIGHT;
originalTouchTime = System.currentTimeMillis(); }
else {
return MouseButtonPacket.BUTTON_LEFT;
}
}
return true; public boolean touchDownEvent(int eventX, int eventY)
} {
originalTouchX = lastTouchX = eventX;
originalTouchY = lastTouchY = eventY;
originalTouchTime = System.currentTimeMillis();
cancelled = false;
public void touchUpEvent(int eventX, int eventY) return true;
{ }
if (isTap())
{
byte buttonIndex = getMouseButtonIndex();
// Lower the mouse button public void touchUpEvent(int eventX, int eventY)
conn.sendMouseButtonDown(buttonIndex); {
if (cancelled) {
return;
}
// We need to sleep a bit here because some games if (isTap())
// do input detection by polling {
try { byte buttonIndex = getMouseButtonIndex();
Thread.sleep(100);
} catch (InterruptedException ignored) {}
// Raise the mouse button // Lower the mouse button
conn.sendMouseButtonUp(buttonIndex); conn.sendMouseButtonDown(buttonIndex);
}
}
public boolean touchMoveEvent(int eventX, int eventY) // We need to sleep a bit here because some games
{ // do input detection by polling
if (eventX != lastTouchX || eventY != lastTouchY) try {
{ Thread.sleep(100);
// We only send moves for the primary touch point } catch (InterruptedException ignored) {}
if (actionIndex == 0) {
conn.sendMouseMove((short)(eventX - lastTouchX),
(short)(eventY - lastTouchY));
}
lastTouchX = eventX; // Raise the mouse button
lastTouchY = eventY; conn.sendMouseButtonUp(buttonIndex);
}
}
return true; 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;
return false; // 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;
}
} }
@@ -1,41 +1,41 @@
package com.limelight.binding.input.evdev; package com.limelight.binding.input.evdev;
public class EvdevEvent { public class EvdevEvent {
public static final int EVDEV_MIN_EVENT_SIZE = 16; public static final int EVDEV_MIN_EVENT_SIZE = 16;
public static final int EVDEV_MAX_EVENT_SIZE = 24; public static final int EVDEV_MAX_EVENT_SIZE = 24;
/* Event types */ /* Event types */
public static final short EV_SYN = 0x00; public static final short EV_SYN = 0x00;
public static final short EV_KEY = 0x01; public static final short EV_KEY = 0x01;
public static final short EV_REL = 0x02; public static final short EV_REL = 0x02;
public static final short EV_MSC = 0x04; public static final short EV_MSC = 0x04;
/* Relative axes */ /* Relative axes */
public static final short REL_X = 0x00; public static final short REL_X = 0x00;
public static final short REL_Y = 0x01; public static final short REL_Y = 0x01;
public static final short REL_WHEEL = 0x08; public static final short REL_WHEEL = 0x08;
/* Buttons */ /* Buttons */
public static final short BTN_LEFT = 0x110; public static final short BTN_LEFT = 0x110;
public static final short BTN_RIGHT = 0x111; public static final short BTN_RIGHT = 0x111;
public static final short BTN_MIDDLE = 0x112; public static final short BTN_MIDDLE = 0x112;
public static final short BTN_SIDE = 0x113; public static final short BTN_SIDE = 0x113;
public static final short BTN_EXTRA = 0x114; public static final short BTN_EXTRA = 0x114;
public static final short BTN_FORWARD = 0x115; public static final short BTN_FORWARD = 0x115;
public static final short BTN_BACK = 0x116; public static final short BTN_BACK = 0x116;
public static final short BTN_TASK = 0x117; public static final short BTN_TASK = 0x117;
public static final short BTN_GAMEPAD = 0x130; public static final short BTN_GAMEPAD = 0x130;
/* Keys */ /* Keys */
public static final short KEY_Q = 16; public static final short KEY_Q = 16;
public short type; public final short type;
public short code; public final short code;
public int value; public final int value;
public EvdevEvent(short type, short code, int value) { public EvdevEvent(short type, short code, int value) {
this.type = type; this.type = type;
this.code = code; this.code = code;
this.value = value; this.value = value;
} }
} }
@@ -7,161 +7,161 @@ import com.limelight.LimeLog;
public class EvdevHandler { public class EvdevHandler {
private String absolutePath; private final String absolutePath;
private EvdevListener listener; private final EvdevListener listener;
private boolean shutdown = false; private boolean shutdown = false;
private int fd = -1; private int fd = -1;
private Thread handlerThread = new Thread() { private final Thread handlerThread = new Thread() {
@Override @Override
public void run() { public void run() {
// All the finally blocks here make this code look like a mess // All the finally blocks here make this code look like a mess
// but it's important that we get this right to avoid causing // but it's important that we get this right to avoid causing
// system-wide input problems. // system-wide input problems.
// Open the /dev/input/eventX file // Open the /dev/input/eventX file
fd = EvdevReader.open(absolutePath); fd = EvdevReader.open(absolutePath);
if (fd == -1) { if (fd == -1) {
LimeLog.warning("Unable to open "+absolutePath); LimeLog.warning("Unable to open "+absolutePath);
return; return;
} }
try { try {
// Check if it's a mouse or keyboard, but not a gamepad // Check if it's a mouse or keyboard, but not a gamepad
if ((!EvdevReader.isMouse(fd) && !EvdevReader.isAlphaKeyboard(fd)) || if ((!EvdevReader.isMouse(fd) && !EvdevReader.isAlphaKeyboard(fd)) ||
EvdevReader.isGamepad(fd)) { EvdevReader.isGamepad(fd)) {
// We only handle keyboards and mice // We only handle keyboards and mice
return; return;
} }
// Grab it for ourselves // Grab it for ourselves
if (!EvdevReader.grab(fd)) { if (!EvdevReader.grab(fd)) {
LimeLog.warning("Unable to grab "+absolutePath); LimeLog.warning("Unable to grab "+absolutePath);
return; return;
} }
LimeLog.info("Grabbed device for raw keyboard/mouse input: "+absolutePath); LimeLog.info("Grabbed device for raw keyboard/mouse input: "+absolutePath);
ByteBuffer buffer = ByteBuffer.allocate(EvdevEvent.EVDEV_MAX_EVENT_SIZE).order(ByteOrder.nativeOrder()); ByteBuffer buffer = ByteBuffer.allocate(EvdevEvent.EVDEV_MAX_EVENT_SIZE).order(ByteOrder.nativeOrder());
try { try {
int deltaX = 0; int deltaX = 0;
int deltaY = 0; int deltaY = 0;
byte deltaScroll = 0; byte deltaScroll = 0;
while (!isInterrupted() && !shutdown) { while (!isInterrupted() && !shutdown) {
EvdevEvent event = EvdevReader.read(fd, buffer); EvdevEvent event = EvdevReader.read(fd, buffer);
if (event == null) { if (event == null) {
return; return;
} }
switch (event.type) switch (event.type)
{ {
case EvdevEvent.EV_SYN: case EvdevEvent.EV_SYN:
if (deltaX != 0 || deltaY != 0) { if (deltaX != 0 || deltaY != 0) {
listener.mouseMove(deltaX, deltaY); listener.mouseMove(deltaX, deltaY);
deltaX = deltaY = 0; deltaX = deltaY = 0;
} }
if (deltaScroll != 0) { if (deltaScroll != 0) {
listener.mouseScroll(deltaScroll); listener.mouseScroll(deltaScroll);
deltaScroll = 0; deltaScroll = 0;
} }
break; break;
case EvdevEvent.EV_REL: case EvdevEvent.EV_REL:
switch (event.code) switch (event.code)
{ {
case EvdevEvent.REL_X: case EvdevEvent.REL_X:
deltaX = event.value; deltaX = event.value;
break; break;
case EvdevEvent.REL_Y: case EvdevEvent.REL_Y:
deltaY = event.value; deltaY = event.value;
break; break;
case EvdevEvent.REL_WHEEL: case EvdevEvent.REL_WHEEL:
deltaScroll = (byte) event.value; deltaScroll = (byte) event.value;
break; break;
} }
break; break;
case EvdevEvent.EV_KEY: case EvdevEvent.EV_KEY:
switch (event.code) switch (event.code)
{ {
case EvdevEvent.BTN_LEFT: case EvdevEvent.BTN_LEFT:
listener.mouseButtonEvent(EvdevListener.BUTTON_LEFT, listener.mouseButtonEvent(EvdevListener.BUTTON_LEFT,
event.value != 0); event.value != 0);
break; break;
case EvdevEvent.BTN_MIDDLE: case EvdevEvent.BTN_MIDDLE:
listener.mouseButtonEvent(EvdevListener.BUTTON_MIDDLE, listener.mouseButtonEvent(EvdevListener.BUTTON_MIDDLE,
event.value != 0); event.value != 0);
break; break;
case EvdevEvent.BTN_RIGHT: case EvdevEvent.BTN_RIGHT:
listener.mouseButtonEvent(EvdevListener.BUTTON_RIGHT, listener.mouseButtonEvent(EvdevListener.BUTTON_RIGHT,
event.value != 0); event.value != 0);
break; break;
case EvdevEvent.BTN_SIDE: case EvdevEvent.BTN_SIDE:
case EvdevEvent.BTN_EXTRA: case EvdevEvent.BTN_EXTRA:
case EvdevEvent.BTN_FORWARD: case EvdevEvent.BTN_FORWARD:
case EvdevEvent.BTN_BACK: case EvdevEvent.BTN_BACK:
case EvdevEvent.BTN_TASK: case EvdevEvent.BTN_TASK:
// Other unhandled mouse buttons // Other unhandled mouse buttons
break; break;
default: default:
// We got some unrecognized button. This means // We got some unrecognized button. This means
// someone is trying to use the other device in this // someone is trying to use the other device in this
// "combination" input device. We'll try to handle // "combination" input device. We'll try to handle
// it via keyboard, but we're not going to disconnect // it via keyboard, but we're not going to disconnect
// if we can't // if we can't
short keyCode = EvdevTranslator.translateEvdevKeyCode(event.code); short keyCode = EvdevTranslator.translateEvdevKeyCode(event.code);
if (keyCode != 0) { if (keyCode != 0) {
listener.keyboardEvent(event.value != 0, keyCode); listener.keyboardEvent(event.value != 0, keyCode);
} }
break; break;
} }
break; break;
case EvdevEvent.EV_MSC: case EvdevEvent.EV_MSC:
break; break;
} }
} }
} finally { } finally {
// Release our grab // Release our grab
EvdevReader.ungrab(fd); EvdevReader.ungrab(fd);
} }
} finally { } finally {
// Close the file // Close the file
EvdevReader.close(fd); EvdevReader.close(fd);
} }
} }
}; };
public EvdevHandler(String absolutePath, EvdevListener listener) { public EvdevHandler(String absolutePath, EvdevListener listener) {
this.absolutePath = absolutePath; this.absolutePath = absolutePath;
this.listener = listener; this.listener = listener;
} }
public void start() { public void start() {
handlerThread.start(); handlerThread.start();
} }
public void stop() { public void stop() {
// Close the fd. It doesn't matter if this races // Close the fd. It doesn't matter if this races
// with the handler thread. We'll close this out from // with the handler thread. We'll close this out from
// under the thread to wake it up // under the thread to wake it up
if (fd != -1) { if (fd != -1) {
EvdevReader.close(fd); EvdevReader.close(fd);
} }
shutdown = true; shutdown = true;
handlerThread.interrupt(); handlerThread.interrupt();
try { try {
handlerThread.join(); handlerThread.join();
} catch (InterruptedException ignored) {} } catch (InterruptedException ignored) {}
} }
public void notifyDeleted() { public void notifyDeleted() {
stop(); stop();
} }
} }
@@ -1,12 +1,12 @@
package com.limelight.binding.input.evdev; package com.limelight.binding.input.evdev;
public interface EvdevListener { public interface EvdevListener {
public static final int BUTTON_LEFT = 1; public static final int BUTTON_LEFT = 1;
public static final int BUTTON_MIDDLE = 2; public static final int BUTTON_MIDDLE = 2;
public static final int BUTTON_RIGHT = 3; public static final int BUTTON_RIGHT = 3;
public void mouseMove(int deltaX, int deltaY); public void mouseMove(int deltaX, int deltaY);
public void mouseButtonEvent(int buttonId, boolean down); public void mouseButtonEvent(int buttonId, boolean down);
public void mouseScroll(byte amount); public void mouseScroll(byte amount);
public void keyboardEvent(boolean buttonDown, short keyCode); public void keyboardEvent(boolean buttonDown, short keyCode);
} }
@@ -1,103 +1,105 @@
package com.limelight.binding.input.evdev; package com.limelight.binding.input.evdev;
import java.io.IOException; import android.os.Build;
import java.io.OutputStream;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.util.Locale; import java.util.Locale;
import com.limelight.LimeLog; import com.limelight.LimeLog;
public class EvdevReader { public class EvdevReader {
static { static {
System.loadLibrary("evdev_reader"); System.loadLibrary("evdev_reader");
} }
// Requires root to chmod /dev/input/eventX public static void patchSeLinuxPolicies() {
public static boolean setPermissions(String[] files, int octalPermissions) { //
ProcessBuilder builder = new ProcessBuilder("su"); // 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 }\"");
}
}
try { // Requires root to chmod /dev/input/eventX
Process p = builder.start(); public static void setPermissions(String[] files, int octalPermissions) {
EvdevShell shell = EvdevShell.getInstance();
OutputStream stdin = p.getOutputStream(); for (String file : files) {
for (String file : files) { shell.runCommand(String.format((Locale)null, "chmod %o %s", octalPermissions, file));
stdin.write(String.format((Locale)null, "chmod %o %s\n", octalPermissions, file).getBytes("UTF-8")); }
} }
stdin.write("exit\n".getBytes("UTF-8"));
stdin.flush();
p.waitFor(); // Returns the fd to be passed to other function or -1 on error
p.destroy(); public static native int open(String fileName);
return true;
} catch (IOException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
}
return false; // 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);
// Returns the fd to be passed to other function or -1 on error // Used for checking device capabilities
public static native int open(String fileName); 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);
// Prevent other apps (including Android itself) from using the device while "grabbed" public static boolean isMouse(int fd) {
public static native boolean grab(int fd); // This is the same check that Android does in EventHub.cpp
public static native boolean ungrab(int fd); return hasRelAxis(fd, EvdevEvent.REL_X) &&
hasRelAxis(fd, EvdevEvent.REL_Y) &&
hasKey(fd, EvdevEvent.BTN_LEFT);
}
// Used for checking device capabilities public static boolean isAlphaKeyboard(int fd) {
public static native boolean hasRelAxis(int fd, short axis); // This is the same check that Android does in EventHub.cpp
public static native boolean hasAbsAxis(int fd, short axis); return hasKey(fd, EvdevEvent.KEY_Q);
public static native boolean hasKey(int fd, short key); }
public static boolean isMouse(int fd) { public static boolean isGamepad(int fd) {
// This is the same check that Android does in EventHub.cpp return hasKey(fd, EvdevEvent.BTN_GAMEPAD);
return hasRelAxis(fd, EvdevEvent.REL_X) && }
hasRelAxis(fd, EvdevEvent.REL_Y) &&
hasKey(fd, EvdevEvent.BTN_LEFT);
}
public static boolean isAlphaKeyboard(int fd) { // Returns the bytes read or -1 on error
// This is the same check that Android does in EventHub.cpp private static native int read(int fd, byte[] buffer);
return hasKey(fd, EvdevEvent.KEY_Q);
}
public static boolean isGamepad(int fd) { // Takes a byte buffer to use to read the output into.
return hasKey(fd, EvdevEvent.BTN_GAMEPAD); // 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;
}
// Returns the bytes read or -1 on error buffer.limit(bytesRead);
private static native int read(int fd, byte[] buffer); buffer.rewind();
// Takes a byte buffer to use to read the output into. // Throw away the time stamp
// This buffer MUST be in native byte order and at least if (bytesRead == EvdevEvent.EVDEV_MAX_EVENT_SIZE) {
// EVDEV_MAX_EVENT_SIZE bytes long. buffer.getLong();
public static EvdevEvent read(int fd, ByteBuffer buffer) { buffer.getLong();
int bytesRead = read(fd, buffer.array()); } else {
if (bytesRead < 0) { buffer.getInt();
LimeLog.warning("Failed to read: "+bytesRead); buffer.getInt();
return null; }
}
else if (bytesRead < EvdevEvent.EVDEV_MIN_EVENT_SIZE) {
LimeLog.warning("Short read: "+bytesRead);
return null;
}
buffer.limit(bytesRead); return new EvdevEvent(buffer.getShort(), buffer.getShort(), buffer.getInt());
buffer.rewind(); }
// Throw away the time stamp // Closes the fd from open()
if (bytesRead == EvdevEvent.EVDEV_MAX_EVENT_SIZE) { public static native int close(int fd);
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);
} }
@@ -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();
}
}
}
@@ -4,136 +4,136 @@ import android.view.KeyEvent;
public class EvdevTranslator { public class EvdevTranslator {
public static final short EVDEV_KEY_CODES[] = { private static final short[] EVDEV_KEY_CODES = {
0, //KeyEvent.VK_RESERVED 0, //KeyEvent.VK_RESERVED
KeyEvent.KEYCODE_ESCAPE, KeyEvent.KEYCODE_ESCAPE,
KeyEvent.KEYCODE_1, KeyEvent.KEYCODE_1,
KeyEvent.KEYCODE_2, KeyEvent.KEYCODE_2,
KeyEvent.KEYCODE_3, KeyEvent.KEYCODE_3,
KeyEvent.KEYCODE_4, KeyEvent.KEYCODE_4,
KeyEvent.KEYCODE_5, KeyEvent.KEYCODE_5,
KeyEvent.KEYCODE_6, KeyEvent.KEYCODE_6,
KeyEvent.KEYCODE_7, KeyEvent.KEYCODE_7,
KeyEvent.KEYCODE_8, KeyEvent.KEYCODE_8,
KeyEvent.KEYCODE_9, KeyEvent.KEYCODE_9,
KeyEvent.KEYCODE_0, KeyEvent.KEYCODE_0,
KeyEvent.KEYCODE_MINUS, KeyEvent.KEYCODE_MINUS,
KeyEvent.KEYCODE_EQUALS, KeyEvent.KEYCODE_EQUALS,
KeyEvent.KEYCODE_DEL, KeyEvent.KEYCODE_DEL,
KeyEvent.KEYCODE_TAB, KeyEvent.KEYCODE_TAB,
KeyEvent.KEYCODE_Q, KeyEvent.KEYCODE_Q,
KeyEvent.KEYCODE_W, KeyEvent.KEYCODE_W,
KeyEvent.KEYCODE_E, KeyEvent.KEYCODE_E,
KeyEvent.KEYCODE_R, KeyEvent.KEYCODE_R,
KeyEvent.KEYCODE_T, KeyEvent.KEYCODE_T,
KeyEvent.KEYCODE_Y, KeyEvent.KEYCODE_Y,
KeyEvent.KEYCODE_U, KeyEvent.KEYCODE_U,
KeyEvent.KEYCODE_I, KeyEvent.KEYCODE_I,
KeyEvent.KEYCODE_O, KeyEvent.KEYCODE_O,
KeyEvent.KEYCODE_P, KeyEvent.KEYCODE_P,
KeyEvent.KEYCODE_LEFT_BRACKET, KeyEvent.KEYCODE_LEFT_BRACKET,
KeyEvent.KEYCODE_RIGHT_BRACKET, KeyEvent.KEYCODE_RIGHT_BRACKET,
KeyEvent.KEYCODE_ENTER, KeyEvent.KEYCODE_ENTER,
KeyEvent.KEYCODE_CTRL_LEFT, KeyEvent.KEYCODE_CTRL_LEFT,
KeyEvent.KEYCODE_A, KeyEvent.KEYCODE_A,
KeyEvent.KEYCODE_S, KeyEvent.KEYCODE_S,
KeyEvent.KEYCODE_D, KeyEvent.KEYCODE_D,
KeyEvent.KEYCODE_F, KeyEvent.KEYCODE_F,
KeyEvent.KEYCODE_G, KeyEvent.KEYCODE_G,
KeyEvent.KEYCODE_H, KeyEvent.KEYCODE_H,
KeyEvent.KEYCODE_J, KeyEvent.KEYCODE_J,
KeyEvent.KEYCODE_K, KeyEvent.KEYCODE_K,
KeyEvent.KEYCODE_L, KeyEvent.KEYCODE_L,
KeyEvent.KEYCODE_SEMICOLON, KeyEvent.KEYCODE_SEMICOLON,
KeyEvent.KEYCODE_APOSTROPHE, KeyEvent.KEYCODE_APOSTROPHE,
KeyEvent.KEYCODE_GRAVE, KeyEvent.KEYCODE_GRAVE,
KeyEvent.KEYCODE_SHIFT_LEFT, KeyEvent.KEYCODE_SHIFT_LEFT,
KeyEvent.KEYCODE_BACKSLASH, KeyEvent.KEYCODE_BACKSLASH,
KeyEvent.KEYCODE_Z, KeyEvent.KEYCODE_Z,
KeyEvent.KEYCODE_X, KeyEvent.KEYCODE_X,
KeyEvent.KEYCODE_C, KeyEvent.KEYCODE_C,
KeyEvent.KEYCODE_V, KeyEvent.KEYCODE_V,
KeyEvent.KEYCODE_B, KeyEvent.KEYCODE_B,
KeyEvent.KEYCODE_N, KeyEvent.KEYCODE_N,
KeyEvent.KEYCODE_M, KeyEvent.KEYCODE_M,
KeyEvent.KEYCODE_COMMA, KeyEvent.KEYCODE_COMMA,
KeyEvent.KEYCODE_PERIOD, KeyEvent.KEYCODE_PERIOD,
KeyEvent.KEYCODE_SLASH, KeyEvent.KEYCODE_SLASH,
KeyEvent.KEYCODE_SHIFT_RIGHT, KeyEvent.KEYCODE_SHIFT_RIGHT,
KeyEvent.KEYCODE_NUMPAD_MULTIPLY, KeyEvent.KEYCODE_NUMPAD_MULTIPLY,
KeyEvent.KEYCODE_ALT_LEFT, KeyEvent.KEYCODE_ALT_LEFT,
KeyEvent.KEYCODE_SPACE, KeyEvent.KEYCODE_SPACE,
KeyEvent.KEYCODE_CAPS_LOCK, KeyEvent.KEYCODE_CAPS_LOCK,
KeyEvent.KEYCODE_F1, KeyEvent.KEYCODE_F1,
KeyEvent.KEYCODE_F2, KeyEvent.KEYCODE_F2,
KeyEvent.KEYCODE_F3, KeyEvent.KEYCODE_F3,
KeyEvent.KEYCODE_F4, KeyEvent.KEYCODE_F4,
KeyEvent.KEYCODE_F5, KeyEvent.KEYCODE_F5,
KeyEvent.KEYCODE_F6, KeyEvent.KEYCODE_F6,
KeyEvent.KEYCODE_F7, KeyEvent.KEYCODE_F7,
KeyEvent.KEYCODE_F8, KeyEvent.KEYCODE_F8,
KeyEvent.KEYCODE_F9, KeyEvent.KEYCODE_F9,
KeyEvent.KEYCODE_F10, KeyEvent.KEYCODE_F10,
KeyEvent.KEYCODE_NUM_LOCK, KeyEvent.KEYCODE_NUM_LOCK,
KeyEvent.KEYCODE_SCROLL_LOCK, KeyEvent.KEYCODE_SCROLL_LOCK,
KeyEvent.KEYCODE_NUMPAD_7, KeyEvent.KEYCODE_NUMPAD_7,
KeyEvent.KEYCODE_NUMPAD_8, KeyEvent.KEYCODE_NUMPAD_8,
KeyEvent.KEYCODE_NUMPAD_9, KeyEvent.KEYCODE_NUMPAD_9,
KeyEvent.KEYCODE_NUMPAD_SUBTRACT, KeyEvent.KEYCODE_NUMPAD_SUBTRACT,
KeyEvent.KEYCODE_NUMPAD_4, KeyEvent.KEYCODE_NUMPAD_4,
KeyEvent.KEYCODE_NUMPAD_5, KeyEvent.KEYCODE_NUMPAD_5,
KeyEvent.KEYCODE_NUMPAD_6, KeyEvent.KEYCODE_NUMPAD_6,
KeyEvent.KEYCODE_NUMPAD_ADD, KeyEvent.KEYCODE_NUMPAD_ADD,
KeyEvent.KEYCODE_NUMPAD_1, KeyEvent.KEYCODE_NUMPAD_1,
KeyEvent.KEYCODE_NUMPAD_2, KeyEvent.KEYCODE_NUMPAD_2,
KeyEvent.KEYCODE_NUMPAD_3, KeyEvent.KEYCODE_NUMPAD_3,
KeyEvent.KEYCODE_NUMPAD_0, KeyEvent.KEYCODE_NUMPAD_0,
KeyEvent.KEYCODE_NUMPAD_DOT, KeyEvent.KEYCODE_NUMPAD_DOT,
0, 0,
0, //KeyEvent.VK_ZENKAKUHANKAKU, 0, //KeyEvent.VK_ZENKAKUHANKAKU,
0, //KeyEvent.VK_102ND, 0, //KeyEvent.VK_102ND,
KeyEvent.KEYCODE_F11, KeyEvent.KEYCODE_F11,
KeyEvent.KEYCODE_F12, KeyEvent.KEYCODE_F12,
0, //KeyEvent.VK_RO, 0, //KeyEvent.VK_RO,
0, //KeyEvent.VK_KATAKANA, 0, //KeyEvent.VK_KATAKANA,
0, //KeyEvent.VK_HIRAGANA, 0, //KeyEvent.VK_HIRAGANA,
0, //KeyEvent.VK_HENKAN, 0, //KeyEvent.VK_HENKAN,
0, //KeyEvent.VK_KATAKANAHIRAGANA, 0, //KeyEvent.VK_KATAKANAHIRAGANA,
0, //KeyEvent.VK_MUHENKAN, 0, //KeyEvent.VK_MUHENKAN,
0, //KeyEvent.VK_KPJPCOMMA, 0, //KeyEvent.VK_KPJPCOMMA,
KeyEvent.KEYCODE_NUMPAD_ENTER, KeyEvent.KEYCODE_NUMPAD_ENTER,
KeyEvent.KEYCODE_CTRL_RIGHT, KeyEvent.KEYCODE_CTRL_RIGHT,
KeyEvent.KEYCODE_NUMPAD_DIVIDE, KeyEvent.KEYCODE_NUMPAD_DIVIDE,
KeyEvent.KEYCODE_SYSRQ, KeyEvent.KEYCODE_SYSRQ,
KeyEvent.KEYCODE_ALT_RIGHT, KeyEvent.KEYCODE_ALT_RIGHT,
0, //KeyEvent.VK_LINEFEED, 0, //KeyEvent.VK_LINEFEED,
KeyEvent.KEYCODE_HOME, KeyEvent.KEYCODE_HOME,
KeyEvent.KEYCODE_DPAD_UP, KeyEvent.KEYCODE_DPAD_UP,
KeyEvent.KEYCODE_PAGE_UP, KeyEvent.KEYCODE_PAGE_UP,
KeyEvent.KEYCODE_DPAD_LEFT, KeyEvent.KEYCODE_DPAD_LEFT,
KeyEvent.KEYCODE_DPAD_RIGHT, KeyEvent.KEYCODE_DPAD_RIGHT,
KeyEvent.KEYCODE_MOVE_END, KeyEvent.KEYCODE_MOVE_END,
KeyEvent.KEYCODE_DPAD_DOWN, KeyEvent.KEYCODE_DPAD_DOWN,
KeyEvent.KEYCODE_PAGE_DOWN, KeyEvent.KEYCODE_PAGE_DOWN,
KeyEvent.KEYCODE_INSERT, KeyEvent.KEYCODE_INSERT,
KeyEvent.KEYCODE_FORWARD_DEL, KeyEvent.KEYCODE_FORWARD_DEL,
0, //KeyEvent.VK_MACRO, 0, //KeyEvent.VK_MACRO,
0, //KeyEvent.VK_MUTE, 0, //KeyEvent.VK_MUTE,
0, //KeyEvent.VK_VOLUMEDOWN, 0, //KeyEvent.VK_VOLUMEDOWN,
0, //KeyEvent.VK_VOLUMEUP, 0, //KeyEvent.VK_VOLUMEUP,
0, //KeyEvent.VK_POWER, /* SC System Power Down */ 0, //KeyEvent.VK_POWER, /* SC System Power Down */
KeyEvent.KEYCODE_NUMPAD_EQUALS, KeyEvent.KEYCODE_NUMPAD_EQUALS,
0, //KeyEvent.VK_KPPLUSMINUS, 0, //KeyEvent.VK_KPPLUSMINUS,
KeyEvent.KEYCODE_BREAK, KeyEvent.KEYCODE_BREAK,
0, //KeyEvent.VK_SCALE, /* AL Compiz Scale (Expose) */ 0, //KeyEvent.VK_SCALE, /* AL Compiz Scale (Expose) */
}; };
public static short translateEvdevKeyCode(short evdevKeyCode) { public static short translateEvdevKeyCode(short evdevKeyCode) {
if (evdevKeyCode < EVDEV_KEY_CODES.length) { if (evdevKeyCode < EVDEV_KEY_CODES.length) {
return EVDEV_KEY_CODES[evdevKeyCode]; return EVDEV_KEY_CODES[evdevKeyCode];
} }
return 0; return 0;
} }
} }
@@ -10,163 +10,179 @@ import android.os.FileObserver;
@SuppressWarnings("ALL") @SuppressWarnings("ALL")
public class EvdevWatcher { public class EvdevWatcher {
private static final String PATH = "/dev/input"; private static final String PATH = "/dev/input";
private static final String REQUIRED_FILE_PREFIX = "event"; private static final String REQUIRED_FILE_PREFIX = "event";
private final HashMap<String, EvdevHandler> handlers = new HashMap<String, EvdevHandler>(); private final HashMap<String, EvdevHandler> handlers = new HashMap<String, EvdevHandler>();
private boolean shutdown = false; private boolean shutdown = false;
private boolean init = false; private boolean init = false;
private boolean ungrabbed = false; private boolean ungrabbed = false;
private EvdevListener listener; private EvdevListener listener;
private Thread startThread; private Thread startThread;
private FileObserver observer = new FileObserver(PATH, FileObserver.CREATE | FileObserver.DELETE) { private static boolean patchedSeLinuxPolicies = false;
@Override
public void onEvent(int event, String fileName) {
if (fileName == null) {
return;
}
if (!fileName.startsWith(REQUIRED_FILE_PREFIX)) { private FileObserver observer = new FileObserver(PATH, FileObserver.CREATE | FileObserver.DELETE) {
return; @Override
} public void onEvent(int event, String fileName) {
if (fileName == null) {
return;
}
synchronized (handlers) { if (!fileName.startsWith(REQUIRED_FILE_PREFIX)) {
if (shutdown) { return;
return; }
}
if ((event & FileObserver.CREATE) != 0) { synchronized (handlers) {
LimeLog.info("Starting evdev handler for "+fileName); if (shutdown) {
return;
}
if (!init) { if ((event & FileObserver.CREATE) != 0) {
// If this a real new device, update permissions again so we can read it LimeLog.info("Starting evdev handler for "+fileName);
EvdevReader.setPermissions(new String[]{PATH + "/" + fileName}, 0666);
}
EvdevHandler handler = new EvdevHandler(PATH + "/" + fileName, listener); if (!init) {
// If this a real new device, update permissions again so we can read it
EvdevReader.setPermissions(new String[]{PATH + "/" + fileName}, 0666);
}
// If we're ungrabbed now, don't start the handler EvdevHandler handler = new EvdevHandler(PATH + "/" + fileName, listener);
if (!ungrabbed) {
handler.start();
}
handlers.put(fileName, handler); // If we're ungrabbed now, don't start the handler
} if (!ungrabbed) {
handler.start();
}
if ((event & FileObserver.DELETE) != 0) { handlers.put(fileName, handler);
LimeLog.info("Halting evdev handler for "+fileName); }
EvdevHandler handler = handlers.remove(fileName); if ((event & FileObserver.DELETE) != 0) {
if (handler != null) { LimeLog.info("Halting evdev handler for "+fileName);
handler.notifyDeleted();
}
}
}
}
};
public EvdevWatcher(EvdevListener listener) { EvdevHandler handler = handlers.remove(fileName);
this.listener = listener; if (handler != null) {
} handler.notifyDeleted();
}
}
}
}
};
private File[] rundownWithPermissionsChange(int newPermissions) { public EvdevWatcher(EvdevListener listener) {
// Rundown existing files this.listener = listener;
File devInputDir = new File(PATH); }
File[] files = devInputDir.listFiles();
if (files == null) {
return new File[0];
}
// Set desired permissions private File[] rundownWithPermissionsChange(int newPermissions) {
String[] filePaths = new String[files.length]; // Rundown existing files
for (int i = 0; i < files.length; i++) { File devInputDir = new File(PATH);
filePaths[i] = files[i].getAbsolutePath(); File[] files = devInputDir.listFiles();
} if (files == null) {
EvdevReader.setPermissions(filePaths, newPermissions); return new File[0];
}
return files; // 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);
public void ungrabAll() { return files;
synchronized (handlers) { }
// Note that we're ungrabbed for now
ungrabbed = true;
// Stop all handlers public void ungrabAll() {
for (EvdevHandler handler : handlers.values()) { synchronized (handlers) {
handler.stop(); // Note that we're ungrabbed for now
} ungrabbed = true;
}
}
public void regrabAll() { // Stop all handlers
synchronized (handlers) { for (EvdevHandler handler : handlers.values()) {
// We're regrabbing everything now handler.stop();
ungrabbed = false; }
}
}
for (Map.Entry<String, EvdevHandler> entry : handlers.entrySet()) { public void regrabAll() {
// We need to recreate each entry since we can't reuse a stopped one synchronized (handlers) {
entry.setValue(new EvdevHandler(PATH + "/" + entry.getKey(), listener)); // We're regrabbing everything now
entry.getValue().start(); ungrabbed = false;
}
}
}
public void start() { for (Map.Entry<String, EvdevHandler> entry : handlers.entrySet()) {
startThread = new Thread() { // We need to recreate each entry since we can't reuse a stopped one
@Override entry.setValue(new EvdevHandler(PATH + "/" + entry.getKey(), listener));
public void run() { entry.getValue().start();
// List all files and allow us access }
File[] files = rundownWithPermissionsChange(0666); }
}
init = true; public void start() {
for (File f : files) { startThread = new Thread() {
observer.onEvent(FileObserver.CREATE, f.getName()); @Override
} public void run() {
// Initialize the root shell
EvdevShell.getInstance().startShell();
// Done with initial onEvent calls // Patch SELinux policies (if needed)
init = false; if (!patchedSeLinuxPolicies) {
EvdevReader.patchSeLinuxPolicies();
patchedSeLinuxPolicies = true;
}
// Start watching for new files // List all files and allow us access
observer.startWatching(); File[] files = rundownWithPermissionsChange(0666);
synchronized (startThread) { init = true;
// Wait to be awoken again by shutdown() for (File f : files) {
try { observer.onEvent(FileObserver.CREATE, f.getName());
startThread.wait(); }
} catch (InterruptedException e) {}
}
// Giveup eventX permissions // Done with initial onEvent calls
rundownWithPermissionsChange(066); init = false;
}
};
startThread.start();
}
public void shutdown() { // Start watching for new files
// Let start thread cleanup on it's own sweet time observer.startWatching();
synchronized (startThread) {
startThread.notify();
}
// Stop the observer synchronized (startThread) {
observer.stopWatching(); // Wait to be awoken again by shutdown()
try {
startThread.wait();
} catch (InterruptedException e) {}
}
synchronized (handlers) { // Giveup eventX permissions
// Stop creating new handlers rundownWithPermissionsChange(0660);
shutdown = true;
// If we've already ungrabbed, there's nothing else to do // Kill the root shell
if (ungrabbed) { try {
return; EvdevShell.getInstance().stopShell();
} } catch (InterruptedException e) {}
}
};
startThread.start();
}
// Stop all handlers public void shutdown() {
for (EvdevHandler handler : handlers.values()) { // Let start thread cleanup on it's own sweet time
handler.stop(); 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();
}
}
}
} }
@@ -5,7 +5,6 @@ import java.io.File;
import java.io.FileReader; import java.io.FileReader;
import java.io.IOException; import java.io.IOException;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.util.concurrent.locks.LockSupport;
import android.graphics.PixelFormat; import android.graphics.PixelFormat;
import android.os.Build; import android.os.Build;
@@ -19,245 +18,270 @@ import com.limelight.nvstream.av.video.VideoDepacketizer;
import com.limelight.nvstream.av.video.cpu.AvcDecoder; import com.limelight.nvstream.av.video.cpu.AvcDecoder;
@SuppressWarnings("EmptyCatchBlock") @SuppressWarnings("EmptyCatchBlock")
public class AndroidCpuDecoderRenderer implements VideoDecoderRenderer { public class AndroidCpuDecoderRenderer extends EnhancedDecoderRenderer {
private Thread rendererThread; private Thread rendererThread, decoderThread;
private int targetFps; private int targetFps;
private static final int DECODER_BUFFER_SIZE = 92*1024; private static final int DECODER_BUFFER_SIZE = 92*1024;
private ByteBuffer decoderBuffer; private ByteBuffer decoderBuffer;
// Only sleep if the difference is above this value // Only sleep if the difference is above this value
private static final int WAIT_CEILING_MS = 8; private static final int WAIT_CEILING_MS = 5;
private static final int LOW_PERF = 1; private static final int LOW_PERF = 1;
private static final int MED_PERF = 2; private static final int MED_PERF = 2;
private static final int HIGH_PERF = 3; private static final int HIGH_PERF = 3;
private int totalFrames; private int totalFrames;
private long totalTimeMs; private long totalTimeMs;
private int cpuCount = Runtime.getRuntime().availableProcessors(); private final int cpuCount = Runtime.getRuntime().availableProcessors();
@SuppressWarnings("unused") @SuppressWarnings("unused")
private int findOptimalPerformanceLevel() { private int findOptimalPerformanceLevel() {
StringBuilder cpuInfo = new StringBuilder(); StringBuilder cpuInfo = new StringBuilder();
BufferedReader br = null; BufferedReader br = null;
try { try {
br = new BufferedReader(new FileReader(new File("/proc/cpuinfo"))); br = new BufferedReader(new FileReader(new File("/proc/cpuinfo")));
for (;;) { for (;;) {
int ch = br.read(); int ch = br.read();
if (ch == -1) if (ch == -1)
break; break;
cpuInfo.append((char)ch); cpuInfo.append((char)ch);
} }
// Here we're doing very simple heuristics based on CPU model // Here we're doing very simple heuristics based on CPU model
String cpuInfoStr = cpuInfo.toString(); String cpuInfoStr = cpuInfo.toString();
// We order them from greatest to least for proper detection // We order them from greatest to least for proper detection
// of devices with multiple sets of cores (like Exynos 5 Octa) // of devices with multiple sets of cores (like Exynos 5 Octa)
// TODO Make this better (only even kind of works on ARM) // TODO Make this better (only even kind of works on ARM)
if (Build.FINGERPRINT.contains("generic")) { if (Build.FINGERPRINT.contains("generic")) {
// Emulator // Emulator
return LOW_PERF; return LOW_PERF;
} }
else if (cpuInfoStr.contains("0xc0f")) { else if (cpuInfoStr.contains("0xc0f")) {
// Cortex-A15 // Cortex-A15
return MED_PERF; return MED_PERF;
} }
else if (cpuInfoStr.contains("0xc09")) { else if (cpuInfoStr.contains("0xc09")) {
// Cortex-A9 // Cortex-A9
return LOW_PERF; return LOW_PERF;
} }
else if (cpuInfoStr.contains("0xc07")) { else if (cpuInfoStr.contains("0xc07")) {
// Cortex-A7 // Cortex-A7
return LOW_PERF; return LOW_PERF;
} }
else { else {
// Didn't have anything we're looking for // Didn't have anything we're looking for
return MED_PERF; return MED_PERF;
} }
} catch (IOException e) { } catch (IOException e) {
} finally { } finally {
if (br != null) { if (br != null) {
try { try {
br.close(); br.close();
} catch (IOException e) {} } catch (IOException e) {}
} }
} }
// Couldn't read cpuinfo, so assume medium // Couldn't read cpuinfo, so assume medium
return MED_PERF; return MED_PERF;
} }
@Override @Override
public boolean setup(int width, int height, int redrawRate, Object renderTarget, int drFlags) { public boolean setup(int width, int height, int redrawRate, Object renderTarget, int drFlags) {
this.targetFps = redrawRate; this.targetFps = redrawRate;
int perfLevel = LOW_PERF; //findOptimalPerformanceLevel(); int perfLevel = LOW_PERF; //findOptimalPerformanceLevel();
int threadCount; int threadCount;
int avcFlags = 0; int avcFlags = 0;
switch (perfLevel) { switch (perfLevel) {
case HIGH_PERF: case HIGH_PERF:
// Single threaded low latency decode is ideal but hard to acheive // Single threaded low latency decode is ideal but hard to acheive
avcFlags = AvcDecoder.LOW_LATENCY_DECODE; avcFlags = AvcDecoder.LOW_LATENCY_DECODE;
threadCount = 1; threadCount = 1;
break; break;
case LOW_PERF: case LOW_PERF:
// Disable the loop filter for performance reasons // Disable the loop filter for performance reasons
avcFlags = AvcDecoder.DISABLE_LOOP_FILTER | avcFlags = AvcDecoder.FAST_BILINEAR_FILTERING;
AvcDecoder.FAST_BILINEAR_FILTERING |
AvcDecoder.FAST_DECODE;
// Use plenty of threads to try to utilize the CPU as best we can // Use plenty of threads to try to utilize the CPU as best we can
threadCount = cpuCount - 1; threadCount = cpuCount - 1;
break; break;
default: default:
case MED_PERF: case MED_PERF:
avcFlags = AvcDecoder.BILINEAR_FILTERING | avcFlags = AvcDecoder.BILINEAR_FILTERING;
AvcDecoder.FAST_DECODE;
// Only use 2 threads to minimize frame processing latency // Only use 2 threads to minimize frame processing latency
threadCount = 2; threadCount = 2;
break; break;
} }
// If the user wants quality, we'll remove the low IQ flags // If the user wants quality, we'll remove the low IQ flags
if ((drFlags & VideoDecoderRenderer.FLAG_PREFER_QUALITY) != 0) { if ((drFlags & VideoDecoderRenderer.FLAG_PREFER_QUALITY) != 0) {
// Make sure the loop filter is enabled // Make sure the loop filter is enabled
avcFlags &= ~AvcDecoder.DISABLE_LOOP_FILTER; avcFlags &= ~AvcDecoder.DISABLE_LOOP_FILTER;
// Disable the non-compliant speed optimizations // Disable the non-compliant speed optimizations
avcFlags &= ~AvcDecoder.FAST_DECODE; avcFlags &= ~AvcDecoder.FAST_DECODE;
LimeLog.info("Using high quality decoding"); LimeLog.info("Using high quality decoding");
} }
SurfaceHolder sh = (SurfaceHolder)renderTarget; SurfaceHolder sh = (SurfaceHolder)renderTarget;
sh.setFormat(PixelFormat.RGBX_8888); sh.setFormat(PixelFormat.RGBX_8888);
int err = AvcDecoder.init(width, height, avcFlags, threadCount); int err = AvcDecoder.init(width, height, avcFlags, threadCount);
if (err != 0) { if (err != 0) {
throw new IllegalStateException("AVC decoder initialization failure: "+err); throw new IllegalStateException("AVC decoder initialization failure: "+err);
} }
AvcDecoder.setRenderTarget(sh.getSurface()); if (!AvcDecoder.setRenderTarget(sh.getSurface())) {
return false;
}
decoderBuffer = ByteBuffer.allocate(DECODER_BUFFER_SIZE + AvcDecoder.getInputPaddingSize()); decoderBuffer = ByteBuffer.allocate(DECODER_BUFFER_SIZE + AvcDecoder.getInputPaddingSize());
LimeLog.info("Using software decoding (performance level: "+perfLevel+")"); LimeLog.info("Using software decoding (performance level: "+perfLevel+")");
return true; return true;
} }
@Override @Override
public boolean start(final VideoDepacketizer depacketizer) { public boolean start(final VideoDepacketizer depacketizer) {
rendererThread = new Thread() { decoderThread = new Thread() {
@Override @Override
public void run() { public void run() {
long nextFrameTime = System.currentTimeMillis(); DecodeUnit du;
DecodeUnit du; while (!isInterrupted()) {
while (!isInterrupted()) try {
{ du = depacketizer.takeNextDecodeUnit();
du = depacketizer.pollNextDecodeUnit(); } catch (InterruptedException e) {
if (du != null) { break;
submitDecodeUnit(du); }
depacketizer.freeDecodeUnit(du);
}
long diff = nextFrameTime - System.currentTimeMillis(); submitDecodeUnit(du);
depacketizer.freeDecodeUnit(du);
}
}
};
decoderThread.setName("Video - Decoder (CPU)");
decoderThread.setPriority(Thread.MAX_PRIORITY - 1);
decoderThread.start();
if (diff > WAIT_CEILING_MS) { rendererThread = new Thread() {
LockSupport.parkNanos(1); @Override
continue; public void run() {
} long nextFrameTime = System.currentTimeMillis();
while (!isInterrupted())
{
long diff = nextFrameTime - System.currentTimeMillis();
nextFrameTime = computePresentationTimeMs(targetFps); if (diff > WAIT_CEILING_MS) {
AvcDecoder.redraw(); try {
} Thread.sleep(diff - WAIT_CEILING_MS);
} } catch (InterruptedException e) {
}; return;
rendererThread.setName("Video - Renderer (CPU)"); }
rendererThread.setPriority(Thread.MAX_PRIORITY); continue;
rendererThread.start(); }
return true;
}
private long computePresentationTimeMs(int frameRate) { nextFrameTime = computePresentationTimeMs(targetFps);
return System.currentTimeMillis() + (1000 / frameRate); AvcDecoder.redraw();
} }
}
};
rendererThread.setName("Video - Renderer (CPU)");
rendererThread.setPriority(Thread.MAX_PRIORITY);
rendererThread.start();
return true;
}
@Override private long computePresentationTimeMs(int frameRate) {
public void stop() { return System.currentTimeMillis() + (1000 / frameRate);
rendererThread.interrupt(); }
try { @Override
rendererThread.join(); public void stop() {
} catch (InterruptedException e) { } rendererThread.interrupt();
} decoderThread.interrupt();
@Override try {
public void release() { rendererThread.join();
AvcDecoder.destroy(); } catch (InterruptedException e) { }
} try {
decoderThread.join();
} catch (InterruptedException e) { }
}
private boolean submitDecodeUnit(DecodeUnit decodeUnit) { @Override
byte[] data; public void release() {
AvcDecoder.destroy();
}
// Use the reserved decoder buffer if this decode unit will fit private boolean submitDecodeUnit(DecodeUnit decodeUnit) {
if (decodeUnit.getDataLength() <= DECODER_BUFFER_SIZE) { byte[] data;
decoderBuffer.clear();
for (ByteBufferDescriptor bbd : decodeUnit.getBufferList()) { // Use the reserved decoder buffer if this decode unit will fit
decoderBuffer.put(bbd.data, bbd.offset, bbd.length); if (decodeUnit.getDataLength() <= DECODER_BUFFER_SIZE) {
} decoderBuffer.clear();
data = decoderBuffer.array(); for (ByteBufferDescriptor bbd : decodeUnit.getBufferList()) {
} decoderBuffer.put(bbd.data, bbd.offset, bbd.length);
else { }
data = new byte[decodeUnit.getDataLength()+AvcDecoder.getInputPaddingSize()];
int offset = 0; data = decoderBuffer.array();
for (ByteBufferDescriptor bbd : decodeUnit.getBufferList()) { }
System.arraycopy(bbd.data, bbd.offset, data, offset, bbd.length); else {
offset += bbd.length; data = new byte[decodeUnit.getDataLength()+AvcDecoder.getInputPaddingSize()];
}
}
boolean success = (AvcDecoder.decode(data, 0, decodeUnit.getDataLength()) == 0); int offset = 0;
if (success) { for (ByteBufferDescriptor bbd : decodeUnit.getBufferList()) {
long timeAfterDecode = System.currentTimeMillis(); System.arraycopy(bbd.data, bbd.offset, data, offset, bbd.length);
offset += bbd.length;
}
}
// Add delta time to the totals (excluding probable outliers) boolean success = (AvcDecoder.decode(data, 0, decodeUnit.getDataLength()) == 0);
long delta = timeAfterDecode - decodeUnit.getReceiveTimestamp(); if (success) {
if (delta >= 0 && delta < 300) { long timeAfterDecode = System.currentTimeMillis();
totalTimeMs += delta;
totalFrames++;
}
}
return success; // Add delta time to the totals (excluding probable outliers)
} long delta = timeAfterDecode - decodeUnit.getReceiveTimestamp();
if (delta >= 0 && delta < 1000) {
totalTimeMs += delta;
totalFrames++;
}
}
@Override return success;
public int getCapabilities() { }
return 0;
}
@Override @Override
public int getAverageDecoderLatency() { public int getCapabilities() {
return 0; return 0;
} }
@Override @Override
public int getAverageEndToEndLatency() { public int getAverageDecoderLatency() {
if (totalFrames == 0) { return 0;
return 0; }
}
return (int)(totalTimeMs / totalFrames); @Override
} public int getAverageEndToEndLatency() {
if (totalFrames == 0) {
return 0;
}
return (int)(totalTimeMs / totalFrames);
}
@Override
public String getDecoderName() {
return "CPU decoding";
}
} }
@@ -3,75 +3,85 @@ package com.limelight.binding.video;
import com.limelight.nvstream.av.video.VideoDecoderRenderer; import com.limelight.nvstream.av.video.VideoDecoderRenderer;
import com.limelight.nvstream.av.video.VideoDepacketizer; import com.limelight.nvstream.av.video.VideoDepacketizer;
public class ConfigurableDecoderRenderer implements VideoDecoderRenderer { public class ConfigurableDecoderRenderer extends EnhancedDecoderRenderer {
private VideoDecoderRenderer decoderRenderer; private EnhancedDecoderRenderer decoderRenderer;
@Override @Override
public void release() { public void release() {
if (decoderRenderer != null) { if (decoderRenderer != null) {
decoderRenderer.release(); decoderRenderer.release();
} }
} }
@Override @Override
public boolean setup(int width, int height, int redrawRate, Object renderTarget, int drFlags) { public boolean setup(int width, int height, int redrawRate, Object renderTarget, int drFlags) {
if (decoderRenderer == null) { if (decoderRenderer == null) {
throw new IllegalStateException("ConfigurableDecoderRenderer not initialized"); throw new IllegalStateException("ConfigurableDecoderRenderer not initialized");
} }
return decoderRenderer.setup(width, height, redrawRate, renderTarget, drFlags); return decoderRenderer.setup(width, height, redrawRate, renderTarget, drFlags);
} }
public void initializeWithFlags(int drFlags) { public void initializeWithFlags(int drFlags) {
if ((drFlags & VideoDecoderRenderer.FLAG_FORCE_HARDWARE_DECODING) != 0 || if ((drFlags & VideoDecoderRenderer.FLAG_FORCE_HARDWARE_DECODING) != 0 ||
((drFlags & VideoDecoderRenderer.FLAG_FORCE_SOFTWARE_DECODING) == 0 && ((drFlags & VideoDecoderRenderer.FLAG_FORCE_SOFTWARE_DECODING) == 0 &&
MediaCodecHelper.findProbableSafeDecoder() != null)) { MediaCodecHelper.findProbableSafeDecoder() != null)) {
decoderRenderer = new MediaCodecDecoderRenderer(); decoderRenderer = new MediaCodecDecoderRenderer();
} }
else { else {
decoderRenderer = new AndroidCpuDecoderRenderer(); decoderRenderer = new AndroidCpuDecoderRenderer();
} }
} }
public boolean isHardwareAccelerated() { public boolean isHardwareAccelerated() {
if (decoderRenderer == null) { if (decoderRenderer == null) {
throw new IllegalStateException("ConfigurableDecoderRenderer not initialized"); throw new IllegalStateException("ConfigurableDecoderRenderer not initialized");
} }
return (decoderRenderer instanceof MediaCodecDecoderRenderer); return (decoderRenderer instanceof MediaCodecDecoderRenderer);
} }
@Override @Override
public boolean start(VideoDepacketizer depacketizer) { public boolean start(VideoDepacketizer depacketizer) {
return decoderRenderer.start(depacketizer); return decoderRenderer.start(depacketizer);
} }
@Override @Override
public void stop() { public void stop() {
decoderRenderer.stop(); decoderRenderer.stop();
} }
@Override @Override
public int getCapabilities() { public int getCapabilities() {
return decoderRenderer.getCapabilities(); return decoderRenderer.getCapabilities();
} }
@Override @Override
public int getAverageDecoderLatency() { public int getAverageDecoderLatency() {
if (decoderRenderer != null) { if (decoderRenderer != null) {
return decoderRenderer.getAverageDecoderLatency(); return decoderRenderer.getAverageDecoderLatency();
} }
else { else {
return 0; return 0;
} }
} }
@Override @Override
public int getAverageEndToEndLatency() { public int getAverageEndToEndLatency() {
if (decoderRenderer != null) { if (decoderRenderer != null) {
return decoderRenderer.getAverageEndToEndLatency(); return decoderRenderer.getAverageEndToEndLatency();
} }
else { else {
return 0; return 0;
} }
} }
@Override
public String getDecoderName() {
if (decoderRenderer != null) {
return decoderRenderer.getDecoderName();
}
else {
return null;
}
}
} }
@@ -0,0 +1,7 @@
package com.limelight.binding.video;
import com.limelight.nvstream.av.video.VideoDecoderRenderer;
public abstract class EnhancedDecoderRenderer implements VideoDecoderRenderer {
public abstract String getDecoderName();
}
File diff suppressed because it is too large Load Diff
@@ -20,11 +20,12 @@ import com.limelight.LimeLog;
public class MediaCodecHelper { public class MediaCodecHelper {
public static final List<String> preferredDecoders; private static final List<String> preferredDecoders;
public static final List<String> blacklistedDecoderPrefixes; private static final List<String> blacklistedDecoderPrefixes;
public static final List<String> spsFixupBitstreamFixupDecoderPrefixes; private static final List<String> spsFixupBitstreamFixupDecoderPrefixes;
public static final List<String> whitelistedAdaptiveResolutionPrefixes; private static final List<String> whitelistedAdaptiveResolutionPrefixes;
private static final List<String> baselineProfileHackPrefixes;
static { static {
preferredDecoders = new LinkedList<String>(); preferredDecoders = new LinkedList<String>();
@@ -43,6 +44,10 @@ public class MediaCodecHelper {
spsFixupBitstreamFixupDecoderPrefixes.add("omx.nvidia"); spsFixupBitstreamFixupDecoderPrefixes.add("omx.nvidia");
spsFixupBitstreamFixupDecoderPrefixes.add("omx.qcom"); spsFixupBitstreamFixupDecoderPrefixes.add("omx.qcom");
spsFixupBitstreamFixupDecoderPrefixes.add("omx.mtk"); spsFixupBitstreamFixupDecoderPrefixes.add("omx.mtk");
spsFixupBitstreamFixupDecoderPrefixes.add("omx.brcm");
baselineProfileHackPrefixes = new LinkedList<String>();
baselineProfileHackPrefixes.add("omx.intel");
whitelistedAdaptiveResolutionPrefixes = new LinkedList<String>(); whitelistedAdaptiveResolutionPrefixes = new LinkedList<String>();
whitelistedAdaptiveResolutionPrefixes.add("omx.nvidia"); whitelistedAdaptiveResolutionPrefixes.add("omx.nvidia");
@@ -66,6 +71,10 @@ public class MediaCodecHelper {
@TargetApi(Build.VERSION_CODES.KITKAT) @TargetApi(Build.VERSION_CODES.KITKAT)
public static boolean decoderSupportsAdaptivePlayback(String decoderName, MediaCodecInfo decoderInfo) { 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)) { if (isDecoderInList(whitelistedAdaptiveResolutionPrefixes, decoderName)) {
LimeLog.info("Adaptive playback supported (whitelist)"); LimeLog.info("Adaptive playback supported (whitelist)");
return true; return true;
@@ -84,7 +93,7 @@ public class MediaCodecHelper {
} catch (Exception e) { } catch (Exception e) {
// Tolerate buggy codecs // Tolerate buggy codecs
} }
} }*/
return false; return false;
} }
@@ -93,6 +102,10 @@ public class MediaCodecHelper {
return isDecoderInList(spsFixupBitstreamFixupDecoderPrefixes, decoderName); return isDecoderInList(spsFixupBitstreamFixupDecoderPrefixes, decoderName);
} }
public static boolean decoderNeedsBaselineSpsHack(String decoderName, MediaCodecInfo decoderInfo) {
return isDecoderInList(baselineProfileHackPrefixes, decoderName);
}
@SuppressWarnings("deprecation") @SuppressWarnings("deprecation")
@SuppressLint("NewApi") @SuppressLint("NewApi")
private static LinkedList<MediaCodecInfo> getMediaCodecList() { private static LinkedList<MediaCodecInfo> getMediaCodecList() {
@@ -133,7 +146,7 @@ public class MediaCodecHelper {
return str; return str;
} }
public static MediaCodecInfo findPreferredDecoder() { private static MediaCodecInfo findPreferredDecoder() {
// This is a different algorithm than the other findXXXDecoder functions, // This is a different algorithm than the other findXXXDecoder functions,
// because we want to evaluate the decoders in our list's order // because we want to evaluate the decoders in our list's order
// rather than MediaCodecList's order // rather than MediaCodecList's order
@@ -204,7 +217,7 @@ public class MediaCodecHelper {
// since some bad decoders can throw IllegalArgumentExceptions unexpectedly // since some bad decoders can throw IllegalArgumentExceptions unexpectedly
// and we want to be sure all callers are handling this possibility // and we want to be sure all callers are handling this possibility
@SuppressWarnings("RedundantThrows") @SuppressWarnings("RedundantThrows")
public static MediaCodecInfo findKnownSafeDecoder() throws Exception { private static MediaCodecInfo findKnownSafeDecoder() throws Exception {
for (MediaCodecInfo codecInfo : getMediaCodecList()) { for (MediaCodecInfo codecInfo : getMediaCodecList()) {
// Skip encoders // Skip encoders
if (codecInfo.isEncoder()) { if (codecInfo.isEncoder()) {
@@ -17,148 +17,153 @@ import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteException; import android.database.sqlite.SQLiteException;
public class ComputerDatabaseManager { public class ComputerDatabaseManager {
private static final String COMPUTER_DB_NAME = "computers.db"; private static final String COMPUTER_DB_NAME = "computers.db";
private static final String COMPUTER_TABLE_NAME = "Computers"; private static final String COMPUTER_TABLE_NAME = "Computers";
private static final String COMPUTER_NAME_COLUMN_NAME = "ComputerName"; private static final String COMPUTER_NAME_COLUMN_NAME = "ComputerName";
private static final String COMPUTER_UUID_COLUMN_NAME = "UUID"; private static final String COMPUTER_UUID_COLUMN_NAME = "UUID";
private static final String LOCAL_IP_COLUMN_NAME = "LocalIp"; private static final String LOCAL_IP_COLUMN_NAME = "LocalIp";
private static final String REMOTE_IP_COLUMN_NAME = "RemoteIp"; private static final String REMOTE_IP_COLUMN_NAME = "RemoteIp";
private static final String MAC_COLUMN_NAME = "Mac"; private static final String MAC_COLUMN_NAME = "Mac";
private SQLiteDatabase computerDb; private SQLiteDatabase computerDb;
public ComputerDatabaseManager(Context c) { public ComputerDatabaseManager(Context c) {
try { try {
// Create or open an existing DB // Create or open an existing DB
computerDb = c.openOrCreateDatabase(COMPUTER_DB_NAME, 0, null); computerDb = c.openOrCreateDatabase(COMPUTER_DB_NAME, 0, null);
} catch (SQLiteException e) { } catch (SQLiteException e) {
// Delete the DB and try again // Delete the DB and try again
c.deleteDatabase(COMPUTER_DB_NAME); c.deleteDatabase(COMPUTER_DB_NAME);
computerDb = c.openOrCreateDatabase(COMPUTER_DB_NAME, 0, null); computerDb = c.openOrCreateDatabase(COMPUTER_DB_NAME, 0, null);
} }
initializeDb(); initializeDb();
} }
public void close() { public void close() {
computerDb.close(); computerDb.close();
} }
private void initializeDb() { private void initializeDb() {
// Create tables if they aren't already there // Create tables if they aren't already there
computerDb.execSQL(String.format((Locale)null, "CREATE TABLE IF NOT EXISTS %s(%s TEXT PRIMARY KEY," + 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)", " %s TEXT NOT NULL, %s TEXT NOT NULL, %s TEXT NOT NULL, %s TEXT NOT NULL)",
COMPUTER_TABLE_NAME, COMPUTER_TABLE_NAME,
COMPUTER_NAME_COLUMN_NAME, COMPUTER_UUID_COLUMN_NAME, LOCAL_IP_COLUMN_NAME, COMPUTER_NAME_COLUMN_NAME, COMPUTER_UUID_COLUMN_NAME, LOCAL_IP_COLUMN_NAME,
REMOTE_IP_COLUMN_NAME, MAC_COLUMN_NAME)); REMOTE_IP_COLUMN_NAME, MAC_COLUMN_NAME));
} }
public void deleteComputer(String name) { public void deleteComputer(String name) {
computerDb.delete(COMPUTER_TABLE_NAME, COMPUTER_NAME_COLUMN_NAME+"='"+name+"'", null); computerDb.delete(COMPUTER_TABLE_NAME, COMPUTER_NAME_COLUMN_NAME+"='"+name+"'", null);
} }
public boolean updateComputer(ComputerDetails details) { public boolean updateComputer(ComputerDetails details) {
ContentValues values = new ContentValues(); ContentValues values = new ContentValues();
values.put(COMPUTER_NAME_COLUMN_NAME, details.name); values.put(COMPUTER_NAME_COLUMN_NAME, details.name);
values.put(COMPUTER_UUID_COLUMN_NAME, details.uuid.toString()); values.put(COMPUTER_UUID_COLUMN_NAME, details.uuid.toString());
values.put(LOCAL_IP_COLUMN_NAME, details.localIp.getAddress()); values.put(LOCAL_IP_COLUMN_NAME, details.localIp.getAddress());
values.put(REMOTE_IP_COLUMN_NAME, details.remoteIp.getAddress()); values.put(REMOTE_IP_COLUMN_NAME, details.remoteIp.getAddress());
values.put(MAC_COLUMN_NAME, details.macAddress); values.put(MAC_COLUMN_NAME, details.macAddress);
return -1 != computerDb.insertWithOnConflict(COMPUTER_TABLE_NAME, null, values, SQLiteDatabase.CONFLICT_REPLACE); return -1 != computerDb.insertWithOnConflict(COMPUTER_TABLE_NAME, null, values, SQLiteDatabase.CONFLICT_REPLACE);
} }
public List<ComputerDetails> getAllComputers() { public List<ComputerDetails> getAllComputers() {
Cursor c = computerDb.rawQuery("SELECT * FROM "+COMPUTER_TABLE_NAME, null); Cursor c = computerDb.rawQuery("SELECT * FROM "+COMPUTER_TABLE_NAME, null);
LinkedList<ComputerDetails> computerList = new LinkedList<ComputerDetails>(); LinkedList<ComputerDetails> computerList = new LinkedList<ComputerDetails>();
while (c.moveToNext()) { while (c.moveToNext()) {
ComputerDetails details = new ComputerDetails(); ComputerDetails details = new ComputerDetails();
details.name = c.getString(0); details.name = c.getString(0);
String uuidStr = c.getString(1); String uuidStr = c.getString(1);
try { try {
details.uuid = UUID.fromString(uuidStr); details.uuid = UUID.fromString(uuidStr);
} catch (IllegalArgumentException e) { } catch (IllegalArgumentException e) {
// We'll delete this entry // We'll delete this entry
LimeLog.severe("DB: Corrupted UUID for "+details.name); LimeLog.severe("DB: Corrupted UUID for "+details.name);
} }
try { try {
details.localIp = InetAddress.getByAddress(c.getBlob(2)); details.localIp = InetAddress.getByAddress(c.getBlob(2));
} catch (UnknownHostException e) { } catch (UnknownHostException e) {
// We'll delete this entry // We'll delete this entry
LimeLog.severe("DB: Corrupted local IP for "+details.name); LimeLog.severe("DB: Corrupted local IP for "+details.name);
} }
try { try {
details.remoteIp = InetAddress.getByAddress(c.getBlob(3)); details.remoteIp = InetAddress.getByAddress(c.getBlob(3));
} catch (UnknownHostException e) { } catch (UnknownHostException e) {
// We'll delete this entry // We'll delete this entry
LimeLog.severe("DB: Corrupted remote IP for "+details.name); LimeLog.severe("DB: Corrupted remote IP for "+details.name);
} }
details.macAddress = c.getString(4); details.macAddress = c.getString(4);
// This signifies we don't have dynamic state (like pair state) // This signifies we don't have dynamic state (like pair state)
details.state = ComputerDetails.State.UNKNOWN; details.state = ComputerDetails.State.UNKNOWN;
details.reachability = ComputerDetails.Reachability.UNKNOWN;
// If a field is corrupt or missing, skip the database entry // If a field is corrupt or missing, skip the database entry
if (details.uuid == null || details.localIp == null || details.remoteIp == null || if (details.uuid == null || details.localIp == null || details.remoteIp == null ||
details.macAddress == null) { details.macAddress == null) {
continue; continue;
} }
computerList.add(details);
}
c.close(); computerList.add(details);
}
return computerList; c.close();
}
public ComputerDetails getComputerByName(String name) { return computerList;
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); 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;
}
String uuidStr = c.getString(1); details.name = c.getString(0);
try {
details.uuid = UUID.fromString(uuidStr);
} catch (IllegalArgumentException e) {
// We'll delete this entry
LimeLog.severe("DB: Corrupted UUID for "+details.name);
}
try { String uuidStr = c.getString(1);
details.localIp = InetAddress.getByAddress(c.getBlob(2)); try {
} catch (UnknownHostException e) { details.uuid = UUID.fromString(uuidStr);
// We'll delete this entry } catch (IllegalArgumentException e) {
LimeLog.severe("DB: Corrupted local IP for "+details.name); // We'll delete this entry
} LimeLog.severe("DB: Corrupted UUID for "+details.name);
}
try { try {
details.remoteIp = InetAddress.getByAddress(c.getBlob(3)); details.localIp = InetAddress.getByAddress(c.getBlob(2));
} catch (UnknownHostException e) { } catch (UnknownHostException e) {
// We'll delete this entry // We'll delete this entry
LimeLog.severe("DB: Corrupted remote IP for "+details.name); LimeLog.severe("DB: Corrupted local IP for "+details.name);
} }
details.macAddress = c.getString(4); 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);
}
c.close(); details.macAddress = c.getString(4);
// If a field is corrupt or missing, delete the database entry c.close();
if (details.uuid == null || details.localIp == null || details.remoteIp == null ||
details.macAddress == null) {
deleteComputer(details.name);
return null;
}
return details; 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;
}
} }
@@ -3,5 +3,5 @@ package com.limelight.computers;
import com.limelight.nvstream.http.ComputerDetails; import com.limelight.nvstream.http.ComputerDetails;
public interface ComputerManagerListener { public interface ComputerManagerListener {
public void notifyComputerUpdated(ComputerDetails details); public void notifyComputerUpdated(ComputerDetails details);
} }
@@ -1,22 +1,25 @@
package com.limelight.computers; package com.limelight.computers;
import java.io.IOException;
import java.io.OutputStream;
import java.io.StringReader;
import java.net.InetAddress; import java.net.InetAddress;
import java.util.HashMap; import java.net.InetSocketAddress;
import java.net.Socket;
import java.util.LinkedList;
import java.util.List; import java.util.List;
import java.util.Timer; import java.util.UUID;
import java.util.TimerTask;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicInteger;
import com.limelight.LimeLog; import com.limelight.LimeLog;
import com.limelight.binding.PlatformBinding; import com.limelight.binding.PlatformBinding;
import com.limelight.discovery.DiscoveryService; import com.limelight.discovery.DiscoveryService;
import com.limelight.nvstream.http.ComputerDetails; import com.limelight.nvstream.http.ComputerDetails;
import com.limelight.nvstream.http.NvApp;
import com.limelight.nvstream.http.NvHTTP; import com.limelight.nvstream.http.NvHTTP;
import com.limelight.nvstream.mdns.MdnsComputer; import com.limelight.nvstream.mdns.MdnsComputer;
import com.limelight.nvstream.mdns.MdnsDiscoveryListener; import com.limelight.nvstream.mdns.MdnsDiscoveryListener;
import com.limelight.utils.CacheHelper;
import android.app.Service; import android.app.Service;
import android.content.ComponentName; import android.content.ComponentName;
@@ -25,45 +28,47 @@ import android.content.ServiceConnection;
import android.os.Binder; import android.os.Binder;
import android.os.IBinder; import android.os.IBinder;
import org.xmlpull.v1.XmlPullParserException;
public class ComputerManagerService extends Service { public class ComputerManagerService extends Service {
private static final int POLLING_PERIOD_MS = 5000; private static final int POLLING_PERIOD_MS = 3000;
private static final int MDNS_QUERY_PERIOD_MS = 1000; 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 ComputerManagerBinder binder = new ComputerManagerBinder(); private final ComputerManagerBinder binder = new ComputerManagerBinder();
private ComputerDatabaseManager dbManager; private ComputerDatabaseManager dbManager;
private AtomicInteger dbRefCount = new AtomicInteger(0); private final AtomicInteger dbRefCount = new AtomicInteger(0);
private IdentityManager idManager; private IdentityManager idManager;
private HashMap<ComputerDetails, Thread> pollingThreads; private final LinkedList<PollingTuple> pollingTuples = new LinkedList<PollingTuple>();
private ComputerManagerListener listener = null; private ComputerManagerListener listener = null;
private AtomicInteger activePolls = new AtomicInteger(0); private final AtomicInteger activePolls = new AtomicInteger(0);
private boolean pollingActive = false;
private DiscoveryService.DiscoveryBinder discoveryBinder; private DiscoveryService.DiscoveryBinder discoveryBinder;
private final ServiceConnection discoveryServiceConnection = new ServiceConnection() { private final ServiceConnection discoveryServiceConnection = new ServiceConnection() {
public void onServiceConnected(ComponentName className, IBinder binder) { public void onServiceConnected(ComponentName className, IBinder binder) {
synchronized (discoveryServiceConnection) { synchronized (discoveryServiceConnection) {
DiscoveryService.DiscoveryBinder privateBinder = ((DiscoveryService.DiscoveryBinder)binder); DiscoveryService.DiscoveryBinder privateBinder = ((DiscoveryService.DiscoveryBinder)binder);
// Set us as the event listener // Set us as the event listener
privateBinder.setListener(createDiscoveryListener()); privateBinder.setListener(createDiscoveryListener());
// Signal a possible waiter that we're all setup // Signal a possible waiter that we're all setup
discoveryBinder = privateBinder; discoveryBinder = privateBinder;
discoveryServiceConnection.notifyAll(); discoveryServiceConnection.notifyAll();
} }
} }
public void onServiceDisconnected(ComponentName className) { public void onServiceDisconnected(ComponentName className) {
discoveryBinder = null; discoveryBinder = null;
} }
}; };
// Returns true if the details object was modified // Returns true if the details object was modified
private boolean runPoll(ComputerDetails details) private boolean runPoll(ComputerDetails details, boolean newPc, int offlineCount) throws InterruptedException {
{
boolean newPc = (details.name == null);
if (!getLocalDatabaseReference()) { if (!getLocalDatabaseReference()) {
return false; return false;
} }
@@ -71,12 +76,22 @@ public class ComputerManagerService extends Service {
activePolls.incrementAndGet(); activePolls.incrementAndGet();
// Poll the machine // Poll the machine
if (!doPollMachine(details)) { try {
details.state = ComputerDetails.State.OFFLINE; if (!pollComputer(details)) {
details.reachability = ComputerDetails.Reachability.OFFLINE; if (!newPc && offlineCount < OFFLINE_POLL_TRIES) {
} // Return without calling the listener
return false;
}
activePolls.decrementAndGet(); 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 it's online, update our persistent state
if (details.state == ComputerDetails.State.ONLINE) { if (details.state == ComputerDetails.State.ONLINE) {
@@ -86,7 +101,7 @@ public class ComputerManagerService extends Service {
if (dbManager.getComputerByName(details.name) == null) { if (dbManager.getComputerByName(details.name) == null) {
// It's gone // It's gone
releaseLocalDatabaseReference(); releaseLocalDatabaseReference();
return true; return false;
} }
} }
@@ -106,22 +121,21 @@ public class ComputerManagerService extends Service {
Thread t = new Thread() { Thread t = new Thread() {
@Override @Override
public void run() { public void run() {
while (!isInterrupted()) {
ComputerDetails originalDetails = new ComputerDetails();
originalDetails.update(details);
// Check if this poll has modified the details int offlineCount = 0;
if (runPoll(details) && !originalDetails.equals(details)) { while (!isInterrupted() && pollingActive) {
// Replace our thread entry with the new one
synchronized (pollingThreads) {
pollingThreads.remove(originalDetails);
pollingThreads.put(details, this);
}
}
// Wait until the next polling interval
try { try {
Thread.sleep(POLLING_PERIOD_MS); // 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(POLLING_PERIOD_MS / ((offlineCount > 0) ? 2 : 1));
} catch (InterruptedException e) { } catch (InterruptedException e) {
break; break;
} }
@@ -132,257 +146,541 @@ public class ComputerManagerService extends Service {
return t; return t;
} }
public class ComputerManagerBinder extends Binder { public class ComputerManagerBinder extends Binder {
public void startPolling(ComputerManagerListener listener) { public void startPolling(ComputerManagerListener listener) {
// Set the listener // Polling is active
ComputerManagerService.this.listener = listener; pollingActive = true;
// Start mDNS autodiscovery too // Set the listener
discoveryBinder.startDiscovery(MDNS_QUERY_PERIOD_MS); ComputerManagerService.this.listener = listener;
// Start polling known machines // Start mDNS autodiscovery too
if (!getLocalDatabaseReference()) { discoveryBinder.startDiscovery(MDNS_QUERY_PERIOD_MS);
return;
}
List<ComputerDetails> computerList = dbManager.getAllComputers();
releaseLocalDatabaseReference();
synchronized (pollingThreads) { synchronized (pollingTuples) {
for (ComputerDetails computer : computerList) { for (PollingTuple tuple : pollingTuples) {
// This polling thread might already be there // This polling thread might already be there
if (!pollingThreads.containsKey(computer)) { if (tuple.thread == null) {
Thread t = createPollingThread(computer); // Report this computer initially
pollingThreads.put(computer, t); listener.notifyComputerUpdated(tuple.computer);
t.start();
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 addComputer(InetAddress addr) {
ComputerManagerService.this.addComputer(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 String getUniqueId() {
return idManager.getUniqueId();
}
}
@Override
public boolean onUnbind(Intent intent) {
// Stop mDNS autodiscovery
discoveryBinder.stopDiscovery();
// Stop polling
synchronized (pollingThreads) {
for (Thread t : pollingThreads.values()) {
t.interrupt();
}
pollingThreads.clear();
} }
// Remove the listener public void waitForReady() {
listener = null; synchronized (discoveryServiceConnection) {
try {
return false; while (discoveryBinder == null) {
} // Wait for the bind notification
discoveryServiceConnection.wait(1000);
private MdnsDiscoveryListener createDiscoveryListener() { }
return new MdnsDiscoveryListener() { } catch (InterruptedException ignored) {
@Override }
public void notifyComputerAdded(MdnsComputer computer) {
// Kick off a serverinfo poll on this machine
addComputer(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();
}
};
}
public void addComputer(InetAddress addr) {
// Setup a placeholder
ComputerDetails fakeDetails = new ComputerDetails();
fakeDetails.localIp = addr;
fakeDetails.remoteIp = addr;
// Spawn a thread for this computer
synchronized (pollingThreads) {
// This polling thread might already be there
if (!pollingThreads.containsKey(fakeDetails)) {
Thread t = createPollingThread(fakeDetails);
pollingThreads.put(fakeDetails, t);
t.start();
} }
} }
}
public boolean addComputerBlocking(InetAddress addr) { public void waitForPollingStopped() {
// Setup a placeholder while (activePolls.get() != 0) {
ComputerDetails fakeDetails = new ComputerDetails(); try {
fakeDetails.localIp = addr; Thread.sleep(250);
fakeDetails.remoteIp = addr; } catch (InterruptedException ignored) {}
}
}
// Block while we try to fill the details public boolean addComputerBlocking(InetAddress addr) {
runPoll(fakeDetails); return ComputerManagerService.this.addComputerBlocking(addr);
}
// If the machine is reachable, it was successful public void removeComputer(String name) {
return fakeDetails.state == ComputerDetails.State.ONLINE; ComputerManagerService.this.removeComputer(name);
} }
public void removeComputer(String name) { public void stopPolling() {
if (!getLocalDatabaseReference()) { // Just call the unbind handler to cleanup
return; ComputerManagerService.this.onUnbind(null);
} }
// Remove it from the database public ApplistPoller createAppListPoller(ComputerDetails computer) {
dbManager.deleteComputer(name); return new ApplistPoller(computer);
}
releaseLocalDatabaseReference(); public String getUniqueId() {
} return idManager.getUniqueId();
}
private boolean getLocalDatabaseReference() { public ComputerDetails getComputer(UUID uuid) {
if (dbRefCount.get() == 0) { synchronized (pollingTuples) {
return false; for (PollingTuple tuple : pollingTuples) {
} if (uuid.equals(tuple.computer.uuid)) {
return tuple.computer;
}
}
}
dbRefCount.incrementAndGet(); return null;
return true; }
} }
private void releaseLocalDatabaseReference() { @Override
if (dbRefCount.decrementAndGet() == 0) { public boolean onUnbind(Intent intent) {
dbManager.close(); // Stop mDNS autodiscovery
} discoveryBinder.stopDiscovery();
}
private ComputerDetails tryPollIp(InetAddress ipAddr) { // Stop polling
try { pollingActive = false;
NvHTTP http = new NvHTTP(ipAddr, idManager.getUniqueId(), synchronized (pollingTuples) {
null, PlatformBinding.getCryptoProvider(ComputerManagerService.this)); for (PollingTuple tuple : pollingTuples) {
if (tuple.thread != null) {
// Interrupt and remove the thread
tuple.thread.interrupt();
tuple.thread = null;
}
}
}
return http.getComputerDetails(); // Remove the listener
} catch (Exception e) { listener = null;
return null;
}
}
private boolean pollComputer(ComputerDetails details, boolean localFirst) { return false;
ComputerDetails polledDetails; }
if (localFirst) { private MdnsDiscoveryListener createDiscoveryListener() {
polledDetails = tryPollIp(details.localIp); return new MdnsDiscoveryListener() {
} @Override
else { public void notifyComputerAdded(MdnsComputer computer) {
polledDetails = tryPollIp(details.remoteIp); // Kick off a serverinfo poll on this machine
} addComputerBlocking(computer.getAddress());
}
if (polledDetails == null && !details.localIp.equals(details.remoteIp)) { @Override
// Failed, so let's try the fallback public void notifyComputerRemoved(MdnsComputer computer) {
if (!localFirst) { // Nothing to do here
polledDetails = tryPollIp(details.localIp); }
}
else {
polledDetails = tryPollIp(details.remoteIp);
}
// The fallback poll worked @Override
if (polledDetails != null) { public void notifyDiscoveryFailure(Exception e) {
polledDetails.reachability = !localFirst ? ComputerDetails.Reachability.LOCAL : LimeLog.severe("mDNS discovery failed");
ComputerDetails.Reachability.REMOTE; e.printStackTrace();
} }
} };
else if (polledDetails != null) { }
polledDetails.reachability = localFirst ? ComputerDetails.Reachability.LOCAL :
ComputerDetails.Reachability.REMOTE;
}
// Machine was unreachable both tries private void addTuple(ComputerDetails details) {
if (polledDetails == null) { synchronized (pollingTuples) {
return false; 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;
// If we got here, it's reachable // Start a polling thread if polling is active
details.update(polledDetails); if (pollingActive && tuple.thread == null) {
return true; tuple.thread = createPollingThread(details);
} tuple.thread.start();
}
private boolean doPollMachine(ComputerDetails details) { // Found an entry so we're done
return pollComputer(details, true); return;
} }
}
@Override // If we got here, we didn't find an entry
public void onCreate() { PollingTuple tuple = new PollingTuple(details, pollingActive ? createPollingThread(details) : null);
// Bind to the discovery service pollingTuples.add(tuple);
bindService(new Intent(this, DiscoveryService.class), if (tuple.thread != null) {
discoveryServiceConnection, Service.BIND_AUTO_CREATE); tuple.thread.start();
}
}
}
pollingThreads = new HashMap<ComputerDetails, Thread>(); public boolean addComputerBlocking(InetAddress addr) {
// Setup a placeholder
ComputerDetails fakeDetails = new ComputerDetails();
fakeDetails.localIp = addr;
fakeDetails.remoteIp = addr;
// Lookup or generate this device's UID // Block while we try to fill the details
idManager = new IdentityManager(this); try {
runPoll(fakeDetails, true, 0);
} catch (InterruptedException e) {
return false;
}
// Initialize the DB // If the machine is reachable, it was successful
dbManager = new ComputerDatabaseManager(this); if (fakeDetails.state == ComputerDetails.State.ONLINE) {
dbRefCount.set(1); LimeLog.info("New PC ("+fakeDetails.name+") is UUID "+fakeDetails.uuid);
}
@Override // Start a polling thread for this machine
public void onDestroy() { addTuple(fakeDetails);
if (discoveryBinder != null) { return true;
// Unbind from the discovery service }
unbindService(discoveryServiceConnection); else {
} return false;
}
}
// FIXME: Should await termination here but we have timeout issues in HttpURLConnection public void removeComputer(String name) {
if (!getLocalDatabaseReference()) {
return;
}
// Remove the initial DB reference // Remove it from the database
releaseLocalDatabaseReference(); dbManager.deleteComputer(name);
}
@Override synchronized (pollingTuples) {
public IBinder onBind(Intent intent) { // Remove the computer from the computer list
return binder; 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) {
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.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 boolean pollComputer(ComputerDetails details) throws InterruptedException {
ComputerDetails polledDetails;
ComputerDetails.Reachability reachability;
// If the local address is routable across the Internet,
// always consider this PC remote to be conservative
if (details.localIp.equals(details.remoteIp)) {
reachability = ComputerDetails.Reachability.REMOTE;
}
else {
// Do a TCP-level connection to the HTTP server to see if it's listening
LimeLog.info("Starting fast poll for "+details.name+" ("+details.localIp+", "+details.remoteIp+")");
reachability = fastPollPc(details.localIp, details.remoteIp);
LimeLog.info("Fast poll for "+details.name+" returned "+reachability.toString());
}
// If no connection could be established to either IP address, there's nothing we can do
if (reachability == ComputerDetails.Reachability.OFFLINE) {
return false;
}
boolean localFirst = (reachability == ComputerDetails.Reachability.LOCAL);
if (localFirst) {
polledDetails = tryPollIp(details, details.localIp);
}
else {
polledDetails = tryPollIp(details, details.remoteIp);
}
if (polledDetails == null && !details.localIp.equals(details.remoteIp)) {
// Failed, so let's try the fallback
if (!localFirst) {
polledDetails = tryPollIp(details, details.localIp);
}
else {
polledDetails = tryPollIp(details, details.remoteIp);
}
// The fallback poll worked
if (polledDetails != null) {
polledDetails.reachability = !localFirst ? ComputerDetails.Reachability.LOCAL :
ComputerDetails.Reachability.REMOTE;
}
}
else if (polledDetails != null) {
polledDetails.reachability = localFirst ? ComputerDetails.Reachability.LOCAL :
ComputerDetails.Reachability.REMOTE;
}
// Machine was unreachable both tries
if (polledDetails == null) {
return false;
}
// Save the old MAC address
String savedMacAddress = details.macAddress;
// If we got here, it's reachable
details.update(polledDetails);
// If the new MAC address is empty, restore the old one (workaround for GFE bug)
if (details.macAddress.equals("00:00:00:00:00:00") && savedMacAddress != null) {
LimeLog.info("MAC address was empty; using existing value: "+savedMacAddress);
details.macAddress = savedMacAddress;
}
return true;
}
@Override
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(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 e) {}
}
// 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;
}
} }
@@ -12,75 +12,75 @@ import com.limelight.LimeLog;
import android.content.Context; import android.content.Context;
public class IdentityManager { public class IdentityManager {
private static final String UNIQUE_ID_FILE_NAME = "uniqueid"; private static final String UNIQUE_ID_FILE_NAME = "uniqueid";
private static final int UID_SIZE_IN_BYTES = 8; private static final int UID_SIZE_IN_BYTES = 8;
private String uniqueId; private String uniqueId;
public IdentityManager(Context c) { public IdentityManager(Context c) {
uniqueId = loadUniqueId(c); uniqueId = loadUniqueId(c);
if (uniqueId == null) { if (uniqueId == null) {
uniqueId = generateNewUniqueId(c); uniqueId = generateNewUniqueId(c);
} }
LimeLog.info("UID is now: "+uniqueId); LimeLog.info("UID is now: "+uniqueId);
} }
public String getUniqueId() { public String getUniqueId() {
return uniqueId; return uniqueId;
} }
private static String loadUniqueId(Context c) { private static String loadUniqueId(Context c) {
// 2 Hex digits per byte // 2 Hex digits per byte
char[] uid = new char[UID_SIZE_IN_BYTES * 2]; char[] uid = new char[UID_SIZE_IN_BYTES * 2];
InputStreamReader reader = null; InputStreamReader reader = null;
LimeLog.info("Reading UID from disk"); LimeLog.info("Reading UID from disk");
try { try {
reader = new InputStreamReader(c.openFileInput(UNIQUE_ID_FILE_NAME)); reader = new InputStreamReader(c.openFileInput(UNIQUE_ID_FILE_NAME));
if (reader.read(uid) != UID_SIZE_IN_BYTES * 2) if (reader.read(uid) != UID_SIZE_IN_BYTES * 2)
{ {
LimeLog.severe("UID file data is truncated"); LimeLog.severe("UID file data is truncated");
return null; return null;
} }
return new String(uid); return new String(uid);
} catch (FileNotFoundException e) { } catch (FileNotFoundException e) {
LimeLog.info("No UID file found"); LimeLog.info("No UID file found");
return null; return null;
} catch (IOException e) { } catch (IOException e) {
LimeLog.severe("Error while reading UID file"); LimeLog.severe("Error while reading UID file");
e.printStackTrace(); e.printStackTrace();
return null; return null;
} finally { } finally {
if (reader != null) { if (reader != null) {
try { try {
reader.close(); reader.close();
} catch (IOException ignored) {} } catch (IOException ignored) {}
} }
} }
} }
private static String generateNewUniqueId(Context c) { private static String generateNewUniqueId(Context c) {
// Generate a new UID hex string // Generate a new UID hex string
LimeLog.info("Generating new UID"); LimeLog.info("Generating new UID");
String uidStr = String.format((Locale)null, "%016x", new Random().nextLong()); String uidStr = String.format((Locale)null, "%016x", new Random().nextLong());
OutputStreamWriter writer = null; OutputStreamWriter writer = null;
try { try {
writer = new OutputStreamWriter(c.openFileOutput(UNIQUE_ID_FILE_NAME, 0)); writer = new OutputStreamWriter(c.openFileOutput(UNIQUE_ID_FILE_NAME, 0));
writer.write(uidStr); writer.write(uidStr);
LimeLog.info("UID written to disk"); LimeLog.info("UID written to disk");
} catch (IOException e) { } catch (IOException e) {
LimeLog.severe("Error while writing UID file"); LimeLog.severe("Error while writing UID file");
e.printStackTrace(); e.printStackTrace();
} finally { } finally {
if (writer != null) { if (writer != null) {
try { try {
writer.close(); writer.close();
} catch (IOException ignored) {} } catch (IOException ignored) {}
} }
} }
// We can return a UID even if I/O fails // We can return a UID even if I/O fails
return uidStr; return uidStr;
} }
} }
@@ -16,75 +16,75 @@ import android.os.IBinder;
public class DiscoveryService extends Service { public class DiscoveryService extends Service {
private MdnsDiscoveryAgent discoveryAgent; private MdnsDiscoveryAgent discoveryAgent;
private MdnsDiscoveryListener boundListener; private MdnsDiscoveryListener boundListener;
private MulticastLock multicastLock; private MulticastLock multicastLock;
public class DiscoveryBinder extends Binder { public class DiscoveryBinder extends Binder {
public void setListener(MdnsDiscoveryListener listener) { public void setListener(MdnsDiscoveryListener listener) {
boundListener = listener; boundListener = listener;
} }
public void startDiscovery(int queryIntervalMs) { public void startDiscovery(int queryIntervalMs) {
multicastLock.acquire(); multicastLock.acquire();
discoveryAgent.startDiscovery(queryIntervalMs); discoveryAgent.startDiscovery(queryIntervalMs);
} }
public void stopDiscovery() { public void stopDiscovery() {
discoveryAgent.stopDiscovery(); discoveryAgent.stopDiscovery();
multicastLock.release(); multicastLock.release();
} }
public List<MdnsComputer> getComputerSet() { public List<MdnsComputer> getComputerSet() {
return discoveryAgent.getComputerSet(); return discoveryAgent.getComputerSet();
} }
} }
@Override @Override
public void onCreate() { public void onCreate() {
WifiManager wifiMgr = (WifiManager) getSystemService(Context.WIFI_SERVICE); WifiManager wifiMgr = (WifiManager) getSystemService(Context.WIFI_SERVICE);
multicastLock = wifiMgr.createMulticastLock("Limelight mDNS"); multicastLock = wifiMgr.createMulticastLock("Limelight mDNS");
multicastLock.setReferenceCounted(false); multicastLock.setReferenceCounted(false);
discoveryAgent = new MdnsDiscoveryAgent(new MdnsDiscoveryListener() { discoveryAgent = new MdnsDiscoveryAgent(new MdnsDiscoveryListener() {
@Override @Override
public void notifyComputerAdded(MdnsComputer computer) { public void notifyComputerAdded(MdnsComputer computer) {
if (boundListener != null) { if (boundListener != null) {
boundListener.notifyComputerAdded(computer); boundListener.notifyComputerAdded(computer);
} }
} }
@Override @Override
public void notifyComputerRemoved(MdnsComputer computer) { public void notifyComputerRemoved(MdnsComputer computer) {
if (boundListener != null) { if (boundListener != null) {
boundListener.notifyComputerRemoved(computer); boundListener.notifyComputerRemoved(computer);
} }
} }
@Override @Override
public void notifyDiscoveryFailure(Exception e) { public void notifyDiscoveryFailure(Exception e) {
if (boundListener != null) { if (boundListener != null) {
boundListener.notifyDiscoveryFailure(e); boundListener.notifyDiscoveryFailure(e);
} }
} }
}); });
} }
private DiscoveryBinder binder = new DiscoveryBinder(); private final DiscoveryBinder binder = new DiscoveryBinder();
@Override @Override
public IBinder onBind(Intent intent) { public IBinder onBind(Intent intent) {
return binder; return binder;
} }
@Override @Override
public boolean onUnbind(Intent intent) { public boolean onUnbind(Intent intent) {
// Stop any discovery session // Stop any discovery session
discoveryAgent.stopDiscovery(); discoveryAgent.stopDiscovery();
multicastLock.release(); multicastLock.release();
// Unbind the listener // Unbind the listener
boundListener = null; boundListener = null;
return false; return false;
} }
} }
@@ -0,0 +1,229 @@
package com.limelight.grid;
import android.app.Activity;
import android.graphics.Bitmap;
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.lang.ref.WeakReference;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.Iterator;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
@SuppressWarnings("unchecked")
public class AppGridAdapter extends GenericGridAdapter<AppView.AppObject> {
private final Activity activity;
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;
private final ConcurrentHashMap<WeakReference<ImageView>, CachedAppAssetLoader.LoaderTuple> loadingTuples = new ConcurrentHashMap<>();
private final ConcurrentHashMap<Object, CachedAppAssetLoader.LoaderTuple> backgroundLoadingTuples = new ConcurrentHashMap<>();
public AppGridAdapter(Activity activity, boolean listMode, boolean small, ComputerDetails computer, String uniqueId) throws KeyManagementException, NoSuchAlgorithmException {
super(activity, listMode ? R.layout.simple_row : (small ? R.layout.app_grid_item_small : R.layout.app_grid_item), R.drawable.image_loading);
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));
if (scalingDivisor < 1.0) {
// We don't want to make them bigger before draw-time
scalingDivisor = 1.0;
}
LimeLog.info("Art scaling divisor: " + scalingDivisor);
this.activity = activity;
this.loader = new CachedAppAssetLoader(computer, scalingDivisor,
new NetworkAssetLoader(context, uniqueId),
new MemoryAssetLoader(), new DiskAssetLoader(context.getCacheDir()));
}
private static void cancelTuples(ConcurrentHashMap<?, CachedAppAssetLoader.LoaderTuple> map) {
Collection<CachedAppAssetLoader.LoaderTuple> tuples = map.values();
for (CachedAppAssetLoader.LoaderTuple tuple : tuples) {
tuple.cancel();
}
map.clear();
}
public void cancelQueuedOperations() {
cancelTuples(loadingTuples);
cancelTuples(backgroundLoadingTuples);
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 in the background
Object tupleKey = new Object();
CachedAppAssetLoader.LoaderTuple tuple =
loader.loadBitmapWithContextInBackground(app.app, tupleKey, backgroundLoadListener);
if (tuple != null) {
backgroundLoadingTuples.put(tupleKey, tuple);
}
itemList.add(app);
sortList();
}
public void removeApp(AppView.AppObject app) {
itemList.remove(app);
}
private final CachedAppAssetLoader.LoadListener imageViewLoadListener = new CachedAppAssetLoader.LoadListener() {
@Override
public void notifyLongLoad(Object object) {
final WeakReference<ImageView> viewRef = (WeakReference<ImageView>) object;
// If the view isn't there anymore, don't bother scheduling on the UI thread
if (viewRef.get() == null) {
return;
}
activity.runOnUiThread(new Runnable() {
@Override
public void run() {
ImageView view = viewRef.get();
if (view != null) {
view.setImageResource(R.drawable.image_loading);
fadeInImage(view);
}
}
});
}
@Override
public void notifyLoadComplete(Object object, final Bitmap bitmap) {
final WeakReference<ImageView> viewRef = (WeakReference<ImageView>) object;
loadingTuples.remove(viewRef);
// Just leave the loading icon in place
if (bitmap == null) {
return;
}
// If the view isn't there anymore, don't bother scheduling on the UI thread
if (viewRef.get() == null) {
return;
}
activity.runOnUiThread(new Runnable() {
@Override
public void run() {
ImageView view = viewRef.get();
if (view != null) {
view.setImageBitmap(bitmap);
fadeInImage(view);
}
}
});
}
};
private final CachedAppAssetLoader.LoadListener backgroundLoadListener = new CachedAppAssetLoader.LoadListener() {
@Override
public void notifyLongLoad(Object object) {}
@Override
public void notifyLoadComplete(Object object, final Bitmap bitmap) {
backgroundLoadingTuples.remove(object);
}
};
private void reapLoaderTuples(ImageView view) {
// Poor HashMap doesn't deserve this...
Iterator<Map.Entry<WeakReference<ImageView>, CachedAppAssetLoader.LoaderTuple>> i = loadingTuples.entrySet().iterator();
while (i.hasNext()) {
Map.Entry<WeakReference<ImageView>, CachedAppAssetLoader.LoaderTuple> entry = i.next();
ImageView imageView = entry.getKey().get();
// Remove tuples that refer to this view or no view
if (imageView == null || imageView == view) {
// FIXME: There's a small chance that this can race if we've already gone down
// the path to notification but haven't been notified yet
entry.getValue().cancel();
// Remove it from the tuple list
i.remove();
}
}
}
public boolean populateImageView(final ImageView imgView, final AppView.AppObject obj) {
// Cancel pending loads on this image view
reapLoaderTuples(imgView);
// Clear existing contents of the image view
imgView.setAlpha(0.0f);
// Start loading the bitmap
WeakReference<ImageView> viewRef = new WeakReference<>(imgView);
CachedAppAssetLoader.LoaderTuple tuple = loader.loadBitmapWithContext(obj.app, viewRef, imageViewLoadListener);
if (tuple != null) {
// The load was issued asynchronously
loadingTuples.put(viewRef, tuple);
}
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;
}
private static void fadeInImage(ImageView view) {
view.animate().alpha(1.0f).setDuration(100).start();
}
}
@@ -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;
}
}
@@ -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;
}
}
@@ -0,0 +1,162 @@
package com.limelight.grid.assets;
import android.graphics.Bitmap;
import com.limelight.nvstream.http.ComputerDetails;
import com.limelight.nvstream.http.NvApp;
import java.io.InputStream;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
public class CachedAppAssetLoader {
private final ComputerDetails computer;
private final double scalingDivider;
private final ThreadPoolExecutor foregroundExecutor = new ThreadPoolExecutor(8, 8, Long.MAX_VALUE, TimeUnit.DAYS, new LinkedBlockingQueue<Runnable>());
private final ThreadPoolExecutor backgroundExecutor = new ThreadPoolExecutor(2, 2, Long.MAX_VALUE, TimeUnit.DAYS, new LinkedBlockingQueue<Runnable>());
private final NetworkAssetLoader networkLoader;
private final MemoryAssetLoader memoryLoader;
private final DiskAssetLoader diskLoader;
public CachedAppAssetLoader(ComputerDetails computer, double scalingDivider,
NetworkAssetLoader networkLoader, MemoryAssetLoader memoryLoader,
DiskAssetLoader diskLoader) {
this.computer = computer;
this.scalingDivider = scalingDivider;
this.networkLoader = networkLoader;
this.memoryLoader = memoryLoader;
this.diskLoader = diskLoader;
}
public void freeCacheMemory() {
memoryLoader.clearCache();
}
private Runnable createLoaderRunnable(final LoaderTuple tuple, final Object context, final LoadListener listener) {
return new Runnable() {
@Override
public void run() {
// Abort if we've been cancelled
if (tuple.cancelled) {
return;
}
Bitmap bmp = diskLoader.loadBitmapFromCache(tuple, (int) scalingDivider);
if (bmp == null) {
// Notify the listener that this may take a while
listener.notifyLongLoad(context);
// Try 5 times maximum
for (int i = 0; i < 5; i++) {
// Check again whether we've been cancelled
if (tuple.cancelled) {
return;
}
InputStream in = networkLoader.getBitmapStream(tuple);
if (in != null) {
// Write the stream straight to disk
diskLoader.populateCacheWithStream(tuple, in);
// Read it back scaled
bmp = diskLoader.loadBitmapFromCache(tuple, (int) scalingDivider);
if (bmp != null) {
break;
}
}
// Wait 1 second with a bit of fuzz
try {
Thread.sleep((int) (1000 + (Math.random() * 500)));
} catch (InterruptedException e) {
break;
}
}
}
if (bmp != null) {
// Populate the memory cache
memoryLoader.populateCache(tuple, bmp);
}
// Check one last time whether we've been cancelled
synchronized (tuple) {
if (tuple.cancelled) {
return;
}
else {
tuple.notified = true;
}
}
// Call the load complete callback (possible with a null bitmap)
listener.notifyLoadComplete(context, bmp);
}
};
}
public LoaderTuple loadBitmapWithContext(NvApp app, Object context, LoadListener listener) {
return loadBitmapWithContext(app, context, listener, false);
}
public LoaderTuple loadBitmapWithContextInBackground(NvApp app, Object context, LoadListener listener) {
return loadBitmapWithContext(app, context, listener, true);
}
private LoaderTuple loadBitmapWithContext(NvApp app, Object context, LoadListener listener, boolean background) {
LoaderTuple tuple = new LoaderTuple(computer, app);
// First, try the memory cache in the current context
Bitmap bmp = memoryLoader.loadBitmapFromCache(tuple);
if (bmp != null) {
// The caller never sees our tuple in this case
listener.notifyLoadComplete(context, bmp);
return null;
}
// If it's not in memory, throw this in our executor
if (background) {
backgroundExecutor.execute(createLoaderRunnable(tuple, context, listener));
}
else {
foregroundExecutor.execute(createLoaderRunnable(tuple, context, listener));
}
return tuple;
}
public class LoaderTuple {
public final ComputerDetails computer;
public final NvApp app;
public boolean notified;
public boolean cancelled;
public LoaderTuple(ComputerDetails computer, NvApp app) {
this.computer = computer;
this.app = app;
}
public boolean cancel() {
synchronized (this) {
cancelled = true;
return !notified;
}
}
@Override
public String toString() {
return "("+computer.uuid+", "+app.getAppId()+")";
}
}
public interface LoadListener {
// Notifies that the load didn't hit any cache and is about to be dispatched
// over the network
public void notifyLongLoad(Object context);
// Bitmap may be null if the load failed
public void notifyLoadComplete(Object context, Bitmap bitmap);
}
}
@@ -0,0 +1,61 @@
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.IOException;
import java.io.InputStream;
import java.io.OutputStream;
public class DiskAssetLoader {
private final File cacheDir;
public DiskAssetLoader(File cacheDir) {
this.cacheDir = cacheDir;
}
public Bitmap loadBitmapFromCache(CachedAppAssetLoader.LoaderTuple tuple, int sampleSize) {
InputStream in = null;
Bitmap bmp = null;
try {
in = CacheHelper.openCacheFileForInput(cacheDir, "boxart", tuple.computer.uuid.toString(), tuple.app.getAppId() + ".png");
BitmapFactory.Options options = new BitmapFactory.Options();
options.inSampleSize = sampleSize;
bmp = BitmapFactory.decodeStream(in, null, options);
} catch (IOException e) {
e.printStackTrace();
} 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;
try {
out = CacheHelper.openCacheFileForOutput(cacheDir, "boxart", tuple.computer.uuid.toString(), tuple.app.getAppId() + ".png");
CacheHelper.writeInputStreamToOutputStream(input, out);
} catch (IOException e) {
e.printStackTrace();
} finally {
if (out != null) {
try {
out.close();
} catch (IOException ignored) {}
}
}
}
}
@@ -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 / 12) {
@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();
}
}
@@ -0,0 +1,51 @@
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;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
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 e) {}
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;
}
}
}
@@ -1,44 +1,44 @@
package com.limelight.nvstream.av.video.cpu; package com.limelight.nvstream.av.video.cpu;
public class AvcDecoder { public class AvcDecoder {
static { static {
// FFMPEG dependencies // FFMPEG dependencies
System.loadLibrary("avutil-52"); System.loadLibrary("avutil-52");
System.loadLibrary("swresample-0"); System.loadLibrary("swresample-0");
System.loadLibrary("swscale-2"); System.loadLibrary("swscale-2");
System.loadLibrary("avcodec-55"); System.loadLibrary("avcodec-55");
System.loadLibrary("avformat-55"); System.loadLibrary("avformat-55");
System.loadLibrary("nv_avc_dec"); System.loadLibrary("nv_avc_dec");
} }
/** Disables the deblocking filter at the cost of image quality */ /** Disables the deblocking filter at the cost of image quality */
public static final int DISABLE_LOOP_FILTER = 0x1; public static final int DISABLE_LOOP_FILTER = 0x1;
/** Uses the low latency decode flag (disables multithreading) */ /** Uses the low latency decode flag (disables multithreading) */
public static final int LOW_LATENCY_DECODE = 0x2; public static final int LOW_LATENCY_DECODE = 0x2;
/** Threads process each slice, rather than each frame */ /** Threads process each slice, rather than each frame */
public static final int SLICE_THREADING = 0x4; public static final int SLICE_THREADING = 0x4;
/** Uses nonstandard speedup tricks */ /** Uses nonstandard speedup tricks */
public static final int FAST_DECODE = 0x8; public static final int FAST_DECODE = 0x8;
/** Uses bilinear filtering instead of bicubic */ /** Uses bilinear filtering instead of bicubic */
public static final int BILINEAR_FILTERING = 0x10; public static final int BILINEAR_FILTERING = 0x10;
/** Uses a faster bilinear filtering with lower image quality */ /** Uses a faster bilinear filtering with lower image quality */
public static final int FAST_BILINEAR_FILTERING = 0x20; public static final int FAST_BILINEAR_FILTERING = 0x20;
/** Disables color conversion (output is NV21) */ /** Disables color conversion (output is NV21) */
public static final int NO_COLOR_CONVERSION = 0x40; public static final int NO_COLOR_CONVERSION = 0x40;
public static native int init(int width, int height, int perflvl, int threadcount); public static native int init(int width, int height, int perflvl, int threadcount);
public static native void destroy(); public static native void destroy();
// Rendering API when NO_COLOR_CONVERSION == 0 // Rendering API when NO_COLOR_CONVERSION == 0
public static native boolean setRenderTarget(Object androidSurface); public static native boolean setRenderTarget(Object androidSurface);
public static native boolean getRgbFrameInt(int[] rgbFrame, int bufferSize); public static native boolean getRgbFrameInt(int[] rgbFrame, int bufferSize);
public static native boolean getRgbFrame(byte[] rgbFrame, int bufferSize); public static native boolean getRgbFrame(byte[] rgbFrame, int bufferSize);
public static native boolean redraw(); public static native boolean redraw();
// Rendering API when NO_COLOR_CONVERSION == 1 // Rendering API when NO_COLOR_CONVERSION == 1
public static native boolean getRawFrame(byte[] yuvFrame, int bufferSize); public static native boolean getRawFrame(byte[] yuvFrame, int bufferSize);
public static native int getInputPaddingSize(); public static native int getInputPaddingSize();
public static native int decode(byte[] indata, int inoff, int inlen); public static native int decode(byte[] indata, int inoff, int inlen);
} }
@@ -2,149 +2,180 @@ package com.limelight.preferences;
import java.net.InetAddress; import java.net.InetAddress;
import java.net.UnknownHostException; import java.net.UnknownHostException;
import java.util.Locale;
import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.LinkedBlockingQueue;
import com.limelight.computers.ComputerManagerService; import com.limelight.computers.ComputerManagerService;
import com.limelight.R; import com.limelight.R;
import com.limelight.utils.Dialog; import com.limelight.utils.Dialog;
import com.limelight.utils.SpinnerDialog;
import com.limelight.utils.UiHelper;
import android.app.Activity; import android.app.Activity;
import android.app.Service; import android.app.Service;
import android.content.ComponentName; import android.content.ComponentName;
import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.content.ServiceConnection; import android.content.ServiceConnection;
import android.content.res.Configuration;
import android.os.Bundle; import android.os.Bundle;
import android.os.IBinder; import android.os.IBinder;
import android.view.View; import android.view.KeyEvent;
import android.view.View.OnClickListener; import android.view.inputmethod.EditorInfo;
import android.widget.Button; import android.view.inputmethod.InputMethodManager;
import android.widget.TextView; import android.widget.TextView;
import android.widget.Toast; import android.widget.Toast;
public class AddComputerManually extends Activity { public class AddComputerManually extends Activity {
private Button addPcButton; private TextView hostText;
private TextView hostText; private ComputerManagerService.ComputerManagerBinder managerBinder;
private ComputerManagerService.ComputerManagerBinder managerBinder; private final LinkedBlockingQueue<String> computersToAdd = new LinkedBlockingQueue<String>();
private LinkedBlockingQueue<String> computersToAdd = new LinkedBlockingQueue<String>(); private Thread addThread;
private Thread addThread; private final ServiceConnection serviceConnection = new ServiceConnection() {
private ServiceConnection serviceConnection = new ServiceConnection() { public void onServiceConnected(ComponentName className, final IBinder binder) {
public void onServiceConnected(ComponentName className, final IBinder binder) { managerBinder = ((ComputerManagerService.ComputerManagerBinder)binder);
managerBinder = ((ComputerManagerService.ComputerManagerBinder)binder); startAddThread();
startAddThread(); }
}
public void onServiceDisconnected(ComponentName className) { public void onServiceDisconnected(ComponentName className) {
joinAddThread(); joinAddThread();
managerBinder = null; managerBinder = null;
} }
}; };
private void doAddPc(String host) { private void doAddPc(String host) {
String msg; String msg;
boolean finish = false; boolean finish = false;
try {
InetAddress addr = InetAddress.getByName(host);
if (!managerBinder.addComputerBlocking(addr)){ SpinnerDialog dialog = SpinnerDialog.displayDialog(this, getResources().getString(R.string.title_add_pc),
msg = "Unable to connect to the specified computer. Make sure the required ports are allowed through the firewall."; getResources().getString(R.string.msg_add_pc), false);
}
else {
msg = "Successfully added computer";
finish = true;
}
} catch (UnknownHostException e) {
msg = "Unable to resolve PC address. Make sure you didn't make a typo in the address.";
}
final boolean toastFinish = finish; try {
final String toastMsg = msg; InetAddress addr = InetAddress.getByName(host);
AddComputerManually.this.runOnUiThread(new Runnable() {
@Override
public void run() {
Toast.makeText(AddComputerManually.this, toastMsg, Toast.LENGTH_LONG).show();
if (toastFinish && !isFinishing()) { if (!managerBinder.addComputerBlocking(addr)){
// Close the activity msg = getResources().getString(R.string.addpc_fail);
AddComputerManually.this.finish(); }
} else {
} msg = getResources().getString(R.string.addpc_success);
}); finish = true;
} }
} catch (UnknownHostException e) {
msg = getResources().getString(R.string.addpc_unknown_host);
}
private void startAddThread() { dialog.dismiss();
addThread = new Thread() {
@Override
public void run() {
while (!isInterrupted()) {
String computer;
try { final boolean toastFinish = finish;
computer = computersToAdd.take(); final String toastMsg = msg;
} catch (InterruptedException e) { AddComputerManually.this.runOnUiThread(new Runnable() {
return; @Override
} public void run() {
Toast.makeText(AddComputerManually.this, toastMsg, Toast.LENGTH_LONG).show();
doAddPc(computer); if (toastFinish && !isFinishing()) {
} // Close the activity
} AddComputerManually.this.finish();
}; }
addThread.setName("UI - AddComputerManually"); }
addThread.start(); });
} }
private void joinAddThread() { private void startAddThread() {
if (addThread != null) { addThread = new Thread() {
addThread.interrupt(); @Override
public void run() {
while (!isInterrupted()) {
String computer;
try { try {
addThread.join(); computer = computersToAdd.take();
} catch (InterruptedException ignored) {} } catch (InterruptedException e) {
return;
}
addThread = null; doAddPc(computer);
} }
} }
};
addThread.setName("UI - AddComputerManually");
addThread.start();
}
@Override private void joinAddThread() {
protected void onStop() { if (addThread != null) {
super.onStop(); addThread.interrupt();
Dialog.closeDialogs(); try {
} addThread.join();
} catch (InterruptedException ignored) {}
@Override addThread = null;
protected void onDestroy() { }
super.onDestroy(); }
if (managerBinder != null) { @Override
joinAddThread(); protected void onStop() {
unbindService(serviceConnection); super.onStop();
}
}
@Override Dialog.closeDialogs();
protected void onCreate(Bundle savedInstanceState) { SpinnerDialog.closeDialogs(this);
super.onCreate(savedInstanceState); }
setContentView(R.layout.activity_add_computer_manually); @Override
protected void onDestroy() {
super.onDestroy();
this.addPcButton = (Button) findViewById(R.id.addPc); if (managerBinder != null) {
this.hostText = (TextView) findViewById(R.id.hostTextView); joinAddThread();
unbindService(serviceConnection);
}
}
// Bind to the ComputerManager service @Override
bindService(new Intent(AddComputerManually.this, protected void onCreate(Bundle savedInstanceState) {
ComputerManagerService.class), serviceConnection, Service.BIND_AUTO_CREATE); super.onCreate(savedInstanceState);
addPcButton.setOnClickListener(new OnClickListener() { String locale = PreferenceConfiguration.readPreferences(this).language;
@Override if (!locale.equals(PreferenceConfiguration.DEFAULT_LANGUAGE)) {
public void onClick(View v) { Configuration config = new Configuration(getResources().getConfiguration());
if (hostText.getText().length() == 0) { config.locale = new Locale(locale);
Toast.makeText(AddComputerManually.this, "You must enter an IP address", Toast.LENGTH_LONG).show(); getResources().updateConfiguration(config, getResources().getDisplayMetrics());
return; }
}
Toast.makeText(AddComputerManually.this, "Adding PC...", Toast.LENGTH_SHORT).show(); setContentView(R.layout.activity_add_computer_manually);
computersToAdd.add(hostText.getText().toString());
} 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);
}
} }
@@ -2,6 +2,7 @@ package com.limelight.preferences;
import android.content.Context; import android.content.Context;
import android.content.SharedPreferences; import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.preference.PreferenceManager; import android.preference.PreferenceManager;
public class PreferenceConfiguration { public class PreferenceConfiguration {
@@ -12,11 +13,16 @@ public class PreferenceConfiguration {
private static final String SOPS_PREF_STRING = "checkbox_enable_sops"; 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 DISABLE_TOASTS_PREF_STRING = "checkbox_disable_warnings";
private static final String HOST_AUDIO_PREF_STRING = "checkbox_host_audio"; 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_30 = 5;
private static final int BITRATE_DEFAULT_720_60 = 10; 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_30 = 10;
private static final int BITRATE_DEFAULT_1080_60 = 30; private static final int BITRATE_DEFAULT_1080_60 = 20;
private static final String DEFAULT_RES_FPS = "720p60"; private static final String DEFAULT_RES_FPS = "720p60";
private static final String DEFAULT_DECODER = "auto"; private static final String DEFAULT_DECODER = "auto";
@@ -25,6 +31,10 @@ public class PreferenceConfiguration {
private static final boolean DEFAULT_SOPS = true; private static final boolean DEFAULT_SOPS = true;
private static final boolean DEFAULT_DISABLE_TOASTS = false; private static final boolean DEFAULT_DISABLE_TOASTS = false;
private static final boolean DEFAULT_HOST_AUDIO = 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 FORCE_HARDWARE_DECODER = -1;
public static final int AUTOSELECT_DECODER = 0; public static final int AUTOSELECT_DECODER = 0;
@@ -33,7 +43,10 @@ public class PreferenceConfiguration {
public int width, height, fps; public int width, height, fps;
public int bitrate; public int bitrate;
public int decoder; public int decoder;
public int deadzonePercentage;
public boolean stretchVideo, enableSops, playHostAudio, disableWarnings; public boolean stretchVideo, enableSops, playHostAudio, disableWarnings;
public String language;
public boolean listMode, smallIconMode, multiController;
public static int getDefaultBitrate(String resFpsString) { public static int getDefaultBitrate(String resFpsString) {
if (resFpsString.equals("720p30")) { if (resFpsString.equals("720p30")) {
@@ -54,6 +67,17 @@ public class PreferenceConfiguration {
} }
} }
public static boolean getDefaultSmallMode(Context context) {
PackageManager manager = context.getPackageManager();
if (manager != null && manager.hasSystemFeature(PackageManager.FEATURE_TELEVISION)) {
// TVs shouldn't use small mode by default
return false;
}
// Use small mode on anything smaller than a 7" tablet
return context.getResources().getConfiguration().smallestScreenWidthDp < 600;
}
public static int getDefaultBitrate(Context context) { public static int getDefaultBitrate(Context context) {
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
@@ -130,11 +154,18 @@ public class PreferenceConfiguration {
config.decoder = getDecoderValue(context); config.decoder = getDecoderValue(context);
config.deadzonePercentage = prefs.getInt(DEADZONE_PREF_STRING, DEFAULT_DEADZONE);
config.language = prefs.getString(LANGUAGE_PREF_STRING, DEFAULT_LANGUAGE);
// Checkbox preferences // Checkbox preferences
config.disableWarnings = prefs.getBoolean(DISABLE_TOASTS_PREF_STRING, DEFAULT_DISABLE_TOASTS); config.disableWarnings = prefs.getBoolean(DISABLE_TOASTS_PREF_STRING, DEFAULT_DISABLE_TOASTS);
config.enableSops = prefs.getBoolean(SOPS_PREF_STRING, DEFAULT_SOPS); config.enableSops = prefs.getBoolean(SOPS_PREF_STRING, DEFAULT_SOPS);
config.stretchVideo = prefs.getBoolean(STRETCH_PREF_STRING, DEFAULT_STRETCH); config.stretchVideo = prefs.getBoolean(STRETCH_PREF_STRING, DEFAULT_STRETCH);
config.playHostAudio = prefs.getBoolean(HOST_AUDIO_PREF_STRING, DEFAULT_HOST_AUDIO); 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; return config;
} }
@@ -19,11 +19,15 @@ public class SeekBarPreference extends DialogPreference
private static final String SCHEMA_URL = "http://schemas.android.com/apk/res/android"; private static final String SCHEMA_URL = "http://schemas.android.com/apk/res/android";
private SeekBar seekBar; private SeekBar seekBar;
private TextView splashText, valueText; private TextView valueText;
private Context context; private final Context context;
private String dialogMessage, suffix; private final String dialogMessage;
private int defaultValue, maxValue, currentValue; 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) { public SeekBarPreference(Context context, AttributeSet attrs) {
super(context, attrs); super(context, attrs);
@@ -47,9 +51,10 @@ public class SeekBarPreference extends DialogPreference
suffix = context.getString(suffixId); suffix = context.getString(suffixId);
} }
// Get default and max seekbar values // Get default, min, and max seekbar values
defaultValue = PreferenceConfiguration.getDefaultBitrate(context); defaultValue = attrs.getAttributeIntValue(SCHEMA_URL, "defaultValue", PreferenceConfiguration.getDefaultBitrate(context));
maxValue = attrs.getAttributeIntValue(SCHEMA_URL, "max", 100); maxValue = attrs.getAttributeIntValue(SCHEMA_URL, "max", 100);
minValue = 1;
} }
@Override @Override
@@ -60,7 +65,7 @@ public class SeekBarPreference extends DialogPreference
layout.setOrientation(LinearLayout.VERTICAL); layout.setOrientation(LinearLayout.VERTICAL);
layout.setPadding(6, 6, 6, 6); layout.setPadding(6, 6, 6, 6);
splashText = new TextView(context); TextView splashText = new TextView(context);
splashText.setPadding(30, 10, 30, 10); splashText.setPadding(30, 10, 30, 10);
if (dialogMessage != null) { if (dialogMessage != null) {
splashText.setText(dialogMessage); splashText.setText(dialogMessage);
@@ -71,7 +76,7 @@ public class SeekBarPreference extends DialogPreference
valueText.setGravity(Gravity.CENTER_HORIZONTAL); valueText.setGravity(Gravity.CENTER_HORIZONTAL);
valueText.setTextSize(32); valueText.setTextSize(32);
params = new LinearLayout.LayoutParams( params = new LinearLayout.LayoutParams(
LinearLayout.LayoutParams.FILL_PARENT, LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.WRAP_CONTENT); LinearLayout.LayoutParams.WRAP_CONTENT);
layout.addView(valueText, params); layout.addView(valueText, params);
@@ -79,8 +84,13 @@ public class SeekBarPreference extends DialogPreference
seekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() { seekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
@Override @Override
public void onProgressChanged(SeekBar seekBar, int value, boolean b) { public void onProgressChanged(SeekBar seekBar, int value, boolean b) {
if (value < minValue) {
seekBar.setProgress(minValue);
return;
}
String t = String.valueOf(value); String t = String.valueOf(value);
valueText.setText(suffix == null ? t : t.concat(" " + suffix)); valueText.setText(suffix == null ? t : t.concat(suffix.length() > 1 ? " "+suffix : suffix));
} }
@Override @Override
@@ -90,7 +100,7 @@ public class SeekBarPreference extends DialogPreference
public void onStopTrackingTouch(SeekBar seekBar) {} public void onStopTrackingTouch(SeekBar seekBar) {}
}); });
layout.addView(seekBar, new LinearLayout.LayoutParams(LinearLayout.LayoutParams.FILL_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT)); layout.addView(seekBar, new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT));
if (shouldPersist()) { if (shouldPersist()) {
currentValue = getPersistedInt(defaultValue); currentValue = getPersistedInt(defaultValue);
@@ -121,13 +131,6 @@ public class SeekBarPreference extends DialogPreference
} }
} }
public void setMax(int max) {
this.maxValue = max;
}
public int getMax() {
return this.maxValue;
}
public void setProgress(int progress) { public void setProgress(int progress) {
this.currentValue = progress; this.currentValue = progress;
if (seekBar != null) { if (seekBar != null) {
@@ -149,10 +152,10 @@ public class SeekBarPreference extends DialogPreference
if (shouldPersist()) { if (shouldPersist()) {
currentValue = seekBar.getProgress(); currentValue = seekBar.getProgress();
persistInt(seekBar.getProgress()); persistInt(seekBar.getProgress());
callChangeListener(Integer.valueOf(seekBar.getProgress())); callChangeListener(seekBar.getProgress());
} }
((AlertDialog) getDialog()).dismiss(); getDialog().dismiss();
} }
}); });
} }
@@ -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());
}
}
@@ -1,23 +1,48 @@
package com.limelight.preferences; package com.limelight.preferences;
import android.content.Intent;
import android.content.SharedPreferences; import android.content.SharedPreferences;
import android.content.res.Configuration;
import android.os.Bundle; import android.os.Bundle;
import android.app.Activity; import android.app.Activity;
import android.preference.Preference; import android.preference.Preference;
import android.preference.PreferenceFragment; import android.preference.PreferenceFragment;
import android.preference.PreferenceManager; import android.preference.PreferenceManager;
import com.limelight.PcView;
import com.limelight.R; import com.limelight.R;
import com.limelight.utils.UiHelper;
import java.util.Locale;
public class StreamSettings extends Activity { public class StreamSettings extends Activity {
@Override @Override
protected void onCreate(Bundle savedInstanceState) { protected void onCreate(Bundle savedInstanceState) {
super.onCreate(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_stream_settings); setContentView(R.layout.activity_stream_settings);
getFragmentManager().beginTransaction().replace( getFragmentManager().beginTransaction().replace(
R.id.stream_settings, new SettingsFragment() R.id.stream_settings, new SettingsFragment()
).commit(); ).commit();
UiHelper.notifyNewRootView(this);
}
@Override
public void onBackPressed() {
finish();
// 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 { public static class SettingsFragment extends PreferenceFragment {
@@ -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));
}
}
@@ -0,0 +1,8 @@
package com.limelight.ui;
import android.widget.AbsListView;
public interface AdapterFragmentCallbacks {
public int getAdapterFragmentLayoutId();
public void receiveAbsListView(AbsListView gridView);
}
@@ -0,0 +1,5 @@
package com.limelight.ui;
public interface GameGestures {
public void showKeyboard();
}
@@ -0,0 +1,66 @@
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 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) throws IOException {
byte[] buf = new byte[4096];
int bytesRead;
while ((bytesRead = in.read(buf)) != -1) {
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);
}
return sb.toString();
}
public static void writeStringToOutputStream(OutputStream out, String str) throws IOException {
out.write(str.getBytes("UTF-8"));
}
}
@@ -7,70 +7,71 @@ import android.app.AlertDialog;
import android.content.DialogInterface; import android.content.DialogInterface;
public class Dialog implements Runnable { public class Dialog implements Runnable {
private String title, message; private final String title;
private Activity activity; private final String message;
private boolean endAfterDismiss; private final Activity activity;
private final boolean endAfterDismiss;
private AlertDialog alert; private AlertDialog alert;
private static final ArrayList<Dialog> rundownDialogs = new ArrayList<Dialog>(); private static final ArrayList<Dialog> rundownDialogs = new ArrayList<Dialog>();
public Dialog(Activity activity, String title, String message, boolean endAfterDismiss) private Dialog(Activity activity, String title, String message, boolean endAfterDismiss)
{ {
this.activity = activity; this.activity = activity;
this.title = title; this.title = title;
this.message = message; this.message = message;
this.endAfterDismiss = endAfterDismiss; this.endAfterDismiss = endAfterDismiss;
} }
public static void closeDialogs() public static void closeDialogs()
{ {
synchronized (rundownDialogs) { synchronized (rundownDialogs) {
for (Dialog d : rundownDialogs) { for (Dialog d : rundownDialogs) {
if (d.alert.isShowing()) { if (d.alert.isShowing()) {
d.alert.dismiss(); d.alert.dismiss();
} }
} }
rundownDialogs.clear(); rundownDialogs.clear();
} }
} }
public static void displayDialog(Activity activity, String title, String message, boolean endAfterDismiss) public static void displayDialog(Activity activity, String title, String message, boolean endAfterDismiss)
{ {
activity.runOnUiThread(new Dialog(activity, title, message, endAfterDismiss)); activity.runOnUiThread(new Dialog(activity, title, message, endAfterDismiss));
} }
@Override @Override
public void run() { public void run() {
// If we're dying, don't bother creating a dialog // If we're dying, don't bother creating a dialog
if (activity.isFinishing()) if (activity.isFinishing())
return; return;
alert = new AlertDialog.Builder(activity).create(); alert = new AlertDialog.Builder(activity).create();
alert.setTitle(title); alert.setTitle(title);
alert.setMessage(message); alert.setMessage(message);
alert.setCancelable(false); alert.setCancelable(false);
alert.setCanceledOnTouchOutside(false); alert.setCanceledOnTouchOutside(false);
alert.setButton(AlertDialog.BUTTON_NEUTRAL, "OK", new DialogInterface.OnClickListener() { alert.setButton(AlertDialog.BUTTON_NEUTRAL, "OK", new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int which) { public void onClick(DialogInterface dialog, int which) {
synchronized (rundownDialogs) { synchronized (rundownDialogs) {
rundownDialogs.remove(Dialog.this); rundownDialogs.remove(Dialog.this);
alert.dismiss(); alert.dismiss();
} }
if (endAfterDismiss) { if (endAfterDismiss) {
activity.finish(); activity.finish();
} }
} }
}); });
synchronized (rundownDialogs) { synchronized (rundownDialogs) {
rundownDialogs.add(this); rundownDialogs.add(this);
alert.show(); alert.show();
} }
} }
} }
@@ -9,111 +9,112 @@ import android.content.DialogInterface;
import android.content.DialogInterface.OnCancelListener; import android.content.DialogInterface.OnCancelListener;
public class SpinnerDialog implements Runnable,OnCancelListener { public class SpinnerDialog implements Runnable,OnCancelListener {
private String title, message; private final String title;
private Activity activity; private final String message;
private ProgressDialog progress; private final Activity activity;
private boolean finish; private ProgressDialog progress;
private final boolean finish;
private static final ArrayList<SpinnerDialog> rundownDialogs = new ArrayList<SpinnerDialog>(); private static final ArrayList<SpinnerDialog> rundownDialogs = new ArrayList<SpinnerDialog>();
public SpinnerDialog(Activity activity, String title, String message, boolean finish) private SpinnerDialog(Activity activity, String title, String message, boolean finish)
{ {
this.activity = activity; this.activity = activity;
this.title = title; this.title = title;
this.message = message; this.message = message;
this.progress = null; this.progress = null;
this.finish = finish; this.finish = finish;
} }
public static SpinnerDialog displayDialog(Activity activity, String title, String message, boolean finish) public static SpinnerDialog displayDialog(Activity activity, String title, String message, boolean finish)
{ {
SpinnerDialog spinner = new SpinnerDialog(activity, title, message, finish); SpinnerDialog spinner = new SpinnerDialog(activity, title, message, finish);
activity.runOnUiThread(spinner); activity.runOnUiThread(spinner);
return spinner; return spinner;
} }
public static void closeDialogs(Activity activity) public static void closeDialogs(Activity activity)
{ {
synchronized (rundownDialogs) { synchronized (rundownDialogs) {
Iterator<SpinnerDialog> i = rundownDialogs.iterator(); Iterator<SpinnerDialog> i = rundownDialogs.iterator();
while (i.hasNext()) { while (i.hasNext()) {
SpinnerDialog dialog = i.next(); SpinnerDialog dialog = i.next();
if (dialog.activity == activity) { if (dialog.activity == activity) {
i.remove(); i.remove();
if (dialog.progress.isShowing()) { if (dialog.progress.isShowing()) {
dialog.progress.dismiss(); dialog.progress.dismiss();
} }
} }
} }
} }
} }
public void dismiss() public void dismiss()
{ {
// Running again with progress != null will destroy it // Running again with progress != null will destroy it
activity.runOnUiThread(this); activity.runOnUiThread(this);
} }
public void setMessage(final String message) public void setMessage(final String message)
{ {
activity.runOnUiThread(new Runnable() { activity.runOnUiThread(new Runnable() {
@Override @Override
public void run() { public void run() {
progress.setMessage(message); progress.setMessage(message);
} }
}); });
} }
@Override @Override
public void run() { public void run() {
// If we're dying, don't bother doing anything // If we're dying, don't bother doing anything
if (activity.isFinishing()) { if (activity.isFinishing()) {
return; return;
} }
if (progress == null) if (progress == null)
{ {
progress = new ProgressDialog(activity); progress = new ProgressDialog(activity);
progress.setTitle(title); progress.setTitle(title);
progress.setMessage(message); progress.setMessage(message);
progress.setProgressStyle(ProgressDialog.STYLE_SPINNER); progress.setProgressStyle(ProgressDialog.STYLE_SPINNER);
progress.setOnCancelListener(this); progress.setOnCancelListener(this);
// If we want to finish the activity when this is killed, make it cancellable // If we want to finish the activity when this is killed, make it cancellable
if (finish) if (finish)
{ {
progress.setCancelable(true); progress.setCancelable(true);
progress.setCanceledOnTouchOutside(false); progress.setCanceledOnTouchOutside(false);
} }
else else
{ {
progress.setCancelable(false); progress.setCancelable(false);
} }
synchronized (rundownDialogs) { synchronized (rundownDialogs) {
rundownDialogs.add(this); rundownDialogs.add(this);
progress.show(); progress.show();
} }
} }
else else
{ {
synchronized (rundownDialogs) { synchronized (rundownDialogs) {
if (rundownDialogs.remove(this) && progress.isShowing()) { if (rundownDialogs.remove(this) && progress.isShowing()) {
progress.dismiss(); progress.dismiss();
} }
} }
} }
} }
@Override @Override
public void onCancel(DialogInterface dialog) { public void onCancel(DialogInterface dialog) {
synchronized (rundownDialogs) { synchronized (rundownDialogs) {
rundownDialogs.remove(this); rundownDialogs.remove(this);
} }
// This will only be called if finish was true, so we don't need to check again // This will only be called if finish was true, so we don't need to check again
activity.finish(); activity.finish();
} }
} }
@@ -0,0 +1,31 @@
package com.limelight.utils;
import android.app.Activity;
import android.app.UiModeManager;
import android.content.Context;
import android.content.res.Configuration;
import android.view.View;
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);
}
}
}
+12 -12
View File
@@ -69,9 +69,6 @@ int nv_avc_init(int width, int height, int perf_lvl, int thread_count) {
return -1; return -1;
} }
// Show frames even before a reference frame
decoder_ctx->flags2 |= CODEC_FLAG2_SHOW_ALL;
if (perf_lvl & DISABLE_LOOP_FILTER) { if (perf_lvl & DISABLE_LOOP_FILTER) {
// Skip the loop filter for performance reasons // Skip the loop filter for performance reasons
decoder_ctx->skip_loop_filter = AVDISCARD_ALL; decoder_ctx->skip_loop_filter = AVDISCARD_ALL;
@@ -370,17 +367,20 @@ int nv_avc_decode(unsigned char* indata, int inlen) {
// Only copy the picture at the end of decoding the packet // Only copy the picture at the end of decoding the packet
if (got_pic) { if (got_pic) {
// Clone the current decode frame outside of the mutex
AVFrame* new_frame = av_frame_clone(dec_frame);
AVFrame* old_frame;
// Swap it in under lock
pthread_mutex_lock(&mutex); pthread_mutex_lock(&mutex);
old_frame = yuv_frame;
// Only clone this frame if the last frame was taken. yuv_frame = new_frame;
// This saves on extra copies for frames that don't get
// rendered.
if (yuv_frame == NULL) {
// Clone a new frame
yuv_frame = av_frame_clone(dec_frame);
}
pthread_mutex_unlock(&mutex); pthread_mutex_unlock(&mutex);
// Free the old frame outside of the mutex
if (old_frame != NULL) {
av_frame_free(&old_frame);
}
} }
return err < 0 ? err : 0; return err < 0 ? err : 0;
Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

@@ -2,6 +2,6 @@
<shape xmlns:android="http://schemas.android.com/apk/res/android" <shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle" > android:shape="rectangle" >
<stroke android:width="1dip" android:color="#ffffff"/> <stroke android:width="1dip" android:color="#ffffff"/>
</shape> </shape>
Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

@@ -8,48 +8,67 @@
android:paddingTop="@dimen/activity_vertical_margin" android:paddingTop="@dimen/activity_vertical_margin"
tools:context=".PcView" > tools:context=".PcView" >
<ListView <RelativeLayout
android:id="@+id/pcListView" android:id="@+id/no_pc_found_layout"
android:layout_width="match_parent" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:layout_centerVertical="true"
android:layout_centerHorizontal="true">
<ImageView
android:id="@+id/pcs_loading"
android:layout_width="75dp"
android:layout_height="75dp"
android:src="@drawable/image_loading"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_toRightOf="@+id/pcs_loading"
android:layout_toEndOf="@+id/pcs_loading"
android:layout_marginLeft="5dp"
android:layout_marginStart="5dp"
android:layout_centerVertical="true"
android:textAppearance="?android:attr/textAppearanceLarge"
android:gravity="center"
android:text="@string/searching_pc"/>
</RelativeLayout>
<FrameLayout
android:id="@+id/pcFragmentContainer"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:layout_alignParentBottom="true" android:layout_alignParentBottom="true"
android:layout_alignParentLeft="true" android:layout_alignParentTop="true"
android:layout_alignParentRight="true" android:layout_toLeftOf="@+id/manuallyAddPc"
android:layout_below="@+id/settingsButton" android:layout_toStartOf="@+id/manuallyAddPc"
android:background="@drawable/list_view_unselected" android:layout_toRightOf="@+id/settingsButton"
android:fastScrollEnabled="true" android:layout_toEndOf="@+id/settingsButton"/>
android:longClickable="false"
android:stackFromBottom="false" >
</ListView> <ImageButton
<TextView
android:id="@+id/discoveryText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="?android:attr/textAppearanceLarge"
android:layout_centerHorizontal="true"
android:layout_alignBaseline="@+id/settingsButton"
android:text="@string/title_pc_view" />
<Button
android:id="@+id/settingsButton" android:id="@+id/settingsButton"
android:layout_width="wrap_content" android:layout_width="70dp"
android:layout_height="wrap_content" android:layout_height="65dp"
android:layout_alignLeft="@+id/pcListView" android:cropToPadding="false"
android:scaleType="fitXY"
android:nextFocusDown="@+id/pcGridView"
android:layout_alignParentStart="true"
android:layout_alignParentLeft="true"
android:layout_alignParentTop="true" android:layout_alignParentTop="true"
android:layout_marginTop="10dp" android:src="@drawable/settings"
android:layout_marginBottom="15dp" style="?android:attr/borderlessButtonStyle"/>
android:text="@string/button_stream_settings" />
<Button <ImageButton
android:id="@+id/manuallyAddPc" android:id="@+id/manuallyAddPc"
android:layout_width="wrap_content" android:layout_width="70dp"
android:layout_height="wrap_content" android:layout_height="65dp"
android:layout_alignRight="@+id/pcListView" android:cropToPadding="false"
android:scaleType="fitXY"
android:nextFocusDown="@+id/pcGridView"
android:layout_alignParentTop="true" android:layout_alignParentTop="true"
android:layout_marginTop="10dp" android:layout_alignParentRight="true"
android:layout_marginBottom="15dp" android:layout_alignParentEnd="true"
android:text="@string/button_add_pc_manually" /> android:src="@drawable/add_computer"
style="?android:attr/borderlessButtonStyle"/>
</RelativeLayout> </RelativeLayout>
@@ -8,46 +8,65 @@
android:paddingTop="@dimen/activity_vertical_margin" android:paddingTop="@dimen/activity_vertical_margin"
tools:context=".PcView" > tools:context=".PcView" >
<ListView <RelativeLayout
android:id="@+id/pcListView" android:id="@+id/no_pc_found_layout"
android:layout_width="match_parent" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:layout_centerVertical="true"
android:layout_centerHorizontal="true">
<ImageView
android:id="@+id/pcs_loading"
android:layout_width="75dp"
android:layout_height="75dp"
android:src="@drawable/image_loading"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_toRightOf="@+id/pcs_loading"
android:layout_toEndOf="@+id/pcs_loading"
android:layout_marginLeft="5dp"
android:layout_marginStart="5dp"
android:layout_centerVertical="true"
android:textAppearance="?android:attr/textAppearanceLarge"
android:gravity="center"
android:text="@string/searching_pc"/>
</RelativeLayout>
<FrameLayout
android:id="@+id/pcFragmentContainer"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:layout_toLeftOf="@+id/manuallyAddPc"
android:layout_toStartOf="@+id/manuallyAddPc"
android:layout_toRightOf="@+id/settingsButton"
android:layout_toEndOf="@+id/settingsButton"
android:layout_alignParentBottom="true" android:layout_alignParentBottom="true"
android:layout_alignParentLeft="true" android:layout_alignParentTop="true"/>
android:layout_alignParentRight="true"
android:layout_below="@+id/discoveryText"
android:background="@drawable/list_view_unselected"
android:fastScrollEnabled="true"
android:longClickable="false"
android:stackFromBottom="false" >
</ListView> <ImageButton
<TextView
android:id="@+id/discoveryText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerHorizontal="true"
android:textAppearance="?android:attr/textAppearanceLarge"
android:layout_below="@+id/manuallyAddPc"
android:paddingTop="10dp"
android:paddingBottom="10dp"
android:text="@string/title_pc_view" />
<Button
android:id="@+id/settingsButton" android:id="@+id/settingsButton"
android:layout_width="wrap_content" android:layout_width="70dp"
android:layout_height="wrap_content" android:layout_height="65dp"
android:layout_centerHorizontal="true" android:cropToPadding="false"
android:scaleType="fitXY"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_alignParentTop="true" android:layout_alignParentTop="true"
android:text="@string/button_stream_settings" /> android:src="@drawable/settings"
style="?android:attr/borderlessButtonStyle"/>
<Button <ImageButton
android:id="@+id/manuallyAddPc" android:id="@+id/manuallyAddPc"
android:layout_width="wrap_content" android:layout_width="70dp"
android:layout_height="wrap_content" android:layout_height="65dp"
android:layout_below="@+id/settingsButton" android:cropToPadding="false"
android:layout_centerHorizontal="true" android:scaleType="fitXY"
android:text="@string/button_add_pc_manually" /> android:layout_alignParentTop="true"
android:layout_alignParentEnd="true"
android:layout_alignParentRight="true"
android:src="@drawable/add_computer"
style="?android:attr/borderlessButtonStyle"/>
</RelativeLayout> </RelativeLayout>
@@ -5,16 +5,28 @@
android:paddingBottom="@dimen/activity_vertical_margin" android:paddingBottom="@dimen/activity_vertical_margin"
android:paddingLeft="@dimen/activity_horizontal_margin" android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin" android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="10dp" android:paddingTop="@dimen/activity_vertical_margin"
tools:context=".Connection" > tools:context=".AddComputerManually" >
<TextView
android:id="@+id/manuallyAddPcText"
android:text="@string/title_add_pc"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerHorizontal="true"
android:textAppearance="?android:attr/textAppearanceLarge"
android:layout_alignParentTop="true"/>
<EditText <EditText
android:id="@+id/hostTextView" android:id="@+id/hostTextView"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_below="@+id/manuallyAddPcText"
android:layout_marginTop="25dp"
android:layout_alignParentLeft="true" android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_alignParentRight="true" android:layout_alignParentRight="true"
android:layout_alignParentTop="true" android:layout_alignParentEnd="true"
android:ems="10" android:ems="10"
android:singleLine="true" android:singleLine="true"
android:inputType="textNoSuggestions" android:inputType="textNoSuggestions"
@@ -23,12 +35,4 @@
<requestFocus /> <requestFocus />
</EditText> </EditText>
<Button
android:id="@+id/addPc"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/hostTextView"
android:layout_centerHorizontal="true"
android:text="@string/button_add_pc" />
</RelativeLayout> </RelativeLayout>
+10 -11
View File
@@ -8,28 +8,27 @@
android:paddingTop="@dimen/activity_vertical_margin" android:paddingTop="@dimen/activity_vertical_margin"
tools:context=".AppView" > tools:context=".AppView" >
<ListView <FrameLayout
android:id="@+id/pcListView" android:id="@+id/appFragmentContainer"
android:layout_width="match_parent" android:layout_width="fill_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:gravity="center"
android:layout_alignParentLeft="true" android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_alignParentRight="true" android:layout_alignParentRight="true"
android:layout_alignParentEnd="true"
android:layout_alignParentBottom="true" android:layout_alignParentBottom="true"
android:layout_below="@+id/appListText" android:layout_below="@+id/appListText"/>
android:fastScrollEnabled="true"
android:longClickable="false"
android:background="@drawable/list_view_unselected"
android:stackFromBottom="false">
</ListView>
<TextView <TextView
android:id="@+id/appListText" android:id="@+id/appListText"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_centerHorizontal="true" android:layout_centerHorizontal="true"
android:textAppearance="?android:attr/textAppearanceLarge"
android:layout_alignParentTop="true" android:layout_alignParentTop="true"
android:gravity="center"
android:paddingTop="0dp" android:paddingTop="0dp"
android:paddingBottom="10dp" /> android:paddingBottom="10dp"
android:textSize="28sp"/>
</RelativeLayout> </RelativeLayout>
+2 -3
View File
@@ -1,8 +1,7 @@
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" <merge xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:background="#000"
tools:context=".Game" > tools:context=".Game" >
<SurfaceView <SurfaceView
@@ -11,4 +10,4 @@
android:layout_height="match_parent" android:layout_height="match_parent"
android:layout_gravity="center" /> android:layout_gravity="center" />
</FrameLayout> </merge>
+41
View File
@@ -0,0 +1,41 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="20dp">
<RelativeLayout
android:id="@+id/grid_image_layout"
android:layout_centerHorizontal="true"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<ImageView
android:id="@+id/grid_image"
android:cropToPadding="false"
android:scaleType="fitXY"
android:layout_centerHorizontal="true"
android:layout_width="150dp"
android:layout_height="175dp">
</ImageView>
<ImageView
android:id="@+id/grid_overlay"
android:layout_centerHorizontal="true"
android:layout_centerVertical="true"
android:layout_width="50dp"
android:layout_height="50dp">
</ImageView>
</RelativeLayout>
<TextView
android:id="@+id/grid_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/grid_image_layout"
android:layout_marginTop="10dp"
android:layout_centerHorizontal="true"
android:gravity="center"
android:singleLine="true"
android:ellipsize="marquee"
android:marqueeRepeatLimit="marquee_forever"
android:scrollHorizontally="true"
android:textSize="18sp" >
</TextView>
</RelativeLayout>
@@ -0,0 +1,41 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="10dp">
<RelativeLayout
android:id="@+id/grid_image_layout"
android:layout_centerHorizontal="true"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<ImageView
android:id="@+id/grid_image"
android:cropToPadding="false"
android:scaleType="fitXY"
android:layout_centerHorizontal="true"
android:layout_width="100dp"
android:layout_height="117dp">
</ImageView>
<ImageView
android:id="@+id/grid_overlay"
android:layout_centerHorizontal="true"
android:layout_centerVertical="true"
android:layout_width="33dp"
android:layout_height="33dp">
</ImageView>
</RelativeLayout>
<TextView
android:id="@+id/grid_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/grid_image_layout"
android:layout_marginTop="10dp"
android:layout_centerHorizontal="true"
android:gravity="center"
android:singleLine="true"
android:ellipsize="marquee"
android:marqueeRepeatLimit="marquee_forever"
android:scrollHorizontally="true"
android:textSize="14sp" >
</TextView>
</RelativeLayout>
+15
View File
@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical" android:layout_width="match_parent"
android:layout_height="match_parent">
<GridView
android:id="@+id/fragmentView"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:numColumns="auto_fit"
android:columnWidth="160dp"
android:stretchMode="spacingWidth"
android:focusable="true"
android:focusableInTouchMode="true"
android:gravity="center"/>
</LinearLayout>
@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical" android:layout_width="match_parent"
android:layout_height="match_parent">
<GridView
android:id="@+id/fragmentView"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:numColumns="auto_fit"
android:columnWidth="105dp"
android:stretchMode="spacingWidth"
android:focusable="true"
android:focusableInTouchMode="true"
android:gravity="center"/>
</LinearLayout>
+19
View File
@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical" android:layout_width="match_parent"
android:layout_height="match_parent">
<ListView
android:id="@+id/fragmentView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@drawable/list_view_unselected"
android:fastScrollEnabled="true"
android:longClickable="false"
android:focusable="true"
android:focusableInTouchMode="true"
android:stackFromBottom="false" >
</ListView>
</LinearLayout>
+37
View File
@@ -0,0 +1,37 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="20dp">
<RelativeLayout
android:id="@+id/grid_image_layout"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<ImageView
android:id="@+id/grid_image"
android:layout_centerHorizontal="true"
android:layout_width="150dp"
android:layout_height="100dp">
</ImageView>
<ImageView
android:id="@+id/grid_overlay"
android:layout_marginTop="15dp"
android:layout_marginLeft="65dp"
android:layout_marginStart="65dp"
android:layout_marginRight="20dp"
android:layout_marginEnd="20dp"
android:layout_width="50dp"
android:layout_height="50dp">
</ImageView>
</RelativeLayout>
<TextView
android:id="@+id/grid_text"
android:layout_width="150dp"
android:layout_height="wrap_content"
android:layout_below="@id/grid_image_layout"
android:layout_marginTop="10dp"
android:layout_centerHorizontal="true"
android:gravity="center"
android:textSize="18sp" >
</TextView>
</RelativeLayout>
@@ -0,0 +1,37 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="15dp">
<RelativeLayout
android:id="@+id/grid_image_layout"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<ImageView
android:id="@+id/grid_image"
android:layout_centerHorizontal="true"
android:layout_width="100dp"
android:layout_height="67dp">
</ImageView>
<ImageView
android:id="@+id/grid_overlay"
android:layout_marginTop="10dp"
android:layout_marginLeft="42dp"
android:layout_marginStart="42dp"
android:layout_marginRight="13dp"
android:layout_marginEnd="13dp"
android:layout_width="33dp"
android:layout_height="33dp">
</ImageView>
</RelativeLayout>
<TextView
android:id="@+id/grid_text"
android:layout_width="100dp"
android:layout_height="wrap_content"
android:layout_below="@id/grid_image_layout"
android:layout_marginTop="10dp"
android:layout_centerHorizontal="true"
android:gravity="center"
android:textSize="14sp" >
</TextView>
</RelativeLayout>
+14
View File
@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical" android:layout_width="match_parent"
android:layout_height="match_parent">
<GridView
android:id="@+id/fragmentView"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:numColumns="auto_fit"
android:columnWidth="160dp"
android:focusable="true"
android:focusableInTouchMode="true"
android:gravity="center"/>
</LinearLayout>
@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical" android:layout_width="match_parent"
android:layout_height="match_parent">
<GridView
android:id="@+id/fragmentView"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:numColumns="auto_fit"
android:columnWidth="105dp"
android:focusable="true"
android:focusableInTouchMode="true"
android:gravity="center"/>
</LinearLayout>
@@ -5,7 +5,7 @@
android:layout_height="wrap_content"> android:layout_height="wrap_content">
<TextView <TextView
android:id="@+id/rowTextView" android:id="@+id/grid_text"
android:layout_width="fill_parent" android:layout_width="fill_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:textIsSelectable="false" android:textIsSelectable="false"
+15
View File
@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string-array name="resolution_names">
<item>720p - 30 FPS</item>
<item>720p - 60 FPS</item>
<item>1080p - 30 FPS</item>
<item>1080p - 60 FPS</item>
</string-array>
<string-array name="decoder_names">
<item>Scegli decoder automaticamente</item>
<item>Forza decoder software</item>
<item>Forza decoder hardware</item>
</string-array>
</resources>
+120
View File
@@ -0,0 +1,120 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- PC view menu entries -->
<string name="pcview_menu_app_list">Lista applicazioni</string>
<string name="pcview_menu_pair_pc">Accoppia PC</string>
<string name="pcview_menu_unpair_pc">Disaccoppia PC</string>
<string name="pcview_menu_send_wol">Invia richiesta Wake-On-LAN</string>
<string name="pcview_menu_delete_pc">Rimuovi PC</string>
<!-- Pair messages -->
<string name="pairing">Accoppiamento…</string>
<string name="pair_pc_offline">PC offline</string>
<string name="pair_pc_ingame">PC con applicazione avviata. Devi chiudere l\'applicazione prima dell\'accoppiamento.</string>
<string name="pair_pairing_title">Accoppiamento</string>
<string name="pair_pairing_msg">Inserisci il seguente PIN sul PC:</string>
<string name="pair_incorrect_pin">PIN non corretto</string>
<string name="pair_fail">Accoppiamento fallito</string>
<!-- WOL messages -->
<string name="wol_pc_online">PC già avviato</string>
<string name="wol_no_mac">Impossibile risvegliare il PC perchè GFE non ha inviato nessun indirizzo MAC</string>
<string name="wol_waking_pc">Risveglio PC…</string>
<string name="wol_waking_msg">Il PC potrebbe impiegare qualche secondo per risvegliarsi.
Se non succede niente, assicurati che l\'opzione Wake-On-LAN sia configurata correttamente.
</string>
<string name="wol_fail">Invio pacchetto Wake-On-LAN fallito</string>
<!-- Unpair messages -->
<string name="unpairing">Disaccoppiamento…</string>
<string name="unpair_success">Disaccoppiato con successo</string>
<string name="unpair_fail">Disaccoppiamento fallito</string>
<string name="unpair_error">PC non accoppiato</string>
<!-- Errors -->
<string name="error_pc_offline">PC offline</string>
<string name="error_manager_not_running">Il servizio ComputerManager non è avviato. Attendi qualche secondo o riavvia l\'applicazione.</string>
<string name="error_unknown_host">Risoluzione nome host fallita</string>
<string name="error_404">GFE ha ritornato un errore HTTP 404 error. Assicurati che il PC stia usando una GPU supportata.
Usare un software di remote-desktop può causare questo errore. Prova a riavviare il PC o a reinstallare GFE.
</string>
<!-- Start application messages -->
<string name="conn_establishing_title">Connessione</string>
<string name="conn_establishing_msg">Connessione in corso</string>
<string name="conn_metered">Attenzione: la rete attiva prevede costi aggiuntivi in base all\'utilizzo!</string>
<string name="conn_client_latency">Latenza frame media client-side:</string>
<string name="conn_client_latency_hw">latenza decoder hardware:</string>
<string name="conn_hardware_latency">Latenza decoder hardware media:</string>
<string name="conn_starting">Avvio in corso…</string>
<string name="conn_error_title">Errore connessione</string>
<string name="conn_error_msg">Avvio fallito</string>
<string name="conn_terminated_title">Connessione terminata</string>
<string name="conn_terminated_msg">La connessione è stata interrotta</string>
<!-- General strings -->
<string name="ip_hint">Indirizzo IP del PC</string>
<string name="searching_pc">Ricerca PC in corso…</string>
<string name="yes"></string>
<string name="no">No</string>
<string name="lost_connection">Connessione con il PC persa</string>
<!-- AppList activity -->
<string name="title_applist">Applicazioni su</string>
<string name="applist_menu_resume">Riprendi Sessione</string>
<string name="applist_menu_quit">Chiudi Sessione</string>
<string name="applist_menu_quit_and_start">Chiudi sessione corrente e avvia</string>
<string name="applist_menu_cancel">Annulla</string>
<string name="applist_refresh_title">Lista applicazioni</string>
<string name="applist_refresh_msg">Aggiornamento lista in corso…</string>
<string name="applist_refresh_error_title">Errore</string>
<string name="applist_refresh_error_msg">Ricezione lista applicazioni fallita</string>
<string name="applist_quit_app">Chiusura in corso…</string>
<string name="applist_quit_success">Sessione chiusa con successo</string>
<string name="applist_quit_fail">Chiusura sessione fallita</string>
<string name="applist_quit_confirmation">Sei sicuro di voler chiudere l\'applicazione avviata? Tutti i dati non salvati saranno persi.</string>
<!-- Add computer manually activity -->
<string name="title_add_pc">Aggiungi PC Manualmente</string>
<string name="msg_add_pc">Connessione al PC in corso…</string>
<string name="addpc_fail">Impossibile connettersi al PC. Assicurati che il firewall del PC sia configurato correttamente.</string>
<string name="addpc_success">PC aggiunto con successo</string>
<string name="addpc_unknown_host">Impossibile risovere l\'indirizzo del PC. Assicurati di aver scritto correttamente l\'indirizzo.</string>
<string name="addpc_enter_ip">Devi inserire un indirizzo IP</string>
<!-- Preferences -->
<string name="category_basic_settings">Impostazioni Base</string>
<string name="title_resolution_list">Risoluzione e FPS</string>
<string name="summary_resolution_list">Valori troppo elevati possono causare lag o crash</string>
<string name="title_seekbar_bitrate">Bitrate video</string>
<string name="summary_seekbar_bitrate">Abbassa il bitrate per ridurre lo stuttering; alza il bitrate per aumenteare la qualità dell\'immagine</string>
<string name="suffix_seekbar_bitrate">Mbps</string>
<string name="title_checkbox_stretch_video">Forza video in full-screen</string>
<string name="title_checkbox_disable_warnings">Disabilita messaggi di warning</string>
<string name="summary_checkbox_disable_warnings">Disabilita i messaggi di warning sullo schermo durante lo streaming</string>
<string name="category_gamepad_settings">Impostazioni Gamepad</string>
<string name="title_checkbox_multi_controller">Supporto controller multipli</string>
<string name="summary_checkbox_multi_controller">Quando disabilitato, tutti i controllers appaiono come uno solo</string>
<string name="title_seekbar_deadzone">Aggiusta deadzone degli stick analogici</string>
<string name="suffix_seekbar_deadzone">%</string>
<string name="category_ui_settings">Impostazioni Interfaccia</string>
<string name="title_language_list">Lingua</string>
<string name="summary_language_list">Lingua da usare in Limelight</string>
<string name="title_checkbox_list_mode">Usa lista invece della griglia</string>
<string name="summary_checkbox_list_mode">Visualizza applicazioni e computers in una lista invece di una griglia</string>
<string name="title_checkbox_small_icon_mode">Usa icone piccole</string>
<string name="summary_checkbox_small_icon_mode">Usa icone piccole nella vista a griglia per avere più oggetti sullo schermo</string>
<string name="category_host_settings">Impostazioni Host</string>
<string name="title_checkbox_enable_sops">Ottimizza le impostazioni dei giochi</string>
<string name="summary_checkbox_enable_sops">Permetti a GFE di modificare le impostazioni dei giochi per uno streaming ottimale</string>
<string name="title_checkbox_host_audio">Riproduci audio sul PC</string>
<string name="summary_checkbox_host_audio">Riproduci l\'audio sul computer e su questo dispositivo</string>
<string name="category_advanced_settings">Impostazioni Avanzate</string>
<string name="title_decoder_list">Cambia decoder</string>
<string name="summary_decoder_list">Il decoder software può ridurre la latenza video quando si usano impostazioni streaming basse</string>
</resources>
@@ -1,8 +0,0 @@
<resources>
<!--
Customize dimensions originally defined in res/values/dimens.xml (such as
screen margins) for sw600dp devices (e.g. 7" tablets) here.
-->
</resources>
@@ -1,9 +0,0 @@
<resources>
<!--
Customize dimensions originally defined in res/values/dimens.xml (such as
screen margins) for sw720dp devices (e.g. 10" tablets) in landscape here.
-->
<dimen name="activity_horizontal_margin">128dp</dimen>
</resources>
+1 -1
View File
@@ -3,7 +3,7 @@
<!-- <!--
Base application theme for API 21+. This theme completely replaces Base application theme for API 21+. This theme completely replaces
AppBaseTheme from BOTH res/values/styles.xml and AppBaseTheme from BOTH res/values/styles.xml and
res/values-v11/styles.xml on API 21+ devices. res/values-v21/styles.xml on API 21+ devices.
--> -->
<style name="AppBaseTheme" parent="android:Theme.Material"> <style name="AppBaseTheme" parent="android:Theme.Material">
<!-- API 21 theme customizations can go here. --> <!-- API 21 theme customizations can go here. -->
+15 -4
View File
@@ -6,21 +6,32 @@
<item>1080p 30 FPS</item> <item>1080p 30 FPS</item>
<item>1080p 60 FPS</item> <item>1080p 60 FPS</item>
</string-array> </string-array>
<string-array name="resolution_values"> <string-array name="resolution_values" translatable="false">
<item>720p30</item> <item>720p30</item>
<item>720p60</item> <item>720p60</item>
<item>1080p30</item> <item>1080p30</item>
<item>1080p60</item> <item>1080p60</item>
</string-array> </string-array>
<string-array name="language_names" translatable="false">
<item>Default</item>
<item>English</item>
<item>Italiano</item>
</string-array>
<string-array name="language_values" translatable="false">
<item>default</item>
<item>en</item>
<item>it</item>
</string-array>
<string-array name="decoder_names"> <string-array name="decoder_names">
<item>Force Software Decoding</item>
<item>Auto-select Decoder</item> <item>Auto-select Decoder</item>
<item>Force Software Decoding</item>
<item>Force Hardware Decoding</item> <item>Force Hardware Decoding</item>
</string-array> </string-array>
<string-array name="decoder_values"> <string-array name="decoder_values" translatable="false">
<item>software</item>
<item>auto</item> <item>auto</item>
<item>software</item>
<item>hardware</item> <item>hardware</item>
</string-array> </string-array>
</resources> </resources>
-14
View File
@@ -1,14 +0,0 @@
<resources>
<!--
Declare custom theme attributes that allow changing which styles are
used for button bars depending on the API level.
?android:attr/buttonBarStyle is new as of API 11 so this is
necessary to support previous API levels.
-->
<declare-styleable name="ButtonBarContainerTheme">
<attr name="buttonBarStyle" format="reference" />
<attr name="buttonBarButtonStyle" format="reference" />
</declare-styleable>
</resources>
+92 -6
View File
@@ -1,16 +1,87 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<!-- PC view menu entries -->
<string name="pcview_menu_app_list">View Game List</string>
<string name="pcview_menu_pair_pc">Pair with PC</string>
<string name="pcview_menu_unpair_pc">Unpair</string>
<string name="pcview_menu_send_wol">Send Wake-On-LAN request</string>
<string name="pcview_menu_delete_pc">Delete PC</string>
<!-- Pair messages -->
<string name="pairing">Pairing…</string>
<string name="pair_pc_offline">Computer is offline</string>
<string name="pair_pc_ingame">Computer is currently in a game. You must close the game before pairing.</string>
<string name="pair_pairing_title">Pairing</string>
<string name="pair_pairing_msg">Please enter the following PIN on the target PC:</string>
<string name="pair_incorrect_pin">Incorrect PIN</string>
<string name="pair_fail">Pairing failed</string>
<!-- WOL messages -->
<string name="wol_pc_online">Computer is online</string>
<string name="wol_no_mac">Unable to wake PC because GFE didn\'t send a MAC address</string>
<string name="wol_waking_pc">Waking PC…</string>
<string name="wol_waking_msg">It may take a few seconds for your PC to wake up.
If it doesn\'t, make sure it\'s configured properly for Wake-On-LAN.
</string>
<string name="wol_fail">Failed to send Wake-On-LAN packets</string>
<!-- Unpair messages -->
<string name="unpairing">Unpairing…</string>
<string name="unpair_success">Unpaired successfully</string>
<string name="unpair_fail">Failed to unpair</string>
<string name="unpair_error">Device was not paired</string>
<!-- Errors -->
<string name="error_pc_offline">Computer is offline</string>
<string name="error_manager_not_running">The ComputerManager service is not running. Please wait a few seconds or restart the app.</string>
<string name="error_unknown_host">Failed to resolve host</string>
<string name="error_404">GFE returned an HTTP 404 error. Make sure your PC is running a supported GPU.
Using remote desktop software can also cause this error. Try rebooting your machine or reinstalling GFE.
</string>
<!-- Start application messages -->
<string name="conn_establishing_title">Establishing Connection</string>
<string name="conn_establishing_msg">Starting connection</string>
<string name="conn_metered">Warning: Your active network connection is metered!</string>
<string name="conn_client_latency">Average client-side frame latency:</string>
<string name="conn_client_latency_hw">hardware decoder latency:</string>
<string name="conn_hardware_latency">Average hardware decoder latency:</string>
<string name="conn_starting">Starting</string>
<string name="conn_error_title">Connection Error</string>
<string name="conn_error_msg">Failed to start</string>
<string name="conn_terminated_title">Connection Terminated</string>
<string name="conn_terminated_msg">The connection was terminated</string>
<!-- General strings --> <!-- General strings -->
<string name="ip_hint">IP address of GeForce PC</string> <string name="ip_hint">IP address of GeForce PC</string>
<string name="searching_pc">Searching for PCs…</string>
<string name="yes">Yes</string>
<string name="no">No</string>
<string name="lost_connection">Lost connection to PC</string>
<!-- PC view activity --> <!-- AppList activity -->
<string name="title_pc_view">PC List</string> <string name="title_applist">Apps on</string>
<string name="button_stream_settings">Streaming Settings</string> <string name="applist_menu_resume">Resume Session</string>
<string name="button_add_pc_manually">Add PC Manually</string> <string name="applist_menu_quit">Quit Session</string>
<string name="applist_menu_quit_and_start">Quit Current Game and Start</string>
<string name="applist_menu_cancel">Cancel</string>
<string name="applist_refresh_title">App List</string>
<string name="applist_refresh_msg">Refreshing apps…</string>
<string name="applist_refresh_error_title">Error</string>
<string name="applist_refresh_error_msg">Failed to get app list</string>
<string name="applist_quit_app">Quitting</string>
<string name="applist_quit_success">Successfully quit</string>
<string name="applist_quit_fail">Failed to quit</string>
<string name="applist_quit_confirmation">Are you sure you want to quit the running app? All unsaved data will be lost.</string>
<!-- Add computer manually activity --> <!-- Add computer manually activity -->
<string name="button_add_pc">Manually Add PC</string> <string name="title_add_pc">Add PC Manually</string>
<string name="msg_add_pc">Connecting to the PC…</string>
<string name="addpc_fail">Unable to connect to the specified computer. Make sure the required ports are allowed through the firewall.</string>
<string name="addpc_success">Successfully added computer</string>
<string name="addpc_unknown_host">Unable to resolve PC address. Make sure you didn\'t make a typo in the address.</string>
<string name="addpc_enter_ip">You must enter an IP address</string>
<!-- Preferences --> <!-- Preferences -->
<string name="category_basic_settings">Basic Settings</string> <string name="category_basic_settings">Basic Settings</string>
@@ -23,12 +94,27 @@
<string name="title_checkbox_disable_warnings">Disable warning messages</string> <string name="title_checkbox_disable_warnings">Disable warning messages</string>
<string name="summary_checkbox_disable_warnings">Disable on-screen connection warning messages while streaming</string> <string name="summary_checkbox_disable_warnings">Disable on-screen connection warning messages while streaming</string>
<string name="category_gamepad_settings">Gamepad Settings</string>
<string name="title_checkbox_multi_controller">Multiple controller support</string>
<string name="summary_checkbox_multi_controller">When unchecked, all controllers appear as one</string>
<string name="title_seekbar_deadzone">Adjust analog stick deadzone</string>
<string name="suffix_seekbar_deadzone">%</string>
<string name="category_ui_settings">UI Settings</string>
<string name="title_language_list">Language</string>
<string name="summary_language_list">Language to use for Limelight</string>
<string name="title_checkbox_list_mode">Use lists instead of grids</string>
<string name="summary_checkbox_list_mode">Display apps and PCs in lists instead of grids</string>
<string name="title_checkbox_small_icon_mode">Use small icons</string>
<string name="summary_checkbox_small_icon_mode">Use small icons in grid items to allow more items on screen</string>
<string name="category_host_settings">Host Settings</string> <string name="category_host_settings">Host Settings</string>
<string name="title_checkbox_enable_sops">Optimize game settings</string> <string name="title_checkbox_enable_sops">Optimize game settings</string>
<string name="summary_checkbox_enable_sops">Allow GFE to modify game settings for optimal streaming</string> <string name="summary_checkbox_enable_sops">Allow GFE to modify game settings for optimal streaming</string>
<string name="title_checkbox_host_audio">Play audio on PC</string> <string name="title_checkbox_host_audio">Play audio on PC</string>
<string name="summary_checkbox_host_audio">Play audio from the computer and this device. Requires GFE 2.1.2+</string> <string name="summary_checkbox_host_audio">Play audio from the computer and this device</string>
<string name="category_advanced_settings">Advanced Settings</string> <string name="category_advanced_settings">Advanced Settings</string>
<string name="title_decoder_list">Change decoder</string> <string name="title_decoder_list">Change decoder</string>
<string name="summary_decoder_list">Software decoding may improve video latency at lower streaming settings</string>
</resources> </resources>
+9 -14
View File
@@ -4,12 +4,14 @@
<!-- <!--
Base application theme, dependent on API level. This theme is replaced Base application theme, dependent on API level. This theme is replaced
by AppBaseTheme from res/values-vXX/styles.xml on newer devices. by AppBaseTheme from res/values-vXX/styles.xml on newer devices.
--> -->
<style name="AppBaseTheme" parent="android:Theme"> <style name="AppBaseTheme" parent="android:Theme">
<!-- <!--
Theme customizations available in newer API levels can go in Theme customizations available in newer API levels can go in
res/values-vXX/styles.xml, while customizations related to res/values-vXX/styles.xml, while customizations related to
backward-compatibility can go here. backward-compatibility can go here.
--> -->
</style> </style>
@@ -20,21 +22,14 @@
<item name="android:windowNoTitle">true</item> <item name="android:windowNoTitle">true</item>
</style> </style>
</style> <!-- Stream activity theme -->
<style name="StreamTheme" parent="AppBaseTheme">
<style name="FullscreenTheme" parent="android:Theme.NoTitleBar"> <!-- All customizations that are NOT specific to a particular API-level can go here. -->
<item name="android:windowContentOverlay">@null</item> <item name="android:windowActionBar">false</item>
<item name="android:windowBackground">@null</item> <item name="android:windowNoTitle">true</item>
<item name="buttonBarStyle">@style/ButtonBar</item>
</style> <!-- Transparent streaming background to avoid extra overdraw -->
<item name="android:windowBackground">@android:color/transparent</item>
<style name="ButtonBar">
<item name="android:paddingLeft">2dp</item>
<item name="android:paddingTop">5dp</item>
<item name="android:paddingRight">2dp</item>
</style> </style>
</style>
</resources> </resources>
+32
View File
@@ -26,6 +26,19 @@
android:summary="@string/summary_checkbox_disable_warnings" android:summary="@string/summary_checkbox_disable_warnings"
android:defaultValue="false" /> android:defaultValue="false" />
</PreferenceCategory> </PreferenceCategory>
<PreferenceCategory android:title="@string/category_gamepad_settings">
<!--com.limelight.preferences.SeekBarPreference
android:key="seekbar_deadzone"
android:defaultValue="15"
android:max="50"
android:text="@string/suffix_seekbar_deadzone"
android:title="@string/title_seekbar_deadzone"/-->
<CheckBoxPreference
android:key="checkbox_multi_controller"
android:title="@string/title_checkbox_multi_controller"
android:summary="@string/summary_checkbox_multi_controller"
android:defaultValue="true" />
</PreferenceCategory>
<PreferenceCategory android:title="@string/category_host_settings"> <PreferenceCategory android:title="@string/category_host_settings">
<CheckBoxPreference <CheckBoxPreference
android:key="checkbox_enable_sops" android:key="checkbox_enable_sops"
@@ -38,12 +51,31 @@
android:summary="@string/summary_checkbox_host_audio" android:summary="@string/summary_checkbox_host_audio"
android:defaultValue="false" /> android:defaultValue="false" />
</PreferenceCategory> </PreferenceCategory>
<PreferenceCategory android:title="@string/category_ui_settings">
<ListPreference
android:key="list_languages"
android:title="@string/title_language_list"
android:entries="@array/language_names"
android:entryValues="@array/language_values"
android:summary="@string/summary_language_list"
android:defaultValue="default" />
<com.limelight.preferences.SmallIconCheckboxPreference
android:key="checkbox_small_icon_mode"
android:title="@string/title_checkbox_small_icon_mode"
android:summary="@string/summary_checkbox_small_icon_mode" />
<CheckBoxPreference
android:key="checkbox_list_mode"
android:title="@string/title_checkbox_list_mode"
android:summary="@string/summary_checkbox_list_mode"
android:defaultValue="false" />
</PreferenceCategory>
<PreferenceCategory android:title="@string/category_advanced_settings"> <PreferenceCategory android:title="@string/category_advanced_settings">
<ListPreference <ListPreference
android:key="list_decoders" android:key="list_decoders"
android:title="@string/title_decoder_list" android:title="@string/title_decoder_list"
android:entries="@array/decoder_names" android:entries="@array/decoder_names"
android:entryValues="@array/decoder_values" android:entryValues="@array/decoder_values"
android:summary="@string/summary_decoder_list"
android:defaultValue="auto" /> android:defaultValue="auto" />
</PreferenceCategory> </PreferenceCategory>
+1 -2
View File
@@ -1,6 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android">
package="com.limelight" >
<!-- Non-root application name --> <!-- Non-root application name -->
<application android:label="Limelight" /> <application android:label="Limelight" />
+1 -2
View File
@@ -1,6 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android">
package="com.limelight.root" >
<!-- Root permissions --> <!-- Root permissions -->
<uses-permission android:name="android.permission.ACCESS_SUPERUSER" /> <uses-permission android:name="android.permission.ACCESS_SUPERUSER" />
+1 -1
View File
@@ -4,7 +4,7 @@ buildscript {
jcenter() jcenter()
} }
dependencies { dependencies {
classpath 'com.android.tools.build:gradle:0.13.2' classpath 'com.android.tools.build:gradle:1.1.0'
} }
} }
+8 -2
View File
@@ -4,12 +4,12 @@ This file serves to document some of the decoder errata when using MediaCodec ha
- Affected decoders: TI OMAP4, Allwinner A20 - Affected decoders: TI OMAP4, Allwinner A20
2. Some decoders have a huge per-frame latency with the unmodified SPS sent from NVENC. Setting max_dec_frame_buffering fixes this latency issue. 2. Some decoders have a huge per-frame latency with the unmodified SPS sent from NVENC. Setting max_dec_frame_buffering fixes this latency issue.
- Affected decoders: NVIDIA Tegra 3 and 4 - Affected decoders: NVIDIA Tegra 3 and 4, Broadcom VideoCore IV
3. Some decoders strictly require that you pass BUFFER_FLAG_CODEC_CONFIG and crash upon the IDR frame if you don't 3. Some decoders strictly require that you pass BUFFER_FLAG_CODEC_CONFIG and crash upon the IDR frame if you don't
- Affected decoders: TI OMAP4 - Affected decoders: TI OMAP4
4. Some decoders require num_ref_frames=1 and max_dec_frame_buffering=1 to avoid crashing on SPS or first I-frame 4. Some decoders require num_ref_frames=1 and max_dec_frame_buffering=1 to avoid crashing on SPS on first I-frame
- Affected decoders: Qualcomm in GS3 on 4.3+, Exynos 4 at 1080p only - Affected decoders: Qualcomm in GS3 on 4.3+, Exynos 4 at 1080p only
5. Some decoders will hang if max_dec_frame_buffering is not present 5. Some decoders will hang if max_dec_frame_buffering is not present
@@ -17,3 +17,9 @@ This file serves to document some of the decoder errata when using MediaCodec ha
6. Some decoders will hang if max_dec_frame_buffering IS present 6. Some decoders will hang if max_dec_frame_buffering IS present
- Affected decoders: Exynos 5 in Galaxy Note 10.1 (2014) - Affected decoders: Exynos 5 in Galaxy Note 10.1 (2014)
7. Some decoders will not enter low latency mode if adaptive playback is enabled
- Affected decoders: Intel decoder in Nexus Player
8. Some decoders will not enter low latency mode if the profile isn't baseline in the first SPS.
- Affected decoders: Intel decoder in Nexus Player
+2 -2
View File
@@ -1,6 +1,6 @@
#Wed Apr 10 15:27:10 PDT 2013 #Sun Dec 07 22:52:07 PST 2014
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-2.1-all.zip distributionUrl=https\://services.gradle.org/distributions/gradle-2.2.1-all.zip