Compare commits

..
Author SHA1 Message Date
Cameron Gutman f1d7f556fd Bump to version 3.1.8 2015-07-21 18:03:37 -07:00
Cameron Gutman 1e70e1d329 GFE 2.5.11 update to fix black screen on Fire TV Stick 2015-07-18 17:06:41 -07:00
Cameron Gutman e02a009635 Add support for the Razer Serval controller. The start and select buttons are manually handled for devices without a mapping for them. The back button is ignored so it can be used to exit the stream. 2015-07-18 00:46:25 -07:00
Cameron Gutman bd6ff35603 Update to 3.1.7 2015-06-15 10:37:58 -07:00
Cameron Gutman 1cb7727dc7 Update common 2015-06-15 10:28:31 -07:00
Cameron Gutman 0c73e3d0ae Only propagate a decoder exception if it happens at the beginning of a stream 2015-06-15 10:28:09 -07:00
Cameron Gutman 6371d364e1 Lint warning cleanup 2015-05-29 23:22:40 -05:00
Cameron Gutman ded9c9140d Handle being online but not having a known reachability 2015-05-29 23:20:04 -05:00
Cameron Gutman 7c8a108e28 Use the leanback feature on API 21+ devices 2015-05-29 23:18:56 -05:00
Cameron Gutman 2a18ffcdba Update to Gradle 1.2.3 2015-05-19 10:10:18 -05:00
Cameron Gutman 381d0d5e81 Add support for multi-window functionality on Samsung devices 2015-05-10 00:02:04 -05:00
Cameron Gutman be126acfd1 Update version info to 3.1.6 2015-05-05 20:52:53 -04:00
Cameron Gutman fc2f5cfe4d Manually pass through Samsung capacitive buttons 2015-05-05 20:20:37 -04:00
Cameron Gutman 9878902a89 Use IDs to track controllers instead of descriptors. Fixes #64 2015-05-05 20:08:58 -04:00
Cameron Gutman f1230d46f3 Android Studio 1.2 and Grade 1.2.2 update 2015-05-05 20:02:53 -04:00
Michelle Bergeron d8822392f1 Link to site 2015-05-03 23:36:19 -07:00
Cameron Gutman 1d9cf71517 Total Eclipse of the Lime 2015-04-21 21:50:40 -04:00
Cameron Gutman 2160e87fef Fix division by zero in ARC 2015-03-31 20:29:22 -04:00
Cameron Gutman 88249ba8aa Enable direct submission for ARC 2015-03-31 19:59:16 -04:00
Cameron Gutman 2856617fb3 Only release controller numbers if they were reserved 2015-03-31 19:58:47 -04:00
Cameron Gutman d822980d5a Fix missing close of Closeables caught by StrictMode 2015-03-29 23:25:00 -04:00
Cameron Gutman b5ba59b413 Fix database reference leak 2015-03-29 23:06:32 -04:00
Cameron Gutman 1148e0163c Only assign a controller number when a valid controller input has been received. Fixes misdetection of other input devices as controllers (issue #65). 2015-03-29 22:54:48 -04:00
Cameron Gutman cf36c7adb1 Increment version 2015-03-25 02:33:46 -04:00
Cameron Gutman eac6998e17 Update the latency message strings to be more clear that this isn't end to end latency 2015-03-25 01:20:55 -04:00
Cameron Gutman 17afbffdb5 Include the time it takes to get an input buffer in the frame latency calculation 2015-03-25 01:08:23 -04:00
Cameron Gutman 072a439c2d Update common and decode unit API 2015-03-25 00:32:22 -04:00
Cameron Gutman c533600983 Update for 3.1.3 release 2015-03-23 17:26:37 -04:00
Cameron Gutman 5847fbb6b6 Add TI decoders to the direct submit whitelist 2015-03-23 17:14:02 -04:00
Cameron Gutman 1876b30c1b Forgot this file 2015-03-23 16:51:57 -04:00
Cameron Gutman 5c71f55993 Add another Exynos prefix 2015-03-23 16:51:32 -04:00
Cameron Gutman 9c0960d03d Add options to quit and resume streaming from the PC view 2015-03-23 16:36:43 -04:00
Cameron Gutman 29a395f3f4 Prevent updating the UI while quitting is in progress 2015-03-23 15:57:29 -04:00
Cameron Gutman a676b8d8e6 Restore the legacy path and only use direct submit for certain whitelisted decoders 2015-03-23 15:51:11 -04:00
Cameron Gutman 7ab0be3b62 Optimize app grid performance on lower end devices 2015-03-23 15:12:25 -04:00
Cameron Gutman 115853fed2 Update version to 3.1.3-beta1 2015-03-16 21:29:07 -04:00
Cameron Gutman 60beb81ae4 Target API 22 2015-03-16 21:28:49 -04:00
Cameron Gutman 5310375d42 Target Android 5.1 2015-03-16 21:28:33 -04:00
Cameron Gutman 7ce29e3a09 Add a workaround for the Nexus 9 dropping frames with the new renderer 2015-03-16 21:26:02 -04:00
Cameron Gutman 42c65f4f16 Use smaller deadzones for SHIELD controllers 2015-03-16 19:36:09 -04:00
Cameron Gutman bf2cc2a4d5 Don't assign controller numbers to devices that don't have an analog stick 2015-03-16 19:35:43 -04:00
Cameron Gutman 6d6d7121f6 Remove the Playpad Pro hack that worked around an issue with old firmware and caused the D-pad to be unresponsive on updated firmware. Fixes #41 2015-03-15 14:30:56 -07:00
Cameron Gutman 2ab67380d6 Use direct submit decoding for MediaCodec. Based on my profiling of a few devices, dequeueInputBuffer and queueInputBuffer don't take much time anyway. It allows us to stop our semi-busy looping which saves power. The depacketizer can avoid expensive synchronization and additional context switching which costs time and CPU cycles. 2015-03-09 01:49:52 -05:00
Cameron Gutman 899387caa1 Use a separate executor for network loads to avoid stalling cached loads. Optimize background cache fill loads. 2015-03-02 18:34:21 -05:00
Cameron Gutman 56c8a9e6fe Use the regular serverinfo query to update the running status of apps 2015-03-02 17:05:45 -05:00
Cameron Gutman 896288a40b Use AsyncTasks and attached Drawables to track background image loading 2015-03-02 17:03:08 -05:00
Cameron Gutman fc8ce5e4b9 Quiet down disk cache misses 2015-03-02 16:13:54 -05:00
Cameron Gutman 4affc3c4ce Update to 3.1.2 release 2015-02-27 18:12:49 -05:00
Cameron Gutman 067be54715 Show the discovery in progress view if no computers remain after one is deleted 2015-02-27 18:05:02 -05:00
Cameron Gutman 0dad2dc64b Only close the app list activity if the PC is offline not unknown 2015-02-27 15:15:01 -05:00
Cameron Gutman 867b703644 Evict cached bitmaps when closing the app list 2015-02-27 15:13:43 -05:00
Cameron Gutman 3d398ef6dd Update common 2015-02-27 14:22:35 -05:00
Cameron Gutman 85d95b2d8e Revert "Immediately show the PC as offline if the first poll fails"
This reverts commit 7b12fd1ad2.
2015-02-27 13:52:17 -05:00
Cameron Gutman d091d9db6b Start apps by ID to work correctly with duplicate app names 2015-02-27 13:42:40 -05:00
Cameron Gutman e081ab5239 Code cleanup and Lint suggestions 2015-02-27 01:43:24 -05:00
Cameron Gutman 7b12fd1ad2 Immediately show the PC as offline if the first poll fails 2015-02-27 01:33:33 -05:00
Cameron Gutman 80d8c5953e Rewrite the app art caching and fetching (again!) to finally address OOM problems and speed up art loading 2015-02-27 01:16:06 -05:00
Cameron Gutman 194037ff41 Clear the bitmap cache since it can get pretty large 2015-02-26 22:04:39 -05:00
Cameron Gutman 094d642739 Stop scaling bitmaps down 2015-02-26 22:04:22 -05:00
Cameron Gutman 010e03252e Encapsulate the cache IO streams in buffered streams 2015-02-26 21:39:26 -05:00
Cameron Gutman 98638186b5 Use weak references to allow the image views to be garbage collected while a load is in progress 2015-02-26 21:05:33 -05:00
Cameron Gutman c5293ef21f Reduce the size of the LRU cache by 2 2015-02-26 21:04:40 -05:00
Cameron Gutman 366a1c91b8 Always close the output stream 2015-02-26 21:04:17 -05:00
Cameron Gutman 157450e674 Update common with stricter applist parser 2015-02-26 18:33:18 -05:00
Cameron Gutman 1b8d2bc81c Cancel asset fetching when the app view is paused 2015-02-26 18:30:02 -05:00
Cameron Gutman f1787c43e5 Generalize the polling grace period to all users of CMS 2015-02-26 18:27:50 -05:00
Cameron Gutman 95ea88e932 Only replace the MAC address if the existing one is non-null 2015-02-26 15:11:46 -05:00
Cameron Gutman f2b8461bb9 Increment version to beta 3.1.2 2015-02-25 23:15:10 -05:00
Cameron Gutman 7838a787df Fix a bug in app removal 2015-02-25 22:27:38 -05:00
Cameron Gutman cc3f2ecb07 Always close the cache output stream if an exception occurs 2015-02-25 22:15:41 -05:00
Cameron Gutman 833b7c3916 Fetch app assets in the background while in the app view 2015-02-25 21:57:54 -05:00
Cameron Gutman 90209f2ca2 Update iml files generated by Android Studio 1.1 2015-02-25 21:07:59 -05:00
Cameron Gutman 2681036c32 Update common 2015-02-25 21:07:44 -05:00
Cameron Gutman ee58071ff1 Fix huge performance issues when dealing with large app lists 2015-02-25 21:07:35 -05:00
Cameron Gutman e222f2f6c3 Fix fast polling 2015-02-22 18:34:28 -05:00
Cameron Gutman 0b7becb161 Remove unused function 2015-02-22 18:10:08 -05:00
Cameron Gutman bf795ab7a5 Fix removal of apps in app list updates 2015-02-22 18:04:42 -05:00
Cameron Gutman 59df38ae8a Cancel app icon requests when the view is recycled 2015-02-22 17:49:52 -05:00
Cameron Gutman e04ff048b8 Implement a fast polling method to speed up polling. Save the old MAC address if it's empty. 2015-02-11 17:04:31 -05:00
Cameron Gutman 7d25d07c6d Update version to 3.1.1 2015-02-11 00:55:47 -05:00
Cameron Gutman 7b0ddfae42 Update iml files 2015-02-11 00:34:56 -05:00
Cameron Gutman 43fa1a7245 Update common to fix null app name issue 2015-02-09 00:12:29 -05:00
Cameron Gutman 057530eed0 Correctly identify computers that are the same 2015-02-08 23:46:03 -05:00
Cameron Gutman aee34f6365 Remove redundant null checks 2015-02-08 23:44:33 -05:00
Cameron Gutman 5519d92243 Disable the start key shortcut to start the keyboard because the keyboard can't receive input after it's started 2015-02-07 13:58:53 -05:00
Cameron Gutman 3d95ac1f93 Fix keyboard dismissal on Fire TV devices 2015-02-07 13:42:49 -05:00
Cameron Gutman 5c938535be Fix app list focus issues with remotes/gamepads 2015-02-07 13:20:01 -05:00
Cameron Gutman 2fdecc551a Tabs -> Spaces 2015-02-07 11:54:46 -05:00
Cameron Gutman 10204afdb4 Only add PCs to the computer list when they have been polled once to get a UUID for equality comparison. Fix equality comparison in PcView to avoid duplicate PCs enumerated over mDNS. 2015-02-07 11:44:56 -05:00
Cameron Gutman 55c800c2a5 Fade in box art when scrolling 2015-02-07 06:52:28 -05:00
Cameron Gutman 265b3f9963 Use image alpha to make images transparent while loading 2015-02-07 06:23:35 -05:00
Cameron Gutman a8bf2cd1cf Fix UI dropped frames when loading images 2015-02-07 06:08:00 -05:00
Cameron Gutman 4fcd8b3dfe Replace unpair option with delete PC 2015-02-07 05:57:30 -05:00
Cameron Gutman e1a1a6344d Fill the whole height with the list view 2015-02-06 13:38:32 -05:00
Cameron Gutman a095c10a25 Increment version to 3.1 and update build files 2015-02-05 16:15:16 -05:00
Cameron Gutman b1ea487e22 Use the mode (power) button on the Asus Nexus Player Gamepad as a select button 2015-02-05 16:06:55 -05:00
Cameron Gutman 47265d0d10 Add another SELinux policy change needed on Nexus 9 2015-02-05 16:06:22 -05:00
Cameron Gutman 6a41b41a38 Merge pull request #55 from Ansa89/italian-translation
Italian translation: update
2015-02-05 13:34:03 -05:00
Cameron Gutman 2247e43a48 Remove unused imports 2015-02-05 13:23:01 -05:00
Cameron Gutman d3986080a3 Tighten up a bunch of declarations to make Lint happier 2015-02-05 13:21:04 -05:00
Cameron Gutman 07277e1a5b Fix a few Lint warnings 2015-02-05 13:01:35 -05:00
Ansa89 39d7fc748f Italian translation: update 2015-02-03 01:06:36 +01:00
Cameron Gutman 4d3a69cf6a Fix GFE 2.1.x controller regression 2015-02-02 18:10:18 -05:00
Cameron Gutman b806522751 Unassign the controller number when a device is removed 2015-02-02 02:13:27 -05:00
Cameron Gutman 256fa897a7 Fix build issues 2015-02-01 18:31:34 -05:00
Cameron Gutman 5c812eed6c Beta 2 version update 2015-02-01 18:20:55 -05:00
Cameron Gutman f0b22f9119 Don't use small mode on TVs 2015-02-01 18:20:39 -05:00
Cameron Gutman 7e1884acb5 Trap Shield's back button as controller 0 2015-02-01 18:07:03 -05:00
Cameron Gutman 9512521783 Update common 2015-02-01 15:06:27 -05:00
Cameron Gutman da7904a767 Add multiple controller support 2015-02-01 15:06:18 -05:00
63 changed files with 1845 additions and 1167 deletions

No files matched your search

+8 -10
View File
@@ -1,14 +1,14 @@
#Limelight
#Moonlight
Limelight is an open source implementation of NVIDIA's GameStream, as used by the NVIDIA Shield.
[Moonlight](http://moonlight-stream.com) is an open source implementation of NVIDIA's GameStream, as used by the NVIDIA Shield.
We reverse engineered the Shield streaming software and created a version that can be run on any Android device.
Limelight will allow you to stream your full collection of games from your Windows PC to your Android device,
Moonlight will allow you to stream your full collection of games from your Windows PC to your Android device,
whether in your own home or over the internet.
[Limelight-pc](https://github.com/limelight-stream/limelight-pc) is also currently in development for Windows, OS X and Linux. Versions for [iOS](https://github.com/limelight-stream/limelight-ios) and [Windows and Windows Phone](https://github.com/limelight-stream/limelight-windows) are also in development.
[Moonlight-pc](https://github.com/moonlight-stream/moonlight-pc) is also currently in development for Windows, OS X and Linux. Versions for [iOS](https://github.com/moonlight-stream/moonlight-ios) and [Windows and Windows Phone](https://github.com/moonlight-stream/moonlight-windows) are also in development.
Check our [wiki](https://github.com/limelight-stream/limelight-android/wiki) for more detailed information or a troubleshooting guide.
Check our [wiki](https://github.com/moonlight-stream/moonlight-android/wiki) for more detailed information or a troubleshooting guide.
##Features
@@ -18,7 +18,7 @@ Check our [wiki](https://github.com/limelight-stream/limelight-android/wiki) for
##Installation
* Download and install Limelight for Android from
* Download and install Moonlight for Android from
[Google Play](https://play.google.com/store/apps/details?id=com.limelight)
* Download [GeForce Experience](http://www.geforce.com/geforce-experience) and install on your Windows PC
@@ -33,7 +33,7 @@ Check our [wiki](https://github.com/limelight-stream/limelight-android/wiki) for
* Turn on GameStream in the GFE settings
* If you are connecting from outside the same network, turn on internet
streaming
* When on the same network as your PC, open Limelight and tap on your PC in the list
* When on the same network as your PC, open Moonlight and tap on your PC in the list
* Accept the pairing confirmation on your PC
* Tap your PC again to view the list of apps to stream
* Play games!
@@ -46,8 +46,6 @@ This project is being actively developed at [XDA Developers](http://forum.xda-de
2. Write code
3. Send Pull Requests
Check out our [website](http://limelight-stream.com) for project links and information.
##Authors
* [Cameron Gutman](https://github.com/cgutman)
@@ -55,5 +53,5 @@ Check out our [website](http://limelight-stream.com) for project links and infor
* [Aaron Neyer](https://github.com/Aaronneyer)
* [Andrew Hennessy](https://github.com/yetanothername)
Limelight is the work of students at [Case Western](http://case.edu) and was
Moonlight is the work of students at [Case Western](http://case.edu) and was
started as a project at [MHacks](http://mhacks.org).
+13 -15
View File
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<module external.linked.project.path="$MODULE_DIR$" external.root.project.path="$MODULE_DIR$/.." external.system.id="GRADLE" external.system.module.group="limelight-android" external.system.module.version="unspecified" type="JAVA_MODULE" version="4">
<module external.linked.project.id=":app" external.linked.project.path="$MODULE_DIR$" external.root.project.path="$MODULE_DIR$/.." external.system.id="GRADLE" external.system.module.group="moonlight-android" external.system.module.version="unspecified" type="JAVA_MODULE" version="4">
<component name="FacetManager">
<facet type="android-gradle" name="Android-Gradle">
<configuration>
@@ -9,11 +9,13 @@
<facet type="android" name="Android">
<configuration>
<option name="SELECTED_BUILD_VARIANT" value="nonRootDebug" />
<option name="SELECTED_TEST_ARTIFACT" value="_android_test_" />
<option name="ASSEMBLE_TASK_NAME" value="assembleNonRootDebug" />
<option name="COMPILE_JAVA_TASK_NAME" value="compileNonRootDebugSources" />
<option name="ASSEMBLE_TEST_TASK_NAME" value="assembleNonRootDebugTest" />
<option name="SOURCE_GEN_TASK_NAME" value="generateNonRootDebugSources" />
<option name="TEST_SOURCE_GEN_TASK_NAME" value="generateNonRootDebugTestSources" />
<option name="ASSEMBLE_TEST_TASK_NAME" value="assembleNonRootDebugAndroidTest" />
<option name="COMPILE_JAVA_TEST_TASK_NAME" value="compileNonRootDebugAndroidTestSources" />
<option name="TEST_SOURCE_GEN_TASK_NAME" value="generateNonRootDebugAndroidTestSources" />
<option name="ALLOW_USER_CONFIGURATION" value="false" />
<option name="MANIFEST_FILE_RELATIVE_PATH" value="/src/main/AndroidManifest.xml" />
<option name="RES_FOLDER_RELATIVE_PATH" value="/src/main/res" />
@@ -24,6 +26,7 @@
</component>
<component name="NewModuleRootManager" inherit-compiler-output="false">
<output url="file://$MODULE_DIR$/build/intermediates/classes/nonRoot/debug" />
<output-test url="file://$MODULE_DIR$/build/intermediates/classes/androidTest/nonRoot/debug" />
<exclude-output />
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/r/nonRoot/debug" isTestSource="false" generated="true" />
@@ -39,12 +42,12 @@
<sourceFolder url="file://$MODULE_DIR$/src/nonRootDebug/java" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/src/nonRootDebug/jni" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/src/nonRootDebug/rs" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/r/test/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/buildConfig/test/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/res/rs/test/nonRoot/debug" type="java-test-resource" />
<sourceFolder url="file://$MODULE_DIR$/build/generated/res/generated/test/nonRoot/debug" type="java-test-resource" />
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/r/androidTest/nonRoot/debug" isTestSource="true" generated="true" />
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/aidl/androidTest/nonRoot/debug" isTestSource="true" generated="true" />
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/buildConfig/androidTest/nonRoot/debug" isTestSource="true" generated="true" />
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/rs/androidTest/nonRoot/debug" isTestSource="true" generated="true" />
<sourceFolder url="file://$MODULE_DIR$/build/generated/res/rs/androidTest/nonRoot/debug" type="java-test-resource" />
<sourceFolder url="file://$MODULE_DIR$/build/generated/res/generated/androidTest/nonRoot/debug" type="java-test-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/nonRoot/res" type="java-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/nonRoot/resources" type="java-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/nonRoot/assets" type="java-resource" />
@@ -101,20 +104,15 @@
<excludeFolder url="file://$MODULE_DIR$/build/outputs" />
<excludeFolder url="file://$MODULE_DIR$/build/tmp" />
</content>
<orderEntry type="jdk" jdkName="Android API 21 Platform" jdkType="Android SDK" />
<orderEntry type="jdk" jdkName="Android API 22 Platform" jdkType="Android SDK" />
<orderEntry type="sourceFolder" forTests="false" />
<orderEntry type="library" exported="" name="bcprov-jdk15on-1.51" level="project" />
<orderEntry type="library" exported="" name="androidasync-2.0.5" level="project" />
<orderEntry type="library" exported="" name="gson-2.3.1" level="project" />
<orderEntry type="library" exported="" name="support-v4-r7" level="project" />
<orderEntry type="library" exported="" name="jmdns-fixed" level="project" />
<orderEntry type="library" exported="" name="bcpkix-jdk15on-1.51" level="project" />
<orderEntry type="library" exported="" name="tinyrtsp" level="project" />
<orderEntry type="library" exported="" name="limelight-common" level="project" />
<orderEntry type="library" exported="" name="ion-2.0.5" level="project" />
<orderEntry type="library" exported="" name="okhttp-2.2.0" level="project" />
<orderEntry type="library" exported="" name="jcodec-0.1.9" level="project" />
<orderEntry type="library" exported="" name="okio-1.2.0" level="project" />
</component>
</module>
+9 -13
View File
@@ -4,15 +4,15 @@ import org.apache.tools.ant.taskdefs.condition.Os
apply plugin: 'com.android.application'
android {
compileSdkVersion 21
compileSdkVersion 22
buildToolsVersion "21.1.2"
defaultConfig {
minSdkVersion 16
targetSdkVersion 21
targetSdkVersion 22
versionName "3.1-beta1"
versionCode = 50
versionName "3.1.8"
versionCode = 63
}
productFlavors {
@@ -62,17 +62,13 @@ android {
}
dependencies {
compile group: 'org.jcodec', name: 'jcodec', version: '+'
compile group: 'org.jcodec', name: 'jcodec', version: '0.1.9'
compile group: 'org.bouncycastle', name: 'bcprov-jdk15on', version: '+'
compile group: 'org.bouncycastle', name: 'bcpkix-jdk15on', version: '+'
compile group: 'org.bouncycastle', name: 'bcprov-jdk15on', version: '1.51'
compile group: 'org.bouncycastle', name: 'bcpkix-jdk15on', version: '1.51'
compile group: 'com.google.android', name: 'support-v4', version:'+'
compile group: 'com.koushikdutta.ion', name: 'ion', version:'+'
compile group: 'com.google.code.gson', name: 'gson', version:'+'
compile group: 'com.squareup.okhttp', name: 'okhttp', version:'+'
compile group: 'com.squareup.okio', name:'okio', version:'+'
compile group: 'com.squareup.okhttp', name: 'okhttp', version:'2.2.0'
compile group: 'com.squareup.okio', name:'okio', version:'1.2.0'
compile files('libs/jmdns-fixed.jar')
compile files('libs/limelight-common.jar')
Binary file not shown.
+5
View File
@@ -17,6 +17,10 @@
android:icon="@drawable/ic_launcher"
android:theme="@style/AppTheme" >
<!-- Samsung multi-window support -->
<uses-library android:name="com.sec.android.app.multiwindow" android:required="false" />
<meta-data android:name="com.sec.android.support.multiwindow" android:value="true" />
<!-- Launcher for traditional devices -->
<activity
android:name=".PcView"
@@ -24,6 +28,7 @@
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
<category android:name="android.intent.category.MULTIWINDOW_LAUNCHER" />
<category android:name="tv.ouya.intent.category.APP" />
</intent-filter>
</activity>
+119 -136
View File
@@ -1,25 +1,14 @@
package com.limelight;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.StringReader;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.List;
import java.util.Locale;
import java.util.UUID;
import org.xmlpull.v1.XmlPullParserException;
import com.limelight.binding.PlatformBinding;
import com.limelight.binding.crypto.AndroidCryptoProvider;
import com.limelight.computers.ComputerManagerListener;
import com.limelight.computers.ComputerManagerService;
import com.limelight.grid.AppGridAdapter;
import com.limelight.nvstream.http.ComputerDetails;
import com.limelight.nvstream.http.GfeHttpResponseException;
import com.limelight.nvstream.http.NvApp;
import com.limelight.nvstream.http.NvHTTP;
import com.limelight.preferences.PreferenceConfiguration;
@@ -27,14 +16,13 @@ import com.limelight.ui.AdapterFragment;
import com.limelight.ui.AdapterFragmentCallbacks;
import com.limelight.utils.CacheHelper;
import com.limelight.utils.Dialog;
import com.limelight.utils.ServerHelper;
import com.limelight.utils.SpinnerDialog;
import com.limelight.utils.UiHelper;
import android.app.Activity;
import android.app.AlertDialog;
import android.app.Service;
import android.content.ComponentName;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.ServiceConnection;
import android.content.res.Configuration;
@@ -48,7 +36,6 @@ import android.view.ContextMenu.ContextMenuInfo;
import android.widget.AbsListView;
import android.widget.AdapterView;
import android.widget.AdapterView.OnItemClickListener;
import android.widget.Spinner;
import android.widget.TextView;
import android.widget.Toast;
import android.widget.AdapterView.AdapterContextMenuInfo;
@@ -61,9 +48,8 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
private ComputerManagerService.ApplistPoller poller;
private SpinnerDialog blockingLoadSpinner;
private String lastRawApplist;
private int consecutiveAppListFailures = 0;
private final static int CONSECUTIVE_FAILURE_LIMIT = 3;
private int lastRunningAppId;
private boolean suspendGridUpdates;
private final static int START_OR_RESUME_ID = 1;
private final static int QUIT_ID = 2;
@@ -74,7 +60,7 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
public final static String UUID_EXTRA = "UUID";
private ComputerManagerService.ComputerManagerBinder managerBinder;
private ServiceConnection serviceConnection = new ServiceConnection() {
private final ServiceConnection serviceConnection = new ServiceConnection() {
public void onServiceConnected(ComponentName className, IBinder binder) {
final ComputerManagerService.ComputerManagerBinder localBinder =
((ComputerManagerService.ComputerManagerBinder)binder);
@@ -121,11 +107,6 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
}
};
private InetAddress getAddress() {
return computer.reachability == ComputerDetails.Reachability.LOCAL ?
computer.localIp : computer.remoteIp;
}
private void startComputerUpdates() {
if (managerBinder == null) {
return;
@@ -134,15 +115,17 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
managerBinder.startPolling(new ComputerManagerListener() {
@Override
public void notifyComputerUpdated(ComputerDetails details) {
// Don't care about other computers
if (details != computer) {
// Do nothing if updates are suspended
if (suspendGridUpdates) {
return;
}
if (details.state != ComputerDetails.State.ONLINE) {
consecutiveAppListFailures++;
// Don't care about other computers
if (!details.uuid.toString().equalsIgnoreCase(uuidString)) {
return;
}
if (consecutiveAppListFailures >= CONSECUTIVE_FAILURE_LIMIT) {
if (details.state == ComputerDetails.State.OFFLINE) {
// The PC is unreachable now
AppView.this.runOnUiThread(new Runnable() {
@Override
@@ -152,27 +135,34 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
finish();
}
});
}
return;
}
consecutiveAppListFailures = 0;
// App list is the same or empty; nothing to do
// App list is the same or empty
if (details.rawAppList == null || details.rawAppList.equals(lastRawApplist)) {
// Let's check if the running app ID changed
if (details.runningGameId != lastRunningAppId) {
// Update the currently running game using the app ID
lastRunningAppId = details.runningGameId;
updateUiWithServerinfo(details);
}
return;
}
lastRunningAppId = details.runningGameId;
lastRawApplist = details.rawAppList;
try {
lastRawApplist = details.rawAppList;
updateUiWithAppList(NvHTTP.getAppListByReader(new StringReader(details.rawAppList)));
if (blockingLoadSpinner != null) {
blockingLoadSpinner.dismiss();
blockingLoadSpinner = null;
}
} catch (Exception e) {}
} catch (Exception ignored) {}
}
});
@@ -190,6 +180,10 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
if (managerBinder != null) {
managerBinder.stopPolling();
}
if (appGridAdapter != null) {
appGridAdapter.cancelQueuedOperations();
}
}
@Override
@@ -272,10 +266,6 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
int runningAppId = -1;
for (int i = 0; i < appGridAdapter.getCount(); i++) {
AppObject app = (AppObject) appGridAdapter.getItem(i);
if (app.app == null) {
continue;
}
if (app.app.getIsRunning()) {
runningAppId = app.app.getAppId();
break;
@@ -290,10 +280,6 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
AdapterContextMenuInfo info = (AdapterContextMenuInfo) menuInfo;
AppObject selectedApp = (AppObject) appGridAdapter.getItem(info.position);
if (selectedApp == null || selectedApp.app == null) {
return;
}
int runningAppId = getRunningAppId();
if (runningAppId != -1) {
if (runningAppId == selectedApp.app.getAppId()) {
@@ -311,33 +297,6 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
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
public boolean onContextItemSelected(MenuItem item) {
AdapterContextMenuInfo info = (AdapterContextMenuInfo) item.getMenuInfo();
@@ -345,25 +304,37 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
switch (item.getItemId()) {
case START_WTIH_QUIT:
// Display a confirmation dialog first
displayQuitConfirmationDialog(new Runnable() {
UiHelper.displayQuitConfirmationDialog(this, new Runnable() {
@Override
public void run() {
doStart(app.app);
ServerHelper.doStart(AppView.this, app.app, computer, managerBinder);
}
}, null);
return true;
case START_OR_RESUME_ID:
// Resume is the same as start for us
doStart(app.app);
ServerHelper.doStart(AppView.this, app.app, computer, managerBinder);
return true;
case QUIT_ID:
// Display a confirmation dialog first
displayQuitConfirmationDialog(new Runnable() {
UiHelper.displayQuitConfirmationDialog(this, new Runnable() {
@Override
public void run() {
doQuit(app.app);
suspendGridUpdates = true;
ServerHelper.doQuit(AppView.this,
ServerHelper.getCurrentAddressFromComputer(computer),
app.app, managerBinder, new Runnable() {
@Override
public void run() {
// Trigger a poll immediately
suspendGridUpdates = false;
if (poller != null) {
poller.pollNow();
}
}
});
}
}, null);
return true;
@@ -376,26 +347,61 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
}
}
private void updateUiWithServerinfo(final ComputerDetails details) {
AppView.this.runOnUiThread(new Runnable() {
@Override
public void run() {
boolean updated = false;
// Look through our current app list to tag the running app
for (int i = 0; i < appGridAdapter.getCount(); i++) {
AppObject existingApp = (AppObject) appGridAdapter.getItem(i);
// There can only be one or zero apps running.
if (existingApp.app.getIsRunning() &&
existingApp.app.getAppId() == details.runningGameId) {
// This app was running and still is, so we're done now
return;
}
else if (existingApp.app.getAppId() == details.runningGameId) {
// This app wasn't running but now is
existingApp.app.setIsRunning(true);
updated = true;
}
else if (existingApp.app.getIsRunning()) {
// This app was running but now isn't
existingApp.app.setIsRunning(false);
updated = true;
}
else {
// This app wasn't running and still isn't
}
}
if (updated) {
appGridAdapter.notifyDataSetChanged();
}
}
});
}
private void updateUiWithAppList(final List<NvApp> appList) {
AppView.this.runOnUiThread(new Runnable() {
@Override
public void run() {
boolean updated = false;
// First handle app updates and additions
for (NvApp app : appList) {
boolean foundExistingApp = false;
// Try to update an existing app in the list first
for (int i = 0; i < appGridAdapter.getCount(); i++) {
AppObject existingApp = (AppObject) appGridAdapter.getItem(i);
if (existingApp.app == null) {
continue;
}
if (existingApp.app.getAppId() == app.getAppId()) {
// Found the app; update its properties
if (existingApp.app.getIsRunning() != app.getIsRunning()) {
existingApp.app.setIsRunningBoolean(app.getIsRunning());
existingApp.app.setIsRunning(app.getIsRunning());
updated = true;
}
if (!existingApp.app.getAppName().equals(app.getAppName())) {
@@ -415,6 +421,34 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
}
}
// 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();
}
@@ -422,58 +456,6 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
});
}
private void doStart(NvApp app) {
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, app.getAppName());
intent.putExtra(Game.EXTRA_UNIQUEID, managerBinder.getUniqueId());
intent.putExtra(Game.EXTRA_STREAMING_REMOTE,
computer.reachability != ComputerDetails.Reachability.LOCAL);
startActivity(intent);
}
private void doQuit(final NvApp app) {
Toast.makeText(AppView.this, getResources().getString(R.string.applist_quit_app)+" "+app.getAppName()+"...", Toast.LENGTH_SHORT).show();
new Thread(new Runnable() {
@Override
public void run() {
NvHTTP httpConn;
String message;
try {
httpConn = new NvHTTP(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 (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();
}
}
final String toastMessage = message;
runOnUiThread(new Runnable() {
@Override
public void run() {
Toast.makeText(AppView.this, toastMessage, Toast.LENGTH_LONG).show();
}
});
}
}).start();
}
@Override
public int getAdapterFragmentLayoutId() {
return PreferenceConfiguration.readPreferences(this).listMode ?
@@ -489,25 +471,26 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
public void onItemClick(AdapterView<?> arg0, View arg1, int pos,
long id) {
AppObject app = (AppObject) appGridAdapter.getItem(pos);
if (app == null || app.app == null) {
return;
}
// Only open the context menu if something is running, otherwise start it
if (getRunningAppId() != -1) {
openContextMenu(arg1);
} else {
doStart(app.app);
ServerHelper.doStart(AppView.this, app.app, computer, managerBinder);
}
}
});
registerForContextMenu(listView);
listView.requestFocus();
}
public class AppObject {
public NvApp app;
public final NvApp app;
public AppObject(NvApp app) {
if (app == null) {
throw new IllegalArgumentException("app must not be null");
}
this.app = app;
}
+23 -18
View File
@@ -13,6 +13,7 @@ import com.limelight.nvstream.NvConnection;
import com.limelight.nvstream.NvConnectionListener;
import com.limelight.nvstream.StreamConfiguration;
import com.limelight.nvstream.av.video.VideoDecoderRenderer;
import com.limelight.nvstream.http.NvApp;
import com.limelight.nvstream.input.KeyboardPacket;
import com.limelight.nvstream.input.MouseButtonPacket;
import com.limelight.preferences.PreferenceConfiguration;
@@ -25,6 +26,7 @@ import android.app.Activity;
import android.content.Context;
import android.content.res.Configuration;
import android.graphics.Point;
import android.hardware.input.InputManager;
import android.media.AudioManager;
import android.net.ConnectivityManager;
import android.net.wifi.WifiManager;
@@ -59,7 +61,7 @@ public class Game extends Activity implements SurfaceHolder.Callback,
private int lastButtonState = 0;
// Only 2 touches are supported
private TouchContext[] touchContextMap = new TouchContext[2];
private final TouchContext[] touchContextMap = new TouchContext[2];
private long threeFingerDownTime = 0;
private static final int THREE_FINGER_TAP_THRESHOLD = 300;
@@ -68,7 +70,7 @@ public class Game extends Activity implements SurfaceHolder.Callback,
private KeyboardTranslator keybTranslator;
private PreferenceConfiguration prefConfig;
private Point screenSize = new Point(0, 0);
private final Point screenSize = new Point(0, 0);
private NvConnection conn;
private SpinnerDialog spinner;
@@ -88,7 +90,8 @@ public class Game extends Activity implements SurfaceHolder.Callback,
private int drFlags = 0;
public static final String EXTRA_HOST = "Host";
public static final String EXTRA_APP = "App";
public static final String EXTRA_APP_NAME = "AppName";
public static final String EXTRA_APP_ID = "AppId";
public static final String EXTRA_UNIQUEID = "UniqueId";
public static final String EXTRA_STREAMING_REMOTE = "Remote";
@@ -170,17 +173,23 @@ public class Game extends Activity implements SurfaceHolder.Callback,
wifiLock.acquire();
String host = Game.this.getIntent().getStringExtra(EXTRA_HOST);
String app = Game.this.getIntent().getStringExtra(EXTRA_APP);
String appName = Game.this.getIntent().getStringExtra(EXTRA_APP_NAME);
int appId = Game.this.getIntent().getIntExtra(EXTRA_APP_ID, StreamConfiguration.INVALID_APP_ID);
String uniqueId = Game.this.getIntent().getStringExtra(EXTRA_UNIQUEID);
boolean remote = Game.this.getIntent().getBooleanExtra(EXTRA_STREAMING_REMOTE, false);
if (appId == StreamConfiguration.INVALID_APP_ID) {
finish();
return;
}
decoderRenderer = new ConfigurableDecoderRenderer();
decoderRenderer.initializeWithFlags(drFlags);
StreamConfiguration config = new StreamConfiguration.Builder()
.setResolution(prefConfig.width, prefConfig.height)
.setRefreshRate(prefConfig.fps)
.setApp(app)
.setApp(new NvApp(appName, appId))
.setBitrate(prefConfig.bitrate * 1000)
.setEnableSops(prefConfig.enableSops)
.enableAdaptiveResolution((decoderRenderer.getCapabilities() &
@@ -193,7 +202,10 @@ public class Game extends Activity implements SurfaceHolder.Callback,
// Initialize the connection
conn = new NvConnection(host, uniqueId, Game.this, config, PlatformBinding.getCryptoProvider(this));
keybTranslator = new KeyboardTranslator(conn);
controllerHandler = new ControllerHandler(conn, this, prefConfig.deadzonePercentage);
controllerHandler = new ControllerHandler(conn, this, prefConfig.multiController, prefConfig.deadzonePercentage);
InputManager inputManager = (InputManager) getSystemService(Context.INPUT_SERVICE);
inputManager.registerInputDeviceListener(controllerHandler, null);
SurfaceHolder sh = sv.getHolder();
if (prefConfig.stretchVideo || !decoderRenderer.isHardwareAccelerated()) {
@@ -222,22 +234,12 @@ public class Game extends Activity implements SurfaceHolder.Callback,
{
// Get the visible width of the activity
double visibleWidth = getWindow().getDecorView().getWidth();
double visibleHeight = getWindow().getDecorView().getHeight();
ViewGroup.LayoutParams lp = sv.getLayoutParams();
double vidRatio = vidHeight / vidWidth;
double visibleRatio = visibleHeight / visibleWidth;
// Calculate the new size of the SurfaceView
if (vidRatio < visibleRatio) {
lp.width = (int) visibleWidth;
lp.height = (int) ((vidHeight / vidWidth) * visibleWidth);
}
else {
lp.width = (int) ((vidWidth / vidHeight) * visibleHeight);
lp.height = (int) visibleHeight;
}
// Apply the size change
sv.setLayoutParams(lp);
@@ -252,7 +254,7 @@ public class Game extends Activity implements SurfaceHolder.Callback,
}
@SuppressLint("InlinedApi")
private Runnable hideSystemUi = new Runnable() {
private final Runnable hideSystemUi = new Runnable() {
@Override
public void run() {
// Use immersive mode on 4.4+ or standard low profile on previous builds
@@ -288,6 +290,9 @@ public class Game extends Activity implements SurfaceHolder.Callback,
SpinnerDialog.closeDialogs(this);
Dialog.closeDialogs();
InputManager inputManager = (InputManager) getSystemService(Context.INPUT_SERVICE);
inputManager.unregisterInputDeviceListener(controllerHandler);
displayedFailureDialog = true;
stopConnection();
@@ -318,7 +323,7 @@ public class Game extends Activity implements SurfaceHolder.Callback,
wifiLock.release();
}
private Runnable toggleGrab = new Runnable() {
private final Runnable toggleGrab = new Runnable() {
@Override
public void run() {
+62 -10
View File
@@ -12,6 +12,7 @@ import com.limelight.computers.ComputerManagerListener;
import com.limelight.computers.ComputerManagerService;
import com.limelight.grid.PcGridAdapter;
import com.limelight.nvstream.http.ComputerDetails;
import com.limelight.nvstream.http.NvApp;
import com.limelight.nvstream.http.NvHTTP;
import com.limelight.nvstream.http.PairingManager;
import com.limelight.nvstream.http.PairingManager.PairState;
@@ -22,10 +23,10 @@ import com.limelight.preferences.StreamSettings;
import com.limelight.ui.AdapterFragment;
import com.limelight.ui.AdapterFragmentCallbacks;
import com.limelight.utils.Dialog;
import com.limelight.utils.ServerHelper;
import com.limelight.utils.UiHelper;
import android.app.Activity;
import android.app.FragmentTransaction;
import android.app.Service;
import android.content.ComponentName;
import android.content.Intent;
@@ -53,7 +54,7 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
private PcGridAdapter pcGridAdapter;
private ComputerManagerService.ComputerManagerBinder managerBinder;
private boolean freezeUpdates, runningPolling;
private ServiceConnection serviceConnection = new ServiceConnection() {
private final ServiceConnection serviceConnection = new ServiceConnection() {
public void onServiceConnected(ComponentName className, IBinder binder) {
final ComputerManagerService.ComputerManagerBinder localBinder =
((ComputerManagerService.ComputerManagerBinder)binder);
@@ -94,6 +95,8 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
private final static int UNPAIR_ID = 3;
private final static int WOL_ID = 4;
private final static int DELETE_ID = 5;
private final static int RESUME_ID = 6;
private final static int QUIT_ID = 7;
private void initializeViews() {
setContentView(R.layout.activity_pc_view);
@@ -238,8 +241,7 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
AdapterContextMenuInfo info = (AdapterContextMenuInfo) menuInfo;
ComputerObject computer = (ComputerObject) pcGridAdapter.getItem(info.position);
if (computer == null || computer.details == null ||
computer.details.reachability == ComputerDetails.Reachability.UNKNOWN) {
if (computer.details.reachability == ComputerDetails.Reachability.UNKNOWN) {
startComputerUpdates();
return;
}
@@ -254,8 +256,16 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
menu.add(Menu.NONE, DELETE_ID, 2, getResources().getString(R.string.pcview_menu_delete_pc));
}
else {
menu.add(Menu.NONE, APP_LIST_ID, 1, getResources().getString(R.string.pcview_menu_app_list));
menu.add(Menu.NONE, UNPAIR_ID, 2, getResources().getString(R.string.pcview_menu_unpair_pc));
if (computer.details.runningGameId != 0) {
menu.add(Menu.NONE, RESUME_ID, 1, getResources().getString(R.string.applist_menu_resume));
menu.add(Menu.NONE, QUIT_ID, 2, getResources().getString(R.string.applist_menu_quit));
}
menu.add(Menu.NONE, APP_LIST_ID, 3, getResources().getString(R.string.pcview_menu_app_list));
// FIXME: We used to be able to unpair here but it's been broken since GFE 2.1.x, so I've replaced
// it with delete which actually work
menu.add(Menu.NONE, DELETE_ID, 4, getResources().getString(R.string.pcview_menu_delete_pc));
}
}
@@ -296,6 +306,10 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
else if (computer.reachability == ComputerDetails.Reachability.REMOTE) {
addr = computer.remoteIp;
}
else {
LimeLog.warning("Unknown reachability - using local IP");
addr = computer.localIp;
}
httpConn = new NvHTTP(addr,
managerBinder.getUniqueId(),
@@ -421,6 +435,10 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
else if (computer.reachability == ComputerDetails.Reachability.REMOTE) {
addr = computer.remoteIp;
}
else {
LimeLog.warning("Unknown reachability - using local IP");
addr = computer.localIp;
}
httpConn = new NvHTTP(addr,
managerBinder.getUniqueId(),
@@ -476,9 +494,8 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
@Override
public boolean onContextItemSelected(MenuItem item) {
AdapterContextMenuInfo info = (AdapterContextMenuInfo) item.getMenuInfo();
ComputerObject computer = (ComputerObject) pcGridAdapter.getItem(info.position);
switch (item.getItemId())
{
final ComputerObject computer = (ComputerObject) pcGridAdapter.getItem(info.position);
switch (item.getItemId()) {
case PAIR_ID:
doPair(computer.details);
return true;
@@ -504,6 +521,32 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
doAppList(computer.details);
return true;
case RESUME_ID:
if (managerBinder == null) {
Toast.makeText(PcView.this, getResources().getString(R.string.error_manager_not_running), Toast.LENGTH_LONG).show();
return true;
}
ServerHelper.doStart(this, new NvApp("app", computer.details.runningGameId), computer.details, managerBinder);
return true;
case QUIT_ID:
if (managerBinder == null) {
Toast.makeText(PcView.this, getResources().getString(R.string.error_manager_not_running), Toast.LENGTH_LONG).show();
return true;
}
// Display a confirmation dialog first
UiHelper.displayQuitConfirmationDialog(this, new Runnable() {
@Override
public void run() {
ServerHelper.doQuit(PcView.this,
ServerHelper.getCurrentAddressFromComputer(computer.details),
new NvApp("app", 0), managerBinder, null);
}
}, null);
return true;
default:
return super.onContextItemSelected(item);
}
@@ -516,6 +559,12 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
if (details.equals(computer.details)) {
pcGridAdapter.removeComputer(computer);
pcGridAdapter.notifyDataSetChanged();
if (pcGridAdapter.getCount() == 0) {
// Show the "Discovery in progress" view
noPcFoundLayout.setVisibility(View.VISIBLE);
}
break;
}
}
@@ -528,7 +577,7 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
ComputerObject computer = (ComputerObject) pcGridAdapter.getItem(i);
// Check if this is the same computer
if (details.equals(computer.details)) {
if (details.uuid.equals(computer.details.uuid)) {
existingEntry = computer;
break;
}
@@ -585,6 +634,9 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
public ComputerDetails details;
public ComputerObject(ComputerDetails details) {
if (details == null) {
throw new IllegalArgumentException("details must not be null");
}
this.details = details;
}
@@ -9,7 +9,7 @@ import com.limelight.nvstream.av.audio.AudioRenderer;
public class AndroidAudioRenderer implements AudioRenderer {
public static final int FRAME_SIZE = 960;
private static final int FRAME_SIZE = 960;
private AudioTrack track;
@@ -45,8 +45,8 @@ import com.limelight.nvstream.http.LimelightCryptoProvider;
public class AndroidCryptoProvider implements LimelightCryptoProvider {
private File certFile;
private File keyFile;
private final File certFile;
private final File keyFile;
private X509Certificate cert;
private RSAPrivateKey key;
@@ -1,8 +1,8 @@
package com.limelight.binding.input;
import java.util.HashMap;
import android.hardware.input.InputManager;
import android.os.SystemClock;
import android.util.SparseArray;
import android.view.InputDevice;
import android.view.KeyEvent;
import android.view.MotionEvent;
@@ -13,26 +13,10 @@ import com.limelight.nvstream.input.ControllerPacket;
import com.limelight.ui.GameGestures;
import com.limelight.utils.Vector2d;
public class ControllerHandler {
private short inputMap = 0x0000;
private byte leftTrigger = 0x00;
private byte rightTrigger = 0x00;
private short rightStickX = 0x0000;
private short rightStickY = 0x0000;
private short leftStickX = 0x0000;
private short leftStickY = 0x0000;
private int emulatingButtonFlags = 0;
public class ControllerHandler implements InputManager.InputDeviceListener {
// Used for OUYA bumper state tracking since they force all buttons
// up when the OUYA button goes down. We watch the last time we get
// a bumper up and compare that to our maximum delay when we receive
// a Start button press to see if we should activate one of our
// emulated button combos.
private long lastLbUpTime = 0;
private long lastRbUpTime = 0;
private static final int MAXIMUM_BUMPER_UP_DELAY_MS = 100;
private long startDownTime = 0;
private static final int START_DOWN_TIME_KEYB_MS = 750;
private static final int MINIMUM_BUTTON_DOWN_TIME_MS = 25;
@@ -43,27 +27,31 @@ public class ControllerHandler {
private static final int EMULATED_SPECIAL_UP_DELAY_MS = 100;
private static final int EMULATED_SELECT_UP_DELAY_MS = 30;
private Vector2d inputVector = new Vector2d();
private final Vector2d inputVector = new Vector2d();
private HashMap<String, ControllerMapping> mappings = new HashMap<String, ControllerMapping>();
private final SparseArray<ControllerContext> contexts = new SparseArray<ControllerContext>();
private NvConnection conn;
private double stickDeadzone;
private final ControllerMapping defaultMapping = new ControllerMapping();
private GameGestures gestures;
private final NvConnection conn;
private final double stickDeadzone;
private final ControllerContext defaultContext = new ControllerContext();
private final GameGestures gestures;
private boolean hasGameController;
public ControllerHandler(NvConnection conn, GameGestures gestures, int deadzonePercentage) {
private final boolean multiControllerEnabled;
private short currentControllers;
public ControllerHandler(NvConnection conn, GameGestures gestures, boolean multiControllerEnabled, int deadzonePercentage) {
this.conn = conn;
this.gestures = gestures;
this.multiControllerEnabled = multiControllerEnabled;
// HACK: For now we're hardcoding a 10% deadzone. Some deadzone
// is required for controller batching support to work.
deadzonePercentage = 10;
int[] ids = InputDevice.getDeviceIds();
for (int i = 0; i < ids.length; i++) {
InputDevice dev = InputDevice.getDevice(ids[i]);
for (int id : ids) {
InputDevice dev = InputDevice.getDevice(id);
if ((dev.getSources() & InputDevice.SOURCE_JOYSTICK) != 0 ||
(dev.getSources() & InputDevice.SOURCE_GAMEPAD) != 0) {
// This looks like a gamepad, but we'll check X and Y to be sure
@@ -82,15 +70,16 @@ public class ControllerHandler {
this.stickDeadzone = (double)deadzonePercentage / 100.0;
// Initialize the default mapping for events with no device
defaultMapping.leftStickXAxis = MotionEvent.AXIS_X;
defaultMapping.leftStickYAxis = MotionEvent.AXIS_Y;
defaultMapping.leftStickDeadzoneRadius = (float) stickDeadzone;
defaultMapping.rightStickXAxis = MotionEvent.AXIS_Z;
defaultMapping.rightStickYAxis = MotionEvent.AXIS_RZ;
defaultMapping.rightStickDeadzoneRadius = (float) stickDeadzone;
defaultMapping.leftTriggerAxis = MotionEvent.AXIS_BRAKE;
defaultMapping.rightTriggerAxis = MotionEvent.AXIS_GAS;
// Initialize the default context for events with no device
defaultContext.leftStickXAxis = MotionEvent.AXIS_X;
defaultContext.leftStickYAxis = MotionEvent.AXIS_Y;
defaultContext.leftStickDeadzoneRadius = (float) stickDeadzone;
defaultContext.rightStickXAxis = MotionEvent.AXIS_Z;
defaultContext.rightStickYAxis = MotionEvent.AXIS_RZ;
defaultContext.rightStickDeadzoneRadius = (float) stickDeadzone;
defaultContext.leftTriggerAxis = MotionEvent.AXIS_BRAKE;
defaultContext.rightTriggerAxis = MotionEvent.AXIS_GAS;
defaultContext.controllerNumber = (short) 0;
}
private static InputDevice.MotionRange getMotionRangeForJoystickAxis(InputDevice dev, int axis) {
@@ -106,19 +95,87 @@ public class ControllerHandler {
return range;
}
private ControllerMapping createMappingForDevice(InputDevice dev) {
ControllerMapping mapping = new ControllerMapping();
@Override
public void onInputDeviceAdded(int deviceId) {
// Nothing happening here yet
}
@Override
public void onInputDeviceRemoved(int deviceId) {
ControllerContext context = contexts.get(deviceId);
if (context != null) {
LimeLog.info("Removed controller: "+context.name+" ("+deviceId+")");
releaseControllerNumber(context);
contexts.remove(deviceId);
}
}
@Override
public void onInputDeviceChanged(int deviceId) {
// Remove and re-add
onInputDeviceRemoved(deviceId);
onInputDeviceAdded(deviceId);
}
private void releaseControllerNumber(ControllerContext context) {
if (context.reservedControllerNumber) {
LimeLog.info("Controller number "+context.controllerNumber+" is now available");
currentControllers &= ~(1 << context.controllerNumber);
}
}
// Called before sending input but after we've determined that this
// is definitely a controller (not a keyboard, mouse, or something else)
private void assignControllerNumberIfNeeded(ControllerContext context) {
if (context.assignedControllerNumber) {
return;
}
LimeLog.info(context.name+" ("+context.id+") needs a controller number assigned");
if (context.name != null && context.name.contains("gpio-keys")) {
// This is the back button on Shield portable consoles
LimeLog.info("Built-in buttons hardcoded as controller 0");
context.controllerNumber = 0;
}
else if (multiControllerEnabled && context.hasJoystickAxes) {
context.controllerNumber = 0;
LimeLog.info("Reserving the next available controller number");
for (short i = 0; i < 4; i++) {
if ((currentControllers & (1 << i)) == 0) {
// Found an unused controller value
currentControllers |= (1 << i);
context.controllerNumber = i;
context.reservedControllerNumber = true;
break;
}
}
}
else {
LimeLog.info("Not reserving a controller number");
context.controllerNumber = 0;
}
LimeLog.info("Assigned as controller "+context.controllerNumber);
context.assignedControllerNumber = true;
}
private ControllerContext createContextForDevice(InputDevice dev) {
ControllerContext context = new ControllerContext();
String devName = dev.getName();
LimeLog.info("Creating controller mapping for device: "+devName);
LimeLog.info("Creating controller context for device: "+devName);
mapping.leftStickXAxis = MotionEvent.AXIS_X;
mapping.leftStickYAxis = MotionEvent.AXIS_Y;
if (getMotionRangeForJoystickAxis(dev, mapping.leftStickXAxis) != null &&
getMotionRangeForJoystickAxis(dev, mapping.leftStickYAxis) != null) {
context.name = devName;
context.id = dev.getId();
context.leftStickXAxis = MotionEvent.AXIS_X;
context.leftStickYAxis = MotionEvent.AXIS_Y;
if (getMotionRangeForJoystickAxis(dev, context.leftStickXAxis) != null &&
getMotionRangeForJoystickAxis(dev, context.leftStickYAxis) != null) {
// This is a gamepad
hasGameController = true;
mapping.hasJoystickAxes = true;
context.hasJoystickAxes = true;
}
InputDevice.MotionRange leftTriggerRange = getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_LTRIGGER);
@@ -128,14 +185,14 @@ public class ControllerHandler {
if (leftTriggerRange != null && rightTriggerRange != null)
{
// Some controllers use LTRIGGER and RTRIGGER (like Ouya)
mapping.leftTriggerAxis = MotionEvent.AXIS_LTRIGGER;
mapping.rightTriggerAxis = MotionEvent.AXIS_RTRIGGER;
context.leftTriggerAxis = MotionEvent.AXIS_LTRIGGER;
context.rightTriggerAxis = MotionEvent.AXIS_RTRIGGER;
}
else if (brakeRange != null && gasRange != null)
{
// Others use GAS and BRAKE (like Moga)
mapping.leftTriggerAxis = MotionEvent.AXIS_BRAKE;
mapping.rightTriggerAxis = MotionEvent.AXIS_GAS;
context.leftTriggerAxis = MotionEvent.AXIS_BRAKE;
context.rightTriggerAxis = MotionEvent.AXIS_GAS;
}
else
{
@@ -144,34 +201,34 @@ public class ControllerHandler {
if (rxRange != null && ryRange != null && devName != null) {
if (devName.contains("Xbox") || devName.contains("XBox") || devName.contains("X-Box")) {
// Xbox controllers use RX and RY for right stick
mapping.rightStickXAxis = MotionEvent.AXIS_RX;
mapping.rightStickYAxis = MotionEvent.AXIS_RY;
context.rightStickXAxis = MotionEvent.AXIS_RX;
context.rightStickYAxis = MotionEvent.AXIS_RY;
// Xbox controllers use Z and RZ for triggers
mapping.leftTriggerAxis = MotionEvent.AXIS_Z;
mapping.rightTriggerAxis = MotionEvent.AXIS_RZ;
mapping.triggersIdleNegative = true;
mapping.isXboxController = true;
context.leftTriggerAxis = MotionEvent.AXIS_Z;
context.rightTriggerAxis = MotionEvent.AXIS_RZ;
context.triggersIdleNegative = true;
context.isXboxController = true;
}
else {
// DS4 controller uses RX and RY for triggers
mapping.leftTriggerAxis = MotionEvent.AXIS_RX;
mapping.rightTriggerAxis = MotionEvent.AXIS_RY;
mapping.triggersIdleNegative = true;
context.leftTriggerAxis = MotionEvent.AXIS_RX;
context.rightTriggerAxis = MotionEvent.AXIS_RY;
context.triggersIdleNegative = true;
mapping.isDualShock4 = true;
context.isDualShock4 = true;
}
}
}
if (mapping.rightStickXAxis == -1 && mapping.rightStickYAxis == -1) {
if (context.rightStickXAxis == -1 && context.rightStickYAxis == -1) {
InputDevice.MotionRange zRange = getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_Z);
InputDevice.MotionRange rzRange = getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_RZ);
// Most other controllers use Z and RZ for the right stick
if (zRange != null && rzRange != null) {
mapping.rightStickXAxis = MotionEvent.AXIS_Z;
mapping.rightStickYAxis = MotionEvent.AXIS_RZ;
context.rightStickXAxis = MotionEvent.AXIS_Z;
context.rightStickYAxis = MotionEvent.AXIS_RZ;
}
else {
InputDevice.MotionRange rxRange = getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_RX);
@@ -179,8 +236,8 @@ public class ControllerHandler {
// Try RX and RY now
if (rxRange != null && ryRange != null) {
mapping.rightStickXAxis = MotionEvent.AXIS_RX;
mapping.rightStickYAxis = MotionEvent.AXIS_RY;
context.rightStickXAxis = MotionEvent.AXIS_RX;
context.rightStickYAxis = MotionEvent.AXIS_RY;
}
}
}
@@ -189,30 +246,39 @@ public class ControllerHandler {
InputDevice.MotionRange hatXRange = getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_HAT_X);
InputDevice.MotionRange hatYRange = getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_HAT_Y);
if (hatXRange != null && hatYRange != null) {
mapping.hatXAxis = MotionEvent.AXIS_HAT_X;
mapping.hatYAxis = MotionEvent.AXIS_HAT_Y;
context.hatXAxis = MotionEvent.AXIS_HAT_X;
context.hatYAxis = MotionEvent.AXIS_HAT_Y;
}
if (mapping.leftStickXAxis != -1 && mapping.leftStickYAxis != -1) {
mapping.leftStickDeadzoneRadius = (float) stickDeadzone;
if (context.leftStickXAxis != -1 && context.leftStickYAxis != -1) {
context.leftStickDeadzoneRadius = (float) stickDeadzone;
}
if (mapping.rightStickXAxis != -1 && mapping.rightStickYAxis != -1) {
mapping.rightStickDeadzoneRadius = (float) stickDeadzone;
if (context.rightStickXAxis != -1 && context.rightStickYAxis != -1) {
context.rightStickDeadzoneRadius = (float) stickDeadzone;
}
if (mapping.leftTriggerAxis != -1 && mapping.rightTriggerAxis != -1) {
InputDevice.MotionRange ltRange = getMotionRangeForJoystickAxis(dev, mapping.leftTriggerAxis);
InputDevice.MotionRange rtRange = getMotionRangeForJoystickAxis(dev, mapping.rightTriggerAxis);
if (context.leftTriggerAxis != -1 && context.rightTriggerAxis != -1) {
InputDevice.MotionRange ltRange = getMotionRangeForJoystickAxis(dev, context.leftTriggerAxis);
InputDevice.MotionRange rtRange = getMotionRangeForJoystickAxis(dev, context.rightTriggerAxis);
// It's important to have a valid deadzone so controller packet batching works properly
mapping.triggerDeadzone = Math.max(Math.abs(ltRange.getFlat()), Math.abs(rtRange.getFlat()));
context.triggerDeadzone = Math.max(Math.abs(ltRange.getFlat()), Math.abs(rtRange.getFlat()));
// For triggers without (valid) deadzones, we'll use 13% (around XInput's default)
if (mapping.triggerDeadzone < 0.13f ||
mapping.triggerDeadzone > 0.30f)
if (context.triggerDeadzone < 0.13f ||
context.triggerDeadzone > 0.30f)
{
mapping.triggerDeadzone = 0.13f;
context.triggerDeadzone = 0.13f;
}
}
// Ignore the back buttonn if a controller has both buttons
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.KITKAT) {
boolean[] hasSelectKey = dev.hasKeys(KeyEvent.KEYCODE_BUTTON_SELECT, KeyEvent.KEYCODE_BACK, 0);
if (hasSelectKey[0] && hasSelectKey[1]) {
LimeLog.info("Ignoring back button because select is present");
context.ignoreBack = true;
}
}
@@ -226,71 +292,84 @@ public class ControllerHandler {
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.KITKAT) {
boolean[] hasStartKey = dev.hasKeys(KeyEvent.KEYCODE_BUTTON_START, KeyEvent.KEYCODE_MENU, 0);
if (!hasStartKey[0] && !hasStartKey[1]) {
mapping.backIsStart = true;
context.backIsStart = true;
context.modeIsSelect = true;
}
}
// The ASUS Gamepad has triggers that sit far forward and are prone to false presses
// so we increase the deadzone on them to minimize this
mapping.triggerDeadzone = 0.30f;
context.triggerDeadzone = 0.30f;
}
// Classify this device as a remote by name
else if (devName.contains("Fire TV Remote") || devName.contains("Nexus Remote")) {
// It's only a remote if it doesn't any sticks
if (!mapping.hasJoystickAxes) {
mapping.isRemote = true;
if (!context.hasJoystickAxes) {
context.ignoreBack = true;
}
}
// NYKO Playpad has a fake hat that mimics the left stick for some reason
else if (devName.contains("NYKO PLAYPAD")) {
mapping.hatXAxis = -1;
mapping.hatYAxis = -1;
// SHIELD controllers will use small stick deadzones
else if (devName.contains("SHIELD")) {
context.leftStickDeadzoneRadius = 0.07f;
context.rightStickDeadzoneRadius = 0.07f;
}
// Samsung's face buttons appear as a non-virtual button so we'll explicitly ignore
// back presses on this device
else if (devName.equals("sec_touchscreen")) {
context.ignoreBack = true;
}
// The Serval has a couple of unknown buttons that are start and select. It also has
// a back button which we want to ignore since there's already a select button.
else if (devName.contains("Razer Serval")) {
context.isServal = true;
context.ignoreBack = true;
}
}
LimeLog.info("Analog stick deadzone: "+mapping.leftStickDeadzoneRadius+" "+mapping.rightStickDeadzoneRadius);
LimeLog.info("Trigger deadzone: "+mapping.triggerDeadzone);
LimeLog.info("Analog stick deadzone: "+context.leftStickDeadzoneRadius+" "+context.rightStickDeadzoneRadius);
LimeLog.info("Trigger deadzone: "+context.triggerDeadzone);
return mapping;
return context;
}
private ControllerMapping getMappingForDevice(InputDevice dev) {
// Unknown devices use the default mapping
private ControllerContext getContextForDevice(InputDevice dev) {
// Unknown devices use the default context
if (dev == null) {
return defaultMapping;
return defaultContext;
}
String descriptor = dev.getDescriptor();
// Return the existing mapping if it exists
ControllerMapping mapping = mappings.get(descriptor);
if (mapping != null) {
return mapping;
// Return the existing context if it exists
ControllerContext context = contexts.get(dev.getId());
if (context != null) {
return context;
}
// Otherwise create a new mapping
mapping = createMappingForDevice(dev);
mappings.put(descriptor, mapping);
// Otherwise create a new context
context = createContextForDevice(dev);
contexts.put(dev.getId(), context);
return mapping;
return context;
}
private void sendControllerInputPacket() {
conn.sendControllerInput(inputMap, leftTrigger, rightTrigger,
leftStickX, leftStickY, rightStickX, rightStickY);
private void sendControllerInputPacket(ControllerContext context) {
assignControllerNumberIfNeeded(context);
conn.sendControllerInput(context.controllerNumber, context.inputMap,
context.leftTrigger, context.rightTrigger,
context.leftStickX, context.leftStickY,
context.rightStickX, context.rightStickY);
}
// Return a valid keycode, 0 to consume, or -1 to not consume the event
// Device MAY BE NULL
private int handleRemapping(ControllerMapping mapping, KeyEvent event) {
// For remotes, don't capture the back button
if (mapping.isRemote) {
private int handleRemapping(ControllerContext context, KeyEvent event) {
// Don't capture the back button if configured
if (context.ignoreBack) {
if (event.getKeyCode() == KeyEvent.KEYCODE_BACK) {
return -1;
}
}
if (mapping.isDualShock4) {
if (context.isDualShock4) {
switch (event.getKeyCode()) {
case KeyEvent.KEYCODE_BUTTON_Y:
return KeyEvent.KEYCODE_BUTTON_L1;
@@ -328,8 +407,18 @@ public class ControllerHandler {
return 0;
}
}
// If this is a Serval controller sending an unknown key code, it's probably
// the start and select buttons
else if (context.isServal && event.getKeyCode() == KeyEvent.KEYCODE_UNKNOWN) {
switch (event.getScanCode()) {
case 314:
return KeyEvent.KEYCODE_BUTTON_SELECT;
case 315:
return KeyEvent.KEYCODE_BUTTON_START;
}
}
if (mapping.hatXAxis != -1 && mapping.hatYAxis != -1) {
if (context.hatXAxis != -1 && context.hatYAxis != -1) {
switch (event.getKeyCode()) {
// These are duplicate dpad events for hat input
case KeyEvent.KEYCODE_DPAD_LEFT:
@@ -340,9 +429,9 @@ public class ControllerHandler {
return 0;
}
}
else if (mapping.hatXAxis == -1 &&
mapping.hatYAxis == -1 &&
mapping.isXboxController &&
else if (context.hatXAxis == -1 &&
context.hatYAxis == -1 &&
context.isXboxController &&
event.getKeyCode() == KeyEvent.KEYCODE_UNKNOWN) {
// If there's not a proper Xbox controller mapping, we'll translate the raw d-pad
// scan codes into proper key codes
@@ -375,12 +464,20 @@ public class ControllerHandler {
if (keyCode == KeyEvent.KEYCODE_BUTTON_START ||
keyCode == KeyEvent.KEYCODE_MENU) {
// Ensure that we never use back as start if we have a real start
mapping.backIsStart = false;
context.backIsStart = false;
}
else if (mapping.backIsStart && keyCode == KeyEvent.KEYCODE_BACK) {
else if (keyCode == KeyEvent.KEYCODE_BUTTON_SELECT) {
// Don't use mode as select if we have a select
context.modeIsSelect = false;
}
else if (context.backIsStart && keyCode == KeyEvent.KEYCODE_BACK) {
// Emulate the start button with back
return KeyEvent.KEYCODE_BUTTON_START;
}
else if (context.modeIsSelect && keyCode == KeyEvent.KEYCODE_BUTTON_MODE) {
// Emulate the select button with mode
return KeyEvent.KEYCODE_BUTTON_SELECT;
}
return keyCode;
}
@@ -402,101 +499,101 @@ public class ControllerHandler {
// evaluates the deadzone.
}
private void handleAxisSet(ControllerMapping mapping, float lsX, float lsY, float rsX,
private void handleAxisSet(ControllerContext context, float lsX, float lsY, float rsX,
float rsY, float lt, float rt, float hatX, float hatY) {
if (mapping.leftStickXAxis != -1 && mapping.leftStickYAxis != -1) {
if (context.leftStickXAxis != -1 && context.leftStickYAxis != -1) {
Vector2d leftStickVector = populateCachedVector(lsX, lsY);
handleDeadZone(leftStickVector, mapping.leftStickDeadzoneRadius);
handleDeadZone(leftStickVector, context.leftStickDeadzoneRadius);
leftStickX = (short) (leftStickVector.getX() * 0x7FFE);
leftStickY = (short) (-leftStickVector.getY() * 0x7FFE);
context.leftStickX = (short) (leftStickVector.getX() * 0x7FFE);
context.leftStickY = (short) (-leftStickVector.getY() * 0x7FFE);
}
if (mapping.rightStickXAxis != -1 && mapping.rightStickYAxis != -1) {
if (context.rightStickXAxis != -1 && context.rightStickYAxis != -1) {
Vector2d rightStickVector = populateCachedVector(rsX, rsY);
handleDeadZone(rightStickVector, mapping.rightStickDeadzoneRadius);
handleDeadZone(rightStickVector, context.rightStickDeadzoneRadius);
rightStickX = (short) (rightStickVector.getX() * 0x7FFE);
rightStickY = (short) (-rightStickVector.getY() * 0x7FFE);
context.rightStickX = (short) (rightStickVector.getX() * 0x7FFE);
context.rightStickY = (short) (-rightStickVector.getY() * 0x7FFE);
}
if (mapping.leftTriggerAxis != -1 && mapping.rightTriggerAxis != -1) {
if (mapping.triggersIdleNegative) {
if (context.leftTriggerAxis != -1 && context.rightTriggerAxis != -1) {
if (context.triggersIdleNegative) {
lt = (lt + 1) / 2;
rt = (rt + 1) / 2;
}
if (lt <= mapping.triggerDeadzone) {
if (lt <= context.triggerDeadzone) {
lt = 0;
}
if (rt <= mapping.triggerDeadzone) {
if (rt <= context.triggerDeadzone) {
rt = 0;
}
leftTrigger = (byte)(lt * 0xFF);
rightTrigger = (byte)(rt * 0xFF);
context.leftTrigger = (byte)(lt * 0xFF);
context.rightTrigger = (byte)(rt * 0xFF);
}
if (mapping.hatXAxis != -1 && mapping.hatYAxis != -1) {
inputMap &= ~(ControllerPacket.LEFT_FLAG | ControllerPacket.RIGHT_FLAG);
if (context.hatXAxis != -1 && context.hatYAxis != -1) {
context.inputMap &= ~(ControllerPacket.LEFT_FLAG | ControllerPacket.RIGHT_FLAG);
if (hatX < -0.5) {
inputMap |= ControllerPacket.LEFT_FLAG;
context.inputMap |= ControllerPacket.LEFT_FLAG;
}
else if (hatX > 0.5) {
inputMap |= ControllerPacket.RIGHT_FLAG;
context.inputMap |= ControllerPacket.RIGHT_FLAG;
}
inputMap &= ~(ControllerPacket.UP_FLAG | ControllerPacket.DOWN_FLAG);
context.inputMap &= ~(ControllerPacket.UP_FLAG | ControllerPacket.DOWN_FLAG);
if (hatY < -0.5) {
inputMap |= ControllerPacket.UP_FLAG;
context.inputMap |= ControllerPacket.UP_FLAG;
}
else if (hatY > 0.5) {
inputMap |= ControllerPacket.DOWN_FLAG;
context.inputMap |= ControllerPacket.DOWN_FLAG;
}
}
sendControllerInputPacket();
sendControllerInputPacket(context);
}
public boolean handleMotionEvent(MotionEvent event) {
ControllerMapping mapping = getMappingForDevice(event.getDevice());
ControllerContext context = getContextForDevice(event.getDevice());
float lsX = 0, lsY = 0, rsX = 0, rsY = 0, rt = 0, lt = 0, hatX = 0, hatY = 0;
// We purposefully ignore the historical values in the motion event as it makes
// the controller feel sluggish for some users.
if (mapping.leftStickXAxis != -1 && mapping.leftStickYAxis != -1) {
lsX = event.getAxisValue(mapping.leftStickXAxis);
lsY = event.getAxisValue(mapping.leftStickYAxis);
if (context.leftStickXAxis != -1 && context.leftStickYAxis != -1) {
lsX = event.getAxisValue(context.leftStickXAxis);
lsY = event.getAxisValue(context.leftStickYAxis);
}
if (mapping.rightStickXAxis != -1 && mapping.rightStickYAxis != -1) {
rsX = event.getAxisValue(mapping.rightStickXAxis);
rsY = event.getAxisValue(mapping.rightStickYAxis);
if (context.rightStickXAxis != -1 && context.rightStickYAxis != -1) {
rsX = event.getAxisValue(context.rightStickXAxis);
rsY = event.getAxisValue(context.rightStickYAxis);
}
if (mapping.leftTriggerAxis != -1 && mapping.rightTriggerAxis != -1) {
lt = event.getAxisValue(mapping.leftTriggerAxis);
rt = event.getAxisValue(mapping.rightTriggerAxis);
if (context.leftTriggerAxis != -1 && context.rightTriggerAxis != -1) {
lt = event.getAxisValue(context.leftTriggerAxis);
rt = event.getAxisValue(context.rightTriggerAxis);
}
if (mapping.hatXAxis != -1 && mapping.hatYAxis != -1) {
if (context.hatXAxis != -1 && context.hatYAxis != -1) {
hatX = event.getAxisValue(MotionEvent.AXIS_HAT_X);
hatY = event.getAxisValue(MotionEvent.AXIS_HAT_Y);
}
handleAxisSet(mapping, lsX, lsY, rsX, rsY, lt, rt, hatX, hatY);
handleAxisSet(context, lsX, lsY, rsX, rsY, lt, rt, hatX, hatY);
return true;
}
public boolean handleButtonUp(KeyEvent event) {
ControllerMapping mapping = getMappingForDevice(event.getDevice());
ControllerContext context = getContextForDevice(event.getDevice());
int keyCode = handleRemapping(mapping, event);
int keyCode = handleRemapping(context, event);
if (keyCode == 0) {
return true;
}
@@ -515,78 +612,80 @@ public class ControllerHandler {
switch (keyCode) {
case KeyEvent.KEYCODE_BUTTON_MODE:
inputMap &= ~ControllerPacket.SPECIAL_BUTTON_FLAG;
context.inputMap &= ~ControllerPacket.SPECIAL_BUTTON_FLAG;
break;
case KeyEvent.KEYCODE_BUTTON_START:
case KeyEvent.KEYCODE_MENU:
if (SystemClock.uptimeMillis() - startDownTime > ControllerHandler.START_DOWN_TIME_KEYB_MS) {
gestures.showKeyboard();
if (SystemClock.uptimeMillis() - context.startDownTime > ControllerHandler.START_DOWN_TIME_KEYB_MS) {
// FIXME: The stock keyboard doesn't have controller focus so isn't usable. I'm not enabling this shortcut
// until we have a custom keyboard or some other fix
//gestures.showKeyboard();
}
inputMap &= ~ControllerPacket.PLAY_FLAG;
context.inputMap &= ~ControllerPacket.PLAY_FLAG;
break;
case KeyEvent.KEYCODE_BACK:
case KeyEvent.KEYCODE_BUTTON_SELECT:
inputMap &= ~ControllerPacket.BACK_FLAG;
context.inputMap &= ~ControllerPacket.BACK_FLAG;
break;
case KeyEvent.KEYCODE_DPAD_LEFT:
inputMap &= ~ControllerPacket.LEFT_FLAG;
context.inputMap &= ~ControllerPacket.LEFT_FLAG;
break;
case KeyEvent.KEYCODE_DPAD_RIGHT:
inputMap &= ~ControllerPacket.RIGHT_FLAG;
context.inputMap &= ~ControllerPacket.RIGHT_FLAG;
break;
case KeyEvent.KEYCODE_DPAD_UP:
inputMap &= ~ControllerPacket.UP_FLAG;
context.inputMap &= ~ControllerPacket.UP_FLAG;
break;
case KeyEvent.KEYCODE_DPAD_DOWN:
inputMap &= ~ControllerPacket.DOWN_FLAG;
context.inputMap &= ~ControllerPacket.DOWN_FLAG;
break;
case KeyEvent.KEYCODE_BUTTON_B:
inputMap &= ~ControllerPacket.B_FLAG;
context.inputMap &= ~ControllerPacket.B_FLAG;
break;
case KeyEvent.KEYCODE_DPAD_CENTER:
case KeyEvent.KEYCODE_BUTTON_A:
inputMap &= ~ControllerPacket.A_FLAG;
context.inputMap &= ~ControllerPacket.A_FLAG;
break;
case KeyEvent.KEYCODE_BUTTON_X:
inputMap &= ~ControllerPacket.X_FLAG;
context.inputMap &= ~ControllerPacket.X_FLAG;
break;
case KeyEvent.KEYCODE_BUTTON_Y:
inputMap &= ~ControllerPacket.Y_FLAG;
context.inputMap &= ~ControllerPacket.Y_FLAG;
break;
case KeyEvent.KEYCODE_BUTTON_L1:
inputMap &= ~ControllerPacket.LB_FLAG;
lastLbUpTime = SystemClock.uptimeMillis();
context.inputMap &= ~ControllerPacket.LB_FLAG;
context.lastLbUpTime = SystemClock.uptimeMillis();
break;
case KeyEvent.KEYCODE_BUTTON_R1:
inputMap &= ~ControllerPacket.RB_FLAG;
lastRbUpTime = SystemClock.uptimeMillis();
context.inputMap &= ~ControllerPacket.RB_FLAG;
context.lastRbUpTime = SystemClock.uptimeMillis();
break;
case KeyEvent.KEYCODE_BUTTON_THUMBL:
inputMap &= ~ControllerPacket.LS_CLK_FLAG;
context.inputMap &= ~ControllerPacket.LS_CLK_FLAG;
break;
case KeyEvent.KEYCODE_BUTTON_THUMBR:
inputMap &= ~ControllerPacket.RS_CLK_FLAG;
context.inputMap &= ~ControllerPacket.RS_CLK_FLAG;
break;
case KeyEvent.KEYCODE_BUTTON_L2:
leftTrigger = 0;
context.leftTrigger = 0;
break;
case KeyEvent.KEYCODE_BUTTON_R2:
rightTrigger = 0;
context.rightTrigger = 0;
break;
default:
return false;
}
// Check if we're emulating the select button
if ((emulatingButtonFlags & ControllerHandler.EMULATING_SELECT) != 0)
if ((context.emulatingButtonFlags & ControllerHandler.EMULATING_SELECT) != 0)
{
// If either start or LB is up, select comes up too
if ((inputMap & ControllerPacket.PLAY_FLAG) == 0 ||
(inputMap & ControllerPacket.LB_FLAG) == 0)
if ((context.inputMap & ControllerPacket.PLAY_FLAG) == 0 ||
(context.inputMap & ControllerPacket.LB_FLAG) == 0)
{
inputMap &= ~ControllerPacket.BACK_FLAG;
context.inputMap &= ~ControllerPacket.BACK_FLAG;
emulatingButtonFlags &= ~ControllerHandler.EMULATING_SELECT;
context.emulatingButtonFlags &= ~ControllerHandler.EMULATING_SELECT;
try {
Thread.sleep(EMULATED_SELECT_UP_DELAY_MS);
@@ -595,16 +694,16 @@ public class ControllerHandler {
}
// Check if we're emulating the special button
if ((emulatingButtonFlags & ControllerHandler.EMULATING_SPECIAL) != 0)
if ((context.emulatingButtonFlags & ControllerHandler.EMULATING_SPECIAL) != 0)
{
// If either start or select and RB is up, the special button comes up too
if ((inputMap & ControllerPacket.PLAY_FLAG) == 0 ||
((inputMap & ControllerPacket.BACK_FLAG) == 0 &&
(inputMap & ControllerPacket.RB_FLAG) == 0))
if ((context.inputMap & ControllerPacket.PLAY_FLAG) == 0 ||
((context.inputMap & ControllerPacket.BACK_FLAG) == 0 &&
(context.inputMap & ControllerPacket.RB_FLAG) == 0))
{
inputMap &= ~ControllerPacket.SPECIAL_BUTTON_FLAG;
context.inputMap &= ~ControllerPacket.SPECIAL_BUTTON_FLAG;
emulatingButtonFlags &= ~ControllerHandler.EMULATING_SPECIAL;
context.emulatingButtonFlags &= ~ControllerHandler.EMULATING_SPECIAL;
try {
Thread.sleep(EMULATED_SPECIAL_UP_DELAY_MS);
@@ -612,112 +711,115 @@ public class ControllerHandler {
}
}
sendControllerInputPacket();
sendControllerInputPacket(context);
return true;
}
public boolean handleButtonDown(KeyEvent event) {
ControllerMapping mapping = getMappingForDevice(event.getDevice());
ControllerContext context = getContextForDevice(event.getDevice());
int keyCode = handleRemapping(mapping, event);
int keyCode = handleRemapping(context, event);
if (keyCode == 0) {
return true;
}
switch (keyCode) {
case KeyEvent.KEYCODE_BUTTON_MODE:
inputMap |= ControllerPacket.SPECIAL_BUTTON_FLAG;
context.inputMap |= ControllerPacket.SPECIAL_BUTTON_FLAG;
break;
case KeyEvent.KEYCODE_BUTTON_START:
case KeyEvent.KEYCODE_MENU:
if (event.getRepeatCount() == 0) {
startDownTime = SystemClock.uptimeMillis();
context.startDownTime = SystemClock.uptimeMillis();
}
inputMap |= ControllerPacket.PLAY_FLAG;
context.inputMap |= ControllerPacket.PLAY_FLAG;
break;
case KeyEvent.KEYCODE_BACK:
case KeyEvent.KEYCODE_BUTTON_SELECT:
inputMap |= ControllerPacket.BACK_FLAG;
context.inputMap |= ControllerPacket.BACK_FLAG;
break;
case KeyEvent.KEYCODE_DPAD_LEFT:
inputMap |= ControllerPacket.LEFT_FLAG;
context.inputMap |= ControllerPacket.LEFT_FLAG;
break;
case KeyEvent.KEYCODE_DPAD_RIGHT:
inputMap |= ControllerPacket.RIGHT_FLAG;
context.inputMap |= ControllerPacket.RIGHT_FLAG;
break;
case KeyEvent.KEYCODE_DPAD_UP:
inputMap |= ControllerPacket.UP_FLAG;
context.inputMap |= ControllerPacket.UP_FLAG;
break;
case KeyEvent.KEYCODE_DPAD_DOWN:
inputMap |= ControllerPacket.DOWN_FLAG;
context.inputMap |= ControllerPacket.DOWN_FLAG;
break;
case KeyEvent.KEYCODE_BUTTON_B:
inputMap |= ControllerPacket.B_FLAG;
context.inputMap |= ControllerPacket.B_FLAG;
break;
case KeyEvent.KEYCODE_DPAD_CENTER:
case KeyEvent.KEYCODE_BUTTON_A:
inputMap |= ControllerPacket.A_FLAG;
context.inputMap |= ControllerPacket.A_FLAG;
break;
case KeyEvent.KEYCODE_BUTTON_X:
inputMap |= ControllerPacket.X_FLAG;
context.inputMap |= ControllerPacket.X_FLAG;
break;
case KeyEvent.KEYCODE_BUTTON_Y:
inputMap |= ControllerPacket.Y_FLAG;
context.inputMap |= ControllerPacket.Y_FLAG;
break;
case KeyEvent.KEYCODE_BUTTON_L1:
inputMap |= ControllerPacket.LB_FLAG;
context.inputMap |= ControllerPacket.LB_FLAG;
break;
case KeyEvent.KEYCODE_BUTTON_R1:
inputMap |= ControllerPacket.RB_FLAG;
context.inputMap |= ControllerPacket.RB_FLAG;
break;
case KeyEvent.KEYCODE_BUTTON_THUMBL:
inputMap |= ControllerPacket.LS_CLK_FLAG;
context.inputMap |= ControllerPacket.LS_CLK_FLAG;
break;
case KeyEvent.KEYCODE_BUTTON_THUMBR:
inputMap |= ControllerPacket.RS_CLK_FLAG;
context.inputMap |= ControllerPacket.RS_CLK_FLAG;
break;
case KeyEvent.KEYCODE_BUTTON_L2:
leftTrigger = (byte)0xFF;
context.leftTrigger = (byte)0xFF;
break;
case KeyEvent.KEYCODE_BUTTON_R2:
rightTrigger = (byte)0xFF;
context.rightTrigger = (byte)0xFF;
break;
default:
return false;
}
// Start+LB acts like select for controllers with one button
if ((inputMap & ControllerPacket.PLAY_FLAG) != 0 &&
((inputMap & ControllerPacket.LB_FLAG) != 0 ||
SystemClock.uptimeMillis() - lastLbUpTime <= MAXIMUM_BUMPER_UP_DELAY_MS))
if ((context.inputMap & ControllerPacket.PLAY_FLAG) != 0 &&
((context.inputMap & ControllerPacket.LB_FLAG) != 0 ||
SystemClock.uptimeMillis() - context.lastLbUpTime <= MAXIMUM_BUMPER_UP_DELAY_MS))
{
inputMap &= ~(ControllerPacket.PLAY_FLAG | ControllerPacket.LB_FLAG);
inputMap |= ControllerPacket.BACK_FLAG;
context.inputMap &= ~(ControllerPacket.PLAY_FLAG | ControllerPacket.LB_FLAG);
context.inputMap |= ControllerPacket.BACK_FLAG;
emulatingButtonFlags |= ControllerHandler.EMULATING_SELECT;
context.emulatingButtonFlags |= ControllerHandler.EMULATING_SELECT;
}
// We detect select+start or start+RB as the special button combo
if (((inputMap & ControllerPacket.RB_FLAG) != 0 ||
(SystemClock.uptimeMillis() - lastRbUpTime <= MAXIMUM_BUMPER_UP_DELAY_MS) ||
(inputMap & ControllerPacket.BACK_FLAG) != 0) &&
(inputMap & ControllerPacket.PLAY_FLAG) != 0)
if (((context.inputMap & ControllerPacket.RB_FLAG) != 0 ||
(SystemClock.uptimeMillis() - context.lastRbUpTime <= MAXIMUM_BUMPER_UP_DELAY_MS) ||
(context.inputMap & ControllerPacket.BACK_FLAG) != 0) &&
(context.inputMap & ControllerPacket.PLAY_FLAG) != 0)
{
inputMap &= ~(ControllerPacket.BACK_FLAG | ControllerPacket.PLAY_FLAG | ControllerPacket.RB_FLAG);
inputMap |= ControllerPacket.SPECIAL_BUTTON_FLAG;
context.inputMap &= ~(ControllerPacket.BACK_FLAG | ControllerPacket.PLAY_FLAG | ControllerPacket.RB_FLAG);
context.inputMap |= ControllerPacket.SPECIAL_BUTTON_FLAG;
emulatingButtonFlags |= ControllerHandler.EMULATING_SPECIAL;
context.emulatingButtonFlags |= ControllerHandler.EMULATING_SPECIAL;
}
// Send a new input packet if this is the first instance of a button down event
// or anytime if we're emulating a button
if (event.getRepeatCount() == 0 || emulatingButtonFlags != 0) {
sendControllerInputPacket();
if (event.getRepeatCount() == 0 || context.emulatingButtonFlags != 0) {
sendControllerInputPacket(context);
}
return true;
}
class ControllerMapping {
class ControllerContext {
public String name;
public int id;
public int leftStickXAxis = -1;
public int leftStickYAxis = -1;
public float leftStickDeadzoneRadius;
@@ -736,8 +838,33 @@ public class ControllerHandler {
public boolean isDualShock4;
public boolean isXboxController;
public boolean isServal;
public boolean backIsStart;
public boolean isRemote;
public boolean modeIsSelect;
public boolean ignoreBack;
public boolean hasJoystickAxes;
public boolean assignedControllerNumber;
public boolean reservedControllerNumber;
public short controllerNumber;
public short inputMap = 0x0000;
public byte leftTrigger = 0x00;
public byte rightTrigger = 0x00;
public short rightStickX = 0x0000;
public short rightStickY = 0x0000;
public short leftStickX = 0x0000;
public short leftStickY = 0x0000;
public int emulatingButtonFlags = 0;
// Used for OUYA bumper state tracking since they force all buttons
// up when the OUYA button goes down. We watch the last time we get
// a bumper up and compare that to our maximum delay when we receive
// a Start button press to see if we should activate one of our
// emulated button combos.
public long lastLbUpTime = 0;
public long lastRbUpTime = 0;
public long startDownTime = 0;
}
}
@@ -15,7 +15,7 @@ public class KeyboardTranslator extends KeycodeTranslator {
/**
* GFE's prefix for every key code
*/
public static final short KEY_PREFIX = (short) 0x80;
private static final short KEY_PREFIX = (short) 0x80;
public static final int VK_0 = 48;
public static final int VK_9 = 57;
@@ -11,9 +11,10 @@ public class TouchContext {
private long originalTouchTime = 0;
private boolean cancelled;
private NvConnection conn;
private int actionIndex;
private double xFactor, yFactor;
private final NvConnection conn;
private final int actionIndex;
private final double xFactor;
private final double yFactor;
private static final int TAP_MOVEMENT_THRESHOLD = 10;
private static final int TAP_TIME_THRESHOLD = 250;
@@ -29,9 +29,9 @@ public class EvdevEvent {
/* Keys */
public static final short KEY_Q = 16;
public short type;
public short code;
public int value;
public final short type;
public final short code;
public final int value;
public EvdevEvent(short type, short code, int value) {
this.type = type;
@@ -7,12 +7,12 @@ import com.limelight.LimeLog;
public class EvdevHandler {
private String absolutePath;
private EvdevListener listener;
private final String absolutePath;
private final EvdevListener listener;
private boolean shutdown = false;
private int fd = -1;
private Thread handlerThread = new Thread() {
private final Thread handlerThread = new Thread() {
@Override
public void run() {
// All the finally blocks here make this code look like a mess
@@ -26,7 +26,7 @@ public class EvdevReader {
// 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 { getattr read search }\" " +
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 }\"");
}
}
@@ -4,7 +4,7 @@ import android.view.KeyEvent;
public class EvdevTranslator {
public static final short EVDEV_KEY_CODES[] = {
private static final short[] EVDEV_KEY_CODES = {
0, //KeyEvent.VK_RESERVED
KeyEvent.KEYCODE_ESCAPE,
KeyEvent.KEYCODE_1,
@@ -36,7 +36,7 @@ public class AndroidCpuDecoderRenderer extends EnhancedDecoderRenderer {
private int totalFrames;
private long totalTimeMs;
private int cpuCount = Runtime.getRuntime().availableProcessors();
private final int cpuCount = Runtime.getRuntime().availableProcessors();
@SuppressWarnings("unused")
private int findOptimalPerformanceLevel() {
@@ -178,7 +178,6 @@ public class AndroidCpuDecoderRenderer extends EnhancedDecoderRenderer {
@Override
public void run() {
long nextFrameTime = System.currentTimeMillis();
DecodeUnit du;
while (!isInterrupted())
{
long diff = nextFrameTime - System.currentTimeMillis();
@@ -232,7 +231,8 @@ public class AndroidCpuDecoderRenderer extends EnhancedDecoderRenderer {
if (decodeUnit.getDataLength() <= DECODER_BUFFER_SIZE) {
decoderBuffer.clear();
for (ByteBufferDescriptor bbd : decodeUnit.getBufferList()) {
for (ByteBufferDescriptor bbd = decodeUnit.getBufferHead();
bbd != null; bbd = bbd.nextDescriptor) {
decoderBuffer.put(bbd.data, bbd.offset, bbd.length);
}
@@ -242,7 +242,8 @@ public class AndroidCpuDecoderRenderer extends EnhancedDecoderRenderer {
data = new byte[decodeUnit.getDataLength()+AvcDecoder.getInputPaddingSize()];
int offset = 0;
for (ByteBufferDescriptor bbd : decodeUnit.getBufferList()) {
for (ByteBufferDescriptor bbd = decodeUnit.getBufferHead();
bbd != null; bbd = bbd.nextDescriptor) {
System.arraycopy(bbd.data, bbd.offset, data, offset, bbd.length);
offset += bbd.length;
}
@@ -1,5 +1,6 @@
package com.limelight.binding.video;
import com.limelight.nvstream.av.DecodeUnit;
import com.limelight.nvstream.av.video.VideoDecoderRenderer;
import com.limelight.nvstream.av.video.VideoDepacketizer;
@@ -55,6 +56,11 @@ public class ConfigurableDecoderRenderer extends EnhancedDecoderRenderer {
return decoderRenderer.getCapabilities();
}
@Override
public void directSubmitDecodeUnit(DecodeUnit du) {
decoderRenderer.directSubmitDecodeUnit(du);
}
@Override
public int getAverageDecoderLatency() {
if (decoderRenderer != null) {
@@ -2,6 +2,6 @@ package com.limelight.binding.video;
import com.limelight.nvstream.av.video.VideoDecoderRenderer;
public abstract class EnhancedDecoderRenderer implements VideoDecoderRenderer {
public abstract class EnhancedDecoderRenderer extends VideoDecoderRenderer {
public abstract String getDecoderName();
}
@@ -28,9 +28,9 @@ public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer {
private ByteBuffer[] videoDecoderInputBuffers;
private MediaCodec videoDecoder;
private Thread rendererThread;
private boolean needsSpsBitstreamFixup, isExynos4;
private final boolean needsSpsBitstreamFixup, isExynos4;
private VideoDepacketizer depacketizer;
private boolean adaptivePlayback;
private final boolean adaptivePlayback, directSubmit;
private int initialWidth, initialHeight;
private boolean needsBaselineSpsHack;
@@ -46,8 +46,6 @@ public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer {
private int numPpsIn;
private int numIframeIn;
private static final boolean ENABLE_ASYNC_RENDERER = false;
@TargetApi(Build.VERSION_CODES.KITKAT)
public MediaCodecDecoderRenderer() {
//dumpDecoders();
@@ -58,12 +56,15 @@ public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer {
}
if (decoder == null) {
// This case is handled later in setup()
needsSpsBitstreamFixup = isExynos4 =
adaptivePlayback = directSubmit = false;
return;
}
decoderName = decoder.getName();
// Set decoder-specific attributes
directSubmit = MediaCodecHelper.decoderCanDirectSubmit(decoderName, decoder);
adaptivePlayback = MediaCodecHelper.decoderSupportsAdaptivePlayback(decoderName, decoder);
needsSpsBitstreamFixup = MediaCodecHelper.decoderNeedsSpsBitstreamRestrictions(decoderName, decoder);
needsBaselineSpsHack = MediaCodecHelper.decoderNeedsBaselineSpsHack(decoderName, decoder);
@@ -77,9 +78,11 @@ public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer {
if (isExynos4) {
LimeLog.info("Decoder "+decoderName+" is on Exynos 4");
}
if (directSubmit) {
LimeLog.info("Decoder "+decoderName+" will use direct submit");
}
}
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
@Override
public boolean setup(int width, int height, int redrawRate, Object renderTarget, int drFlags) {
this.initialWidth = width;
@@ -107,52 +110,6 @@ public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer {
videoFormat.setInteger(MediaFormat.KEY_MAX_HEIGHT, height);
}
// On Lollipop, we use asynchronous mode to avoid having a busy looping renderer thread
if (ENABLE_ASYNC_RENDERER && Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
videoDecoder.setCallback(new MediaCodec.Callback() {
@Override
public void onOutputFormatChanged(MediaCodec codec, MediaFormat format) {
LimeLog.info("Output format changed");
LimeLog.info("New output Format: " + format);
}
@Override
public void onOutputBufferAvailable(MediaCodec codec, int index,
BufferInfo info) {
try {
// FIXME: It looks like we can't frameskip here
codec.releaseOutputBuffer(index, true);
} catch (Exception e) {
handleDecoderException(MediaCodecDecoderRenderer.this, e, null, 0);
}
}
@Override
public void onInputBufferAvailable(MediaCodec codec, int index) {
try {
submitDecodeUnit(depacketizer.takeNextDecodeUnit(), codec.getInputBuffer(index), index);
} catch (InterruptedException e) {
// What do we do here?
e.printStackTrace();
} catch (Exception e) {
handleDecoderException(MediaCodecDecoderRenderer.this, e, null, 0);
}
}
@Override
public void onError(MediaCodec codec, CodecException e) {
if (e.isTransient()) {
LimeLog.warning(e.getDiagnosticInfo());
e.printStackTrace();
}
else {
LimeLog.severe(e.getDiagnosticInfo());
e.printStackTrace();
}
}
});
}
videoDecoder.configure(videoFormat, ((SurfaceHolder)renderTarget).getSurface(), null, 0);
videoDecoder.setVideoScalingMode(MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT);
@@ -162,7 +119,7 @@ public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer {
}
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
private void handleDecoderException(MediaCodecDecoderRenderer dr, Exception e, ByteBuffer buf, int codecFlags) {
private void handleDecoderException(Exception e, ByteBuffer buf, int codecFlags) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
if (e instanceof CodecException) {
CodecException codecExc = (CodecException) e;
@@ -177,15 +134,96 @@ public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer {
}
}
// Only throw if this happens at the beginning of a stream
if (totalFrames < 60) {
if (buf != null || codecFlags != 0) {
throw new RendererException(dr, e, buf, codecFlags);
throw new RendererException(this, e, buf, codecFlags);
}
else {
throw new RendererException(dr, e);
throw new RendererException(this, e);
}
}
}
private void startRendererThread()
private void startDirectSubmitRendererThread()
{
rendererThread = new Thread() {
@SuppressWarnings("deprecation")
@Override
public void run() {
BufferInfo info = new BufferInfo();
while (!isInterrupted()) {
try {
// Try to output a frame
int outIndex = videoDecoder.dequeueOutputBuffer(info, 50000);
if (outIndex >= 0) {
long presentationTimeUs = info.presentationTimeUs;
int lastIndex = outIndex;
// Get the last output buffer in the queue
while ((outIndex = videoDecoder.dequeueOutputBuffer(info, 0)) >= 0) {
videoDecoder.releaseOutputBuffer(lastIndex, false);
lastIndex = outIndex;
presentationTimeUs = info.presentationTimeUs;
}
// Render the last buffer
videoDecoder.releaseOutputBuffer(lastIndex, true);
// Add delta time to the totals (excluding probable outliers)
long delta = System.currentTimeMillis() - (presentationTimeUs / 1000);
if (delta >= 0 && delta < 1000) {
decoderTimeMs += delta;
totalTimeMs += delta;
}
} else {
switch (outIndex) {
case MediaCodec.INFO_TRY_AGAIN_LATER:
break;
case MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED:
LimeLog.info("Output buffers changed");
break;
case MediaCodec.INFO_OUTPUT_FORMAT_CHANGED:
LimeLog.info("Output format changed");
LimeLog.info("New output Format: " + videoDecoder.getOutputFormat());
break;
default:
break;
}
}
} catch (Exception e) {
handleDecoderException(e, null, 0);
}
}
}
};
rendererThread.setName("Video - Renderer (MediaCodec)");
rendererThread.setPriority(Thread.NORM_PRIORITY + 2);
rendererThread.start();
}
private int dequeueInputBuffer(boolean wait, boolean infiniteWait) {
int index;
long startTime, queueTime;
startTime = System.currentTimeMillis();
index = videoDecoder.dequeueInputBuffer(wait ? (infiniteWait ? -1 : 3000) : 0);
if (index < 0) {
return index;
}
queueTime = System.currentTimeMillis();
if (queueTime - startTime >= 20) {
LimeLog.warning("Queue input buffer ran long: "+(queueTime - startTime)+" ms");
}
return index;
}
private void startLegacyRendererThread()
{
rendererThread = new Thread() {
@SuppressWarnings("deprecation")
@@ -194,6 +232,7 @@ public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer {
BufferInfo info = new BufferInfo();
DecodeUnit du = null;
int inputIndex = -1;
long lastDuDequeueTime = 0;
while (!isInterrupted())
{
// In order to get as much data to the decoder as early as possible,
@@ -201,8 +240,12 @@ public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer {
if (inputIndex == -1 && du == null) {
try {
for (int i = 0; i < 5; i++) {
inputIndex = videoDecoder.dequeueInputBuffer(0);
inputIndex = dequeueInputBuffer(false, false);
du = depacketizer.pollNextDecodeUnit();
if (du != null) {
lastDuDequeueTime = System.currentTimeMillis();
notifyDuReceived(du);
}
// Stop if we can't get a DU or input buffer
if (du == null || inputIndex == -1) {
@@ -216,7 +259,7 @@ public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer {
}
} catch (Exception e) {
inputIndex = -1;
handleDecoderException(MediaCodecDecoderRenderer.this, e, null, 0);
handleDecoderException(e, null, 0);
}
}
@@ -231,21 +274,30 @@ public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer {
// If we've got a DU waiting to be given to the decoder,
// wait a full 3 ms for an input buffer. Otherwise
// just see if we can get one immediately.
inputIndex = videoDecoder.dequeueInputBuffer(du != null ? 3000 : 0);
inputIndex = dequeueInputBuffer(du != null, false);
} catch (Exception e) {
inputIndex = -1;
handleDecoderException(MediaCodecDecoderRenderer.this, e, null, 0);
handleDecoderException(e, null, 0);
}
}
// Grab a decode unit if we don't have one already
if (du == null) {
du = depacketizer.pollNextDecodeUnit();
if (du != null) {
lastDuDequeueTime = System.currentTimeMillis();
notifyDuReceived(du);
}
}
// If we've got both a decode unit and an input buffer, we'll
// submit now. Otherwise, we wait until we have one.
if (du != null && inputIndex >= 0) {
long submissionTime = System.currentTimeMillis();
if (submissionTime - lastDuDequeueTime >= 20) {
LimeLog.warning("Receiving an input buffer took too long: "+(submissionTime - lastDuDequeueTime)+" ms");
}
submitDecodeUnit(du, videoDecoderInputBuffers[inputIndex], inputIndex);
// DU and input buffer have both been consumed
@@ -298,7 +350,7 @@ public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer {
}
}
} catch (Exception e) {
handleDecoderException(MediaCodecDecoderRenderer.this, e, null, 0);
handleDecoderException(e, null, 0);
}
}
}
@@ -316,11 +368,15 @@ public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer {
// Start the decoder
videoDecoder.start();
// On devices pre-Lollipop, we'll use a rendering thread
if (!ENABLE_ASYNC_RENDERER || Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
videoDecoderInputBuffers = videoDecoder.getInputBuffers();
startRendererThread();
if (directSubmit) {
startDirectSubmitRendererThread();
}
else {
startLegacyRendererThread();
}
return true;
}
@@ -357,7 +413,7 @@ public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer {
timestampUs, codecFlags);
break;
} catch (Exception e) {
handleDecoderException(this, e, null, codecFlags);
handleDecoderException(e, null, codecFlags);
lastException = e;
}
}
@@ -369,14 +425,7 @@ public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer {
@SuppressWarnings("deprecation")
private void submitDecodeUnit(DecodeUnit decodeUnit, ByteBuffer buf, int inputBufferIndex) {
long currentTime = System.currentTimeMillis();
long delta = currentTime-decodeUnit.getReceiveTimestamp();
if (delta >= 0 && delta < 1000) {
totalTimeMs += currentTime-decodeUnit.getReceiveTimestamp();
totalFrames++;
}
long timestampUs = currentTime * 1000;
long timestampUs = System.currentTimeMillis() * 1000;
if (timestampUs <= lastTimestampUs) {
// We can't submit multiple buffers with the same timestamp
// so bump it up by one before queuing
@@ -400,7 +449,7 @@ public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer {
boolean needsSpsReplay = false;
if ((decodeUnitFlags & DecodeUnit.DU_FLAG_CODEC_CONFIG) != 0) {
ByteBufferDescriptor header = decodeUnit.getBufferList().get(0);
ByteBufferDescriptor header = decodeUnit.getBufferHead();
if (header.data[header.offset+4] == 0x67) {
numSpsIn++;
@@ -437,19 +486,49 @@ public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer {
LimeLog.info("Patching num_ref_frames in SPS");
sps.num_ref_frames = 1;
// GFE 2.5.11 changed the SPS to add additional extensions
// Some devices don't like these so we remove them here.
sps.vuiParams.video_signal_type_present_flag = false;
sps.vuiParams.colour_description_present_flag = false;
sps.vuiParams.colour_primaries = 2;
sps.vuiParams.transfer_characteristics = 2;
sps.vuiParams.matrix_coefficients = 2;
sps.vuiParams.chroma_loc_info_present_flag = false;
sps.vuiParams.chroma_sample_loc_type_bottom_field = 0;
sps.vuiParams.chroma_sample_loc_type_top_field = 0;
if (needsSpsBitstreamFixup || isExynos4) {
// The SPS that comes in the current H264 bytestream doesn't set bitstream_restriction_flag
// or max_dec_frame_buffering which increases decoding latency on Tegra.
LimeLog.info("Adding bitstream restrictions");
// GFE 2.5.11 started sending bitstream restrictions
if (sps.vuiParams.bitstreamRestriction == null) {
LimeLog.info("Adding bitstream restrictions");
sps.vuiParams.bitstreamRestriction = new VUIParameters.BitstreamRestriction();
sps.vuiParams.bitstreamRestriction.motion_vectors_over_pic_boundaries_flag = true;
sps.vuiParams.bitstreamRestriction.max_bytes_per_pic_denom = 2;
sps.vuiParams.bitstreamRestriction.max_bits_per_mb_denom = 1;
sps.vuiParams.bitstreamRestriction.log2_max_mv_length_horizontal = 16;
sps.vuiParams.bitstreamRestriction.log2_max_mv_length_vertical = 16;
sps.vuiParams.bitstreamRestriction.num_reorder_frames = 0;
sps.vuiParams.bitstreamRestriction.max_dec_frame_buffering = 1;
}
else {
LimeLog.info("Patching bitstream restrictions");
}
// Some devices throw errors if max_dec_frame_buffering < num_ref_frames
sps.vuiParams.bitstreamRestriction.max_dec_frame_buffering = sps.num_ref_frames;
// These values are the defaults for the fields, but they are more aggressive
// than what GFE sends in 2.5.11, but it doesn't seem to cause picture problems.
sps.vuiParams.bitstreamRestriction.max_bytes_per_pic_denom = 2;
sps.vuiParams.bitstreamRestriction.max_bits_per_mb_denom = 1;
// log2_max_mv_length_horizontal and log2_max_mv_length_vertical are set to more
// conservative values by GFE 2.5.11. We'll let those values stand.
}
else {
// Devices that didn't/couldn't get bitstream restrictions before GFE 2.5.11
// will continue to not receive them now
sps.vuiParams.bitstreamRestriction = null;
}
// If we need to hack this SPS to say we're baseline, do so now
@@ -485,8 +564,8 @@ public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer {
}
// Copy data from our buffer list into the input buffer
for (ByteBufferDescriptor desc : decodeUnit.getBufferList())
{
for (ByteBufferDescriptor desc = decodeUnit.getBufferHead();
desc != null; desc = desc.nextDescriptor) {
buf.put(desc.data, desc.offset, desc.length);
}
@@ -502,7 +581,7 @@ public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer {
}
private void replaySps() {
int inputIndex = videoDecoder.dequeueInputBuffer(-1);
int inputIndex = dequeueInputBuffer(true, true);
ByteBuffer inputBuffer = videoDecoderInputBuffers[inputIndex];
inputBuffer.clear();
@@ -530,8 +609,15 @@ public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer {
@Override
public int getCapabilities() {
return adaptivePlayback ?
int caps = 0;
caps |= adaptivePlayback ?
VideoDecoderRenderer.CAPABILITY_ADAPTIVE_RESOLUTION : 0;
caps |= directSubmit ?
VideoDecoderRenderer.CAPABILITY_DIRECT_SUBMIT : 0;
return caps;
}
@Override
@@ -555,11 +641,40 @@ public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer {
return decoderName;
}
private void notifyDuReceived(DecodeUnit du) {
long currentTime = System.currentTimeMillis();
long delta = currentTime-du.getReceiveTimestamp();
if (delta >= 0 && delta < 1000) {
totalTimeMs += currentTime-du.getReceiveTimestamp();
totalFrames++;
}
}
@Override
public void directSubmitDecodeUnit(DecodeUnit du) {
int inputIndex;
notifyDuReceived(du);
for (;;) {
try {
inputIndex = dequeueInputBuffer(true, true);
break;
} catch (Exception e) {
handleDecoderException(e, null, 0);
}
}
if (inputIndex >= 0) {
submitDecodeUnit(du, videoDecoderInputBuffers[inputIndex], inputIndex);
}
}
public class RendererException extends RuntimeException {
private static final long serialVersionUID = 8985937536997012406L;
private Exception originalException;
private MediaCodecDecoderRenderer renderer;
private final Exception originalException;
private final MediaCodecDecoderRenderer renderer;
private ByteBuffer currentBuffer;
private int currentCodecFlags;
@@ -20,12 +20,27 @@ import com.limelight.LimeLog;
public class MediaCodecHelper {
public static final List<String> preferredDecoders;
private static final List<String> preferredDecoders;
public static final List<String> blacklistedDecoderPrefixes;
public static final List<String> spsFixupBitstreamFixupDecoderPrefixes;
public static final List<String> whitelistedAdaptiveResolutionPrefixes;
public static final List<String> baselineProfileHackPrefixes;
private static final List<String> blacklistedDecoderPrefixes;
private static final List<String> spsFixupBitstreamFixupDecoderPrefixes;
private static final List<String> whitelistedAdaptiveResolutionPrefixes;
private static final List<String> baselineProfileHackPrefixes;
private static final List<String> directSubmitPrefixes;
static {
directSubmitPrefixes = new LinkedList<String>();
// These decoders have low enough input buffer latency that they
// can be directly invoked from the receive thread
directSubmitPrefixes.add("omx.qcom");
directSubmitPrefixes.add("omx.sec");
directSubmitPrefixes.add("omx.exynos");
directSubmitPrefixes.add("omx.intel");
directSubmitPrefixes.add("omx.brcm");
directSubmitPrefixes.add("omx.TI");
directSubmitPrefixes.add("omx.arc");
}
static {
preferredDecoders = new LinkedList<String>();
@@ -98,6 +113,10 @@ public class MediaCodecHelper {
return false;
}
public static boolean decoderCanDirectSubmit(String decoderName, MediaCodecInfo decoderInfo) {
return isDecoderInList(directSubmitPrefixes, decoderName) && !isExynos4Device();
}
public static boolean decoderNeedsSpsBitstreamRestrictions(String decoderName, MediaCodecInfo decoderInfo) {
return isDecoderInList(spsFixupBitstreamFixupDecoderPrefixes, decoderName);
}
@@ -146,7 +165,7 @@ public class MediaCodecHelper {
return str;
}
public static MediaCodecInfo findPreferredDecoder() {
private static MediaCodecInfo findPreferredDecoder() {
// This is a different algorithm than the other findXXXDecoder functions,
// because we want to evaluate the decoders in our list's order
// rather than MediaCodecList's order
@@ -217,7 +236,7 @@ public class MediaCodecHelper {
// since some bad decoders can throw IllegalArgumentExceptions unexpectedly
// and we want to be sure all callers are handling this possibility
@SuppressWarnings("RedundantThrows")
public static MediaCodecInfo findKnownSafeDecoder() throws Exception {
private static MediaCodecInfo findKnownSafeDecoder() throws Exception {
for (MediaCodecInfo codecInfo : getMediaCodecList()) {
// Skip encoders
if (codecInfo.isEncoder()) {
@@ -1,10 +1,11 @@
package com.limelight.computers;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.StringReader;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.util.LinkedList;
import java.util.List;
import java.util.UUID;
@@ -30,18 +31,21 @@ import android.os.IBinder;
import org.xmlpull.v1.XmlPullParserException;
public class ComputerManagerService extends Service {
private static final int POLLING_PERIOD_MS = 3000;
private static final int SERVERINFO_POLLING_PERIOD_MS = 3000;
private static final int APPLIST_POLLING_PERIOD_MS = 30000;
private static final int MDNS_QUERY_PERIOD_MS = 1000;
private static final int FAST_POLL_TIMEOUT = 500;
private static final int OFFLINE_POLL_TRIES = 3;
private ComputerManagerBinder binder = new ComputerManagerBinder();
private final ComputerManagerBinder binder = new ComputerManagerBinder();
private ComputerDatabaseManager dbManager;
private AtomicInteger dbRefCount = new AtomicInteger(0);
private final AtomicInteger dbRefCount = new AtomicInteger(0);
private IdentityManager idManager;
private final LinkedList<PollingTuple> pollingTuples = new LinkedList<PollingTuple>();
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;
@@ -65,10 +69,7 @@ public class ComputerManagerService extends Service {
};
// Returns true if the details object was modified
private boolean runPoll(ComputerDetails details)
{
boolean newPc = details.name.isEmpty();
private boolean runPoll(ComputerDetails details, boolean newPc, int offlineCount) throws InterruptedException {
if (!getLocalDatabaseReference()) {
return false;
}
@@ -76,12 +77,23 @@ public class ComputerManagerService extends Service {
activePolls.incrementAndGet();
// Poll the machine
if (!doPollMachine(details)) {
try {
if (!pollComputer(details)) {
if (!newPc && offlineCount < OFFLINE_POLL_TRIES) {
// Return without calling the listener
releaseLocalDatabaseReference();
return false;
}
details.state = ComputerDetails.State.OFFLINE;
details.reachability = ComputerDetails.Reachability.OFFLINE;
}
} catch (InterruptedException e) {
releaseLocalDatabaseReference();
throw e;
} finally {
activePolls.decrementAndGet();
}
// If it's online, update our persistent state
if (details.state == ComputerDetails.State.ONLINE) {
@@ -91,7 +103,7 @@ public class ComputerManagerService extends Service {
if (dbManager.getComputerByName(details.name) == null) {
// It's gone
releaseLocalDatabaseReference();
return true;
return false;
}
}
@@ -111,13 +123,21 @@ public class ComputerManagerService extends Service {
Thread t = new Thread() {
@Override
public void run() {
int offlineCount = 0;
while (!isInterrupted() && pollingActive) {
try {
// Check if this poll has modified the details
runPoll(details);
if (!runPoll(details, false, offlineCount)) {
LimeLog.warning(details.name + " is offline (try " + offlineCount + ")");
offlineCount++;
}
else {
offlineCount = 0;
}
// Wait until the next polling interval
try {
Thread.sleep(POLLING_PERIOD_MS);
Thread.sleep(SERVERINFO_POLLING_PERIOD_MS / ((offlineCount > 0) ? 2 : 1));
} catch (InterruptedException e) {
break;
}
@@ -177,10 +197,6 @@ public class ComputerManagerService extends Service {
return ComputerManagerService.this.addComputerBlocking(addr);
}
public void addComputer(InetAddress addr) {
ComputerManagerService.this.addComputer(addr);
}
public void removeComputer(String name) {
ComputerManagerService.this.removeComputer(name);
}
@@ -239,7 +255,7 @@ public class ComputerManagerService extends Service {
@Override
public void notifyComputerAdded(MdnsComputer computer) {
// Kick off a serverinfo poll on this machine
addComputer(computer.getAddress());
addComputerBlocking(computer.getAddress());
}
@Override
@@ -255,28 +271,11 @@ public class ComputerManagerService extends Service {
};
}
public void addComputer(InetAddress addr) {
// Setup a placeholder
ComputerDetails fakeDetails = new ComputerDetails();
fakeDetails.localIp = addr;
fakeDetails.remoteIp = addr;
fakeDetails.name = "";
addTuple(fakeDetails);
}
private void addTuple(ComputerDetails details) {
synchronized (pollingTuples) {
for (PollingTuple tuple : pollingTuples) {
// Check if this is the same computer
if (tuple.computer == details ||
// If there's no name on one of these computers, compare with the local IP
((details.name.isEmpty() || tuple.computer.name.isEmpty()) &&
tuple.computer.localIp.equals(details.localIp)) ||
// If there is a name on both computers, compare with name
((!details.name.isEmpty() && !tuple.computer.name.isEmpty()) &&
tuple.computer.name.equals(details.name))) {
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;
@@ -307,13 +306,18 @@ public class ComputerManagerService extends Service {
ComputerDetails fakeDetails = new ComputerDetails();
fakeDetails.localIp = addr;
fakeDetails.remoteIp = addr;
fakeDetails.name = "";
// Block while we try to fill the details
runPoll(fakeDetails);
try {
runPoll(fakeDetails, true, 0);
} catch (InterruptedException e) {
return false;
}
// If the machine is reachable, it was successful
if (fakeDetails.state == ComputerDetails.State.ONLINE) {
LimeLog.info("New PC ("+fakeDetails.name+") is UUID "+fakeDetails.uuid);
// Start a polling thread for this machine
addTuple(fakeDetails);
return true;
@@ -384,14 +388,91 @@ public class ComputerManagerService extends Service {
}
}
private boolean pollComputer(ComputerDetails details, boolean localFirst) {
// Just try to establish a TCP connection to speculatively detect a running
// GFE server
private boolean fastPollIp(InetAddress addr) {
Socket s = new Socket();
try {
s.connect(new InetSocketAddress(addr, NvHTTP.HTTPS_PORT), FAST_POLL_TIMEOUT);
s.close();
return true;
} catch (IOException e) {
return false;
}
}
private void startFastPollThread(final InetAddress addr, final boolean[] info) {
Thread t = new Thread() {
@Override
public void run() {
boolean pollRes = fastPollIp(addr);
synchronized (info) {
info[0] = true; // Done
info[1] = pollRes; // Polling result
info.notify();
}
}
};
t.setName("Fast Poll - "+addr.getHostAddress());
t.start();
}
private ComputerDetails.Reachability fastPollPc(final InetAddress local, final InetAddress remote) throws InterruptedException {
final boolean[] remoteInfo = new boolean[2];
final boolean[] localInfo = new boolean[2];
startFastPollThread(local, localInfo);
startFastPollThread(remote, remoteInfo);
// Check local first
synchronized (localInfo) {
while (!localInfo[0]) {
localInfo.wait(500);
}
if (localInfo[1]) {
return ComputerDetails.Reachability.LOCAL;
}
}
// Now remote
synchronized (remoteInfo) {
while (!remoteInfo[0]) {
remoteInfo.wait(500);
}
if (remoteInfo[1]) {
return ComputerDetails.Reachability.REMOTE;
}
}
return ComputerDetails.Reachability.OFFLINE;
}
private 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)) {
localFirst = false;
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);
@@ -425,24 +506,19 @@ public class ComputerManagerService extends Service {
return false;
}
// Save the old MAC address
String savedMacAddress = details.macAddress;
// If we got here, it's reachable
details.update(polledDetails);
return true;
// 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;
}
private boolean doPollMachine(ComputerDetails details) {
if (details.reachability == ComputerDetails.Reachability.UNKNOWN ||
details.reachability == ComputerDetails.Reachability.OFFLINE) {
// Always try local first to avoid potential UDP issues when
// attempting to stream via the router's external IP address
// behind its NAT
return pollComputer(details, true);
}
else {
// If we're already reached a machine via a particular IP address,
// always try that one first
return pollComputer(details, details.reachability == ComputerDetails.Reachability.LOCAL);
}
return true;
}
@Override
@@ -491,8 +567,8 @@ public class ComputerManagerService extends Service {
public class ApplistPoller {
private Thread thread;
private ComputerDetails computer;
private Object pollEvent = new Object();
private final ComputerDetails computer;
private final Object pollEvent = new Object();
public ApplistPoller(ComputerDetails computer) {
this.computer = computer;
@@ -507,7 +583,7 @@ public class ComputerManagerService extends Service {
private boolean waitPollingDelay() {
try {
synchronized (pollEvent) {
pollEvent.wait(POLLING_PERIOD_MS);
pollEvent.wait(APPLIST_POLLING_PERIOD_MS);
}
} catch (InterruptedException e) {
return false;
@@ -552,9 +628,19 @@ public class ComputerManagerService extends Service {
List<NvApp> list = NvHTTP.getAppListByReader(new StringReader(appList));
if (appList != null && !appList.isEmpty() && !list.isEmpty()) {
// Open the cache file
FileOutputStream cacheOut = CacheHelper.openCacheFileForOutput(getCacheDir(), "applist", computer.uuid.toString());
OutputStream cacheOut = null;
try {
cacheOut = CacheHelper.openCacheFileForOutput(getCacheDir(), "applist", computer.uuid.toString());
CacheHelper.writeStringToOutputStream(cacheOut, appList);
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (cacheOut != null) {
cacheOut.close();
}
} catch (IOException ignored) {}
}
// Update the computer
computer.rawAppList = appList;
@@ -593,7 +679,7 @@ public class ComputerManagerService extends Service {
class PollingTuple {
public Thread thread;
public ComputerDetails computer;
public final ComputerDetails computer;
public PollingTuple(ComputerDetails computer, Thread thread) {
this.computer = computer;
@@ -70,7 +70,7 @@ public class DiscoveryService extends Service {
});
}
private DiscoveryBinder binder = new DiscoveryBinder();
private final DiscoveryBinder binder = new DiscoveryBinder();
@Override
public IBinder onBind(Intent intent) {
@@ -1,118 +1,69 @@
package com.limelight.grid;
import android.content.Context;
import android.graphics.Bitmap;
import android.app.Activity;
import android.graphics.BitmapFactory;
import android.os.AsyncTask;
import android.view.View;
import android.widget.ImageView;
import android.widget.TextView;
import com.koushikdutta.async.future.FutureCallback;
import com.koushikdutta.ion.ImageViewBitmapInfo;
import com.koushikdutta.ion.Ion;
import com.limelight.AppView;
import com.limelight.LimeLog;
import com.limelight.R;
import com.limelight.binding.PlatformBinding;
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 com.limelight.nvstream.http.LimelightCryptoProvider;
import com.limelight.utils.CacheHelper;
import java.io.BufferedInputStream;
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.net.InetAddress;
import java.net.Socket;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import java.security.Principal;
import java.security.PrivateKey;
import java.security.SecureRandom;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.Future;
import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.KeyManager;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSession;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509KeyManager;
import javax.net.ssl.X509TrustManager;
import java.security.cert.X509Certificate;
@SuppressWarnings("unchecked")
public class AppGridAdapter extends GenericGridAdapter<AppView.AppObject> {
private static final int ART_WIDTH_PX = 300;
private static final int SMALL_WIDTH_DP = 100;
private static final int LARGE_WIDTH_DP = 150;
private ComputerDetails computer;
private String uniqueId;
private LimelightCryptoProvider cryptoProvider;
private SSLContext sslContext;
private final HashMap<ImageView, Future> pendingRequests = new HashMap<ImageView, Future>();
private final CachedAppAssetLoader loader;
public AppGridAdapter(Context context, boolean listMode, boolean small, ComputerDetails computer, String uniqueId) throws NoSuchAlgorithmException, KeyManagementException {
super(context, listMode ? R.layout.simple_row : (small ? R.layout.app_grid_item_small : R.layout.app_grid_item), R.drawable.image_loading);
public AppGridAdapter(Activity activity, boolean listMode, boolean small, ComputerDetails computer, String uniqueId) {
super(activity, listMode ? R.layout.simple_row : (small ? R.layout.app_grid_item_small : R.layout.app_grid_item), R.drawable.image_loading);
this.computer = computer;
this.uniqueId = uniqueId;
int dpi = activity.getResources().getDisplayMetrics().densityDpi;
int dp;
cryptoProvider = PlatformBinding.getCryptoProvider(context);
sslContext = SSLContext.getInstance("SSL");
sslContext.init(ourKeyman, trustAllCerts, new SecureRandom());
if (small) {
dp = SMALL_WIDTH_DP;
}
else {
dp = LARGE_WIDTH_DP;
}
TrustManager[] trustAllCerts = new TrustManager[] {
new X509TrustManager() {
public X509Certificate[] getAcceptedIssuers() {
return new X509Certificate[0];
double scalingDivisor = ART_WIDTH_PX / (dp * (dpi / 160.0));
if (scalingDivisor < 1.0) {
// We don't want to make them bigger before draw-time
scalingDivisor = 1.0;
}
public void checkClientTrusted(X509Certificate[] certs, String authType) {}
public void checkServerTrusted(X509Certificate[] certs, String authType) {}
}};
LimeLog.info("Art scaling divisor: " + scalingDivisor);
KeyManager[] ourKeyman = new KeyManager[] {
new X509KeyManager() {
public String chooseClientAlias(String[] keyTypes,
Principal[] issuers, Socket socket) {
return "Limelight-RSA";
BitmapFactory.Options options = new BitmapFactory.Options();
options.inSampleSize = (int) scalingDivisor;
this.loader = new CachedAppAssetLoader(computer, scalingDivisor,
new NetworkAssetLoader(context, uniqueId),
new MemoryAssetLoader(),
new DiskAssetLoader(context.getCacheDir()),
BitmapFactory.decodeResource(activity.getResources(),
R.drawable.image_loading, options));
}
public String chooseServerAlias(String keyType, Principal[] issuers,
Socket socket) {
return null;
public void cancelQueuedOperations() {
loader.cancelForegroundLoads();
loader.cancelBackgroundLoads();
loader.freeCacheMemory();
}
public X509Certificate[] getCertificateChain(String alias) {
return new X509Certificate[] {cryptoProvider.getClientCertificate()};
}
public String[] getClientAliases(String keyType, Principal[] issuers) {
return null;
}
public PrivateKey getPrivateKey(String alias) {
return cryptoProvider.getClientPrivateKey();
}
public String[] getServerAliases(String keyType, Principal[] issuers) {
return null;
}
}
};
// Ignore differences between given hostname and certificate hostname
HostnameVerifier hv = new HostnameVerifier() {
public boolean verify(String hostname, SSLSession session) { return true; }
};
private void sortList() {
Collections.sort(itemList, new Comparator<AppView.AppObject>() {
@Override
@@ -122,62 +73,22 @@ public class AppGridAdapter extends GenericGridAdapter<AppView.AppObject> {
});
}
private InetAddress getCurrentAddress() {
if (computer.reachability == ComputerDetails.Reachability.LOCAL) {
return computer.localIp;
}
else {
return computer.remoteIp;
}
}
public void addApp(AppView.AppObject app) {
// Queue a request to fetch this bitmap into cache
loader.queueCacheLoad(app.app);
// Add the app to our sorted list
itemList.add(app);
sortList();
}
public void abortPendingRequests() {
HashMap<ImageView, Future> tempMap;
synchronized (pendingRequests) {
// Copy the pending requests under a lock
tempMap = new HashMap<ImageView, Future>(pendingRequests);
public void removeApp(AppView.AppObject app) {
itemList.remove(app);
}
for (Future f : tempMap.values()) {
if (!f.isCancelled() && !f.isDone()) {
f.cancel(true);
}
}
synchronized (pendingRequests) {
// Remove cancelled requests
for (ImageView v : tempMap.keySet()) {
pendingRequests.remove(v);
}
}
}
// TODO: Handle pruning of bitmap cache
private void populateBitmapCache(UUID uuid, int appId, Bitmap bitmap) {
try {
// PNG ignores quality setting
FileOutputStream out = CacheHelper.openCacheFileForOutput(context.getCacheDir(), "boxart", uuid.toString(), appId+".png");
bitmap.compress(Bitmap.CompressFormat.PNG, 0, out);
out.close();
} catch (IOException e) {
e.printStackTrace();
}
}
@Override
public boolean populateImageView(final ImageView imgView, final AppView.AppObject obj) {
// Hide the image view while we're loading the image from disk cache
imgView.setVisibility(View.INVISIBLE);
// Check the on-disk cache
new ImageCacheRequest(imgView, obj.app.getAppId()).execute();
public boolean populateImageView(ImageView imgView, AppView.AppObject obj) {
// Let the cached asset loader handle it
loader.populateImageView(obj.app, imgView);
return true;
}
@@ -201,84 +112,4 @@ public class AppGridAdapter extends GenericGridAdapter<AppView.AppObject> {
// No overlay
return false;
}
private class ImageCacheRequest extends AsyncTask<Void, Void, Bitmap> {
private ImageView view;
private int appId;
public ImageCacheRequest(ImageView view, int appId) {
this.view = view;
this.appId = appId;
}
@Override
protected Bitmap doInBackground(Void... v) {
InputStream in = null;
try {
in = CacheHelper.openCacheFileForInput(context.getCacheDir(), "boxart", computer.uuid.toString(), appId + ".png");
return BitmapFactory.decodeStream(in);
} catch (IOException e) {
e.printStackTrace();
} finally {
if (in != null) {
try {
in.close();
} catch (IOException e) {}
}
}
return null;
}
@Override
protected void onPostExecute(Bitmap result) {
if (result != null) {
// Disk cache was read successfully
LimeLog.info("Image disk cache hit for ("+computer.uuid+", "+appId+")");
view.setImageBitmap(result);
view.setVisibility(View.VISIBLE);
}
else {
LimeLog.info("Image disk cache miss for ("+computer.uuid+", "+appId+")");
LimeLog.info("Requesting: "+"https://" + getCurrentAddress().getHostAddress() + ":47984/appasset?uniqueid=" + uniqueId + "&appid=" +
appId + "&AssetType=2&AssetIdx=0");
// Load the placeholder image
view.setImageResource(defaultImageRes);
view.setVisibility(View.VISIBLE);
// Set SSL contexts correctly to allow us to authenticate
Ion.getDefault(context).getHttpClient().getSSLSocketMiddleware().setTrustManagers(trustAllCerts);
Ion.getDefault(context).getHttpClient().getSSLSocketMiddleware().setSSLContext(sslContext);
// Kick off the deferred image load
synchronized (pendingRequests) {
Future<Bitmap> f = Ion.with(context)
.load("https://" + getCurrentAddress().getHostAddress() + ":47984/appasset?uniqueid=" + uniqueId + "&appid=" +
appId + "&AssetType=2&AssetIdx=0")
.asBitmap()
.setCallback(new FutureCallback<Bitmap>() {
@Override
public void onCompleted(Exception e, Bitmap result) {
synchronized (pendingRequests) {
pendingRequests.remove(view);
}
if (result != null) {
// Make the view visible now
view.setImageBitmap(result);
view.setVisibility(View.VISIBLE);
// Populate the disk cache if we got an image back
populateBitmapCache(computer.uuid, appId, result);
}
else {
// Leave the loading icon as is (probably should change this eventually...)
}
}
});
pendingRequests.put(view, f);
}
}
}
};
}
@@ -13,11 +13,11 @@ import com.limelight.R;
import java.util.ArrayList;
public abstract class GenericGridAdapter<T> extends BaseAdapter {
protected Context context;
protected int defaultImageRes;
protected int layoutId;
protected ArrayList<T> itemList = new ArrayList<T>();
protected LayoutInflater inflater;
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;
@@ -0,0 +1,333 @@
package com.limelight.grid.assets;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.os.AsyncTask;
import android.widget.ImageView;
import com.limelight.nvstream.http.ComputerDetails;
import com.limelight.nvstream.http.NvApp;
import java.io.IOException;
import java.io.InputStream;
import java.lang.ref.WeakReference;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
public class CachedAppAssetLoader {
private static final int MAX_CONCURRENT_DISK_LOADS = 3;
private static final int MAX_CONCURRENT_NETWORK_LOADS = 3;
private static final int MAX_CONCURRENT_CACHE_LOADS = 1;
private static final int MAX_PENDING_CACHE_LOADS = 100;
private static final int MAX_PENDING_NETWORK_LOADS = 40;
private static final int MAX_PENDING_DISK_LOADS = 40;
private final ThreadPoolExecutor cacheExecutor = new ThreadPoolExecutor(
MAX_CONCURRENT_CACHE_LOADS, MAX_CONCURRENT_CACHE_LOADS,
Long.MAX_VALUE, TimeUnit.DAYS,
new LinkedBlockingQueue<Runnable>(MAX_PENDING_CACHE_LOADS),
new ThreadPoolExecutor.DiscardOldestPolicy());
private final ThreadPoolExecutor foregroundExecutor = new ThreadPoolExecutor(
MAX_CONCURRENT_DISK_LOADS, MAX_CONCURRENT_DISK_LOADS,
Long.MAX_VALUE, TimeUnit.DAYS,
new LinkedBlockingQueue<Runnable>(MAX_PENDING_DISK_LOADS),
new ThreadPoolExecutor.DiscardOldestPolicy());
private final ThreadPoolExecutor networkExecutor = new ThreadPoolExecutor(
MAX_CONCURRENT_NETWORK_LOADS, MAX_CONCURRENT_NETWORK_LOADS,
Long.MAX_VALUE, TimeUnit.DAYS,
new LinkedBlockingQueue<Runnable>(MAX_PENDING_NETWORK_LOADS),
new ThreadPoolExecutor.DiscardOldestPolicy());
private final ComputerDetails computer;
private final double scalingDivider;
private final NetworkAssetLoader networkLoader;
private final MemoryAssetLoader memoryLoader;
private final DiskAssetLoader diskLoader;
private final Bitmap placeholderBitmap;
public CachedAppAssetLoader(ComputerDetails computer, double scalingDivider,
NetworkAssetLoader networkLoader, MemoryAssetLoader memoryLoader,
DiskAssetLoader diskLoader, Bitmap placeholderBitmap) {
this.computer = computer;
this.scalingDivider = scalingDivider;
this.networkLoader = networkLoader;
this.memoryLoader = memoryLoader;
this.diskLoader = diskLoader;
this.placeholderBitmap = placeholderBitmap;
}
public void cancelBackgroundLoads() {
Runnable r;
while ((r = cacheExecutor.getQueue().poll()) != null) {
cacheExecutor.remove(r);
}
}
public void cancelForegroundLoads() {
Runnable r;
while ((r = foregroundExecutor.getQueue().poll()) != null) {
foregroundExecutor.remove(r);
}
while ((r = networkExecutor.getQueue().poll()) != null) {
networkExecutor.remove(r);
}
}
public void freeCacheMemory() {
memoryLoader.clearCache();
}
private Bitmap doNetworkAssetLoad(LoaderTuple tuple, LoaderTask task) {
// Try 3 times
for (int i = 0; i < 3; i++) {
// Check again whether we've been cancelled or the image view is gone
if (task != null && (task.isCancelled() || task.imageViewRef.get() == null)) {
return null;
}
InputStream in = networkLoader.getBitmapStream(tuple);
if (in != null) {
// Write the stream straight to disk
diskLoader.populateCacheWithStream(tuple, in);
// Close the network input stream
try {
in.close();
} catch (IOException ignored) {}
// If there's a task associated with this load, we should return the bitmap
if (task != null) {
return diskLoader.loadBitmapFromCache(tuple, (int) scalingDivider);
}
else {
// Otherwise it's a background load and we return nothing
return null;
}
}
// Wait 1 second with a bit of fuzz
try {
Thread.sleep((int) (1000 + (Math.random() * 500)));
} catch (InterruptedException e) {
return null;
}
}
return null;
}
private class LoaderTask extends AsyncTask<LoaderTuple, Void, Bitmap> {
private final WeakReference<ImageView> imageViewRef;
private final boolean diskOnly;
private LoaderTuple tuple;
public LoaderTask(ImageView imageView, boolean diskOnly) {
this.imageViewRef = new WeakReference<ImageView>(imageView);
this.diskOnly = diskOnly;
}
@Override
protected Bitmap doInBackground(LoaderTuple... params) {
tuple = params[0];
// Check whether it has been cancelled or the image view is gone
if (isCancelled() || imageViewRef.get() == null) {
return null;
}
Bitmap bmp = diskLoader.loadBitmapFromCache(tuple, (int) scalingDivider);
if (bmp == null) {
if (!diskOnly) {
// Try to load the asset from the network
bmp = doNetworkAssetLoad(tuple, this);
} else {
// Report progress to display the placeholder and spin
// off the network-capable task
publishProgress();
}
}
// Cache the bitmap
if (bmp != null) {
memoryLoader.populateCache(tuple, bmp);
}
return bmp;
}
@Override
protected void onProgressUpdate(Void... nothing) {
// Do nothing if cancelled
if (isCancelled()) {
return;
}
// If the current loader task for this view isn't us, do nothing
final ImageView imageView = imageViewRef.get();
if (getLoaderTask(imageView) == this) {
// Set off another loader task on the network executor
LoaderTask task = new LoaderTask(imageView, false);
AsyncDrawable asyncDrawable = new AsyncDrawable(imageView.getResources(), placeholderBitmap, task);
imageView.setAlpha(1.0f);
imageView.setImageDrawable(asyncDrawable);
task.executeOnExecutor(networkExecutor, tuple);
}
}
@Override
protected void onPostExecute(Bitmap bitmap) {
// Do nothing if cancelled
if (isCancelled()) {
return;
}
final ImageView imageView = imageViewRef.get();
if (getLoaderTask(imageView) == this) {
// Set the bitmap
if (bitmap != null) {
imageView.setImageBitmap(bitmap);
}
// Show the view
imageView.setAlpha(1.0f);
}
}
}
static class AsyncDrawable extends BitmapDrawable {
private final WeakReference<LoaderTask> loaderTaskReference;
public AsyncDrawable(Resources res, Bitmap bitmap,
LoaderTask loaderTask) {
super(res, bitmap);
loaderTaskReference = new WeakReference<LoaderTask>(loaderTask);
}
public LoaderTask getLoaderTask() {
return loaderTaskReference.get();
}
}
private static LoaderTask getLoaderTask(ImageView imageView) {
if (imageView == null) {
return null;
}
final Drawable drawable = imageView.getDrawable();
// If our drawable is in play, get the loader task
if (drawable instanceof AsyncDrawable) {
final AsyncDrawable asyncDrawable = (AsyncDrawable) drawable;
return asyncDrawable.getLoaderTask();
}
return null;
}
private static boolean cancelPendingLoad(LoaderTuple tuple, ImageView imageView) {
final LoaderTask loaderTask = getLoaderTask(imageView);
// Check if any task was pending for this image view
if (loaderTask != null && !loaderTask.isCancelled()) {
final LoaderTuple taskTuple = loaderTask.tuple;
// Cancel the task if it's not already loading the same data
if (taskTuple == null || !taskTuple.equals(tuple)) {
loaderTask.cancel(true);
} else {
// It's already loading what we want
return false;
}
}
// Allow the load to proceed
return true;
}
public void queueCacheLoad(NvApp app) {
final LoaderTuple tuple = new LoaderTuple(computer, app);
if (memoryLoader.loadBitmapFromCache(tuple) != null) {
// It's in memory which means it must also be on disk
return;
}
// Queue a fetch in the cache executor
cacheExecutor.execute(new Runnable() {
@Override
public void run() {
// Check if the image is cached on disk
if (diskLoader.checkCacheExists(tuple)) {
return;
}
// Try to load the asset from the network and cache result on disk
doNetworkAssetLoad(tuple, null);
}
});
}
public void populateImageView(NvApp app, ImageView view) {
LoaderTuple tuple = new LoaderTuple(computer, app);
// If there's already a task in progress for this view,
// cancel it. If the task is already loading the same image,
// we return and let that load finish.
if (!cancelPendingLoad(tuple, view)) {
return;
}
// First, try the memory cache in the current context
Bitmap bmp = memoryLoader.loadBitmapFromCache(tuple);
if (bmp != null) {
// Show the bitmap immediately
view.setAlpha(1.0f);
view.setImageBitmap(bmp);
return;
}
// If it's not in memory, create an async task to load it. This task will be attached
// via AsyncDrawable to this view.
final LoaderTask task = new LoaderTask(view, true);
final AsyncDrawable asyncDrawable = new AsyncDrawable(view.getResources(), placeholderBitmap, task);
view.setAlpha(0.0f);
view.setImageDrawable(asyncDrawable);
// Run the task on our foreground executor
task.executeOnExecutor(foregroundExecutor, tuple);
}
public class LoaderTuple {
public final ComputerDetails computer;
public final NvApp app;
public LoaderTuple(ComputerDetails computer, NvApp app) {
this.computer = computer;
this.app = app;
}
@Override
public boolean equals(Object o) {
if (!(o instanceof LoaderTuple)) {
return false;
}
LoaderTuple other = (LoaderTuple) o;
return computer.uuid.equals(other.computer.uuid) && app.getAppId() == other.app.getAppId();
}
@Override
public String toString() {
return "("+computer.uuid+", "+app.getAppId()+")";
}
}
}
@@ -0,0 +1,67 @@
package com.limelight.grid.assets;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import com.limelight.LimeLog;
import com.limelight.utils.CacheHelper;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
public class DiskAssetLoader {
private final File cacheDir;
public DiskAssetLoader(File cacheDir) {
this.cacheDir = cacheDir;
}
public boolean checkCacheExists(CachedAppAssetLoader.LoaderTuple tuple) {
return CacheHelper.cacheFileExists(cacheDir, "boxart", tuple.computer.uuid.toString(), tuple.app.getAppId() + ".png");
}
public Bitmap loadBitmapFromCache(CachedAppAssetLoader.LoaderTuple tuple, int sampleSize) {
InputStream in = null;
Bitmap bmp = null;
try {
in = CacheHelper.openCacheFileForInput(cacheDir, "boxart", tuple.computer.uuid.toString(), tuple.app.getAppId() + ".png");
BitmapFactory.Options options = new BitmapFactory.Options();
options.inSampleSize = sampleSize;
bmp = BitmapFactory.decodeStream(in, null, options);
} catch (FileNotFoundException ignored) {
} catch (IOException e) {
e.printStackTrace();
} finally {
if (in != null) {
try {
in.close();
} catch (IOException ignored) {}
}
}
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 / 16) {
@Override
protected int sizeOf(String key, Bitmap bitmap) {
// Sizeof returns kilobytes
return bitmap.getByteCount() / 1024;
}
};
private static String constructKey(CachedAppAssetLoader.LoaderTuple tuple) {
return tuple.computer.uuid.toString()+"-"+tuple.app.getAppId();
}
public Bitmap loadBitmapFromCache(CachedAppAssetLoader.LoaderTuple tuple) {
Bitmap bmp = memoryCache.get(constructKey(tuple));
if (bmp != null) {
LimeLog.info("Memory cache hit for tuple: "+tuple);
}
return bmp;
}
public void populateCache(CachedAppAssetLoader.LoaderTuple tuple, Bitmap bitmap) {
memoryCache.put(constructKey(tuple), bitmap);
}
public void clearCache() {
memoryCache.evictAll();
}
}
@@ -0,0 +1,49 @@
package com.limelight.grid.assets;
import android.content.Context;
import com.limelight.LimeLog;
import com.limelight.binding.PlatformBinding;
import com.limelight.nvstream.http.ComputerDetails;
import com.limelight.nvstream.http.NvHTTP;
import java.io.IOException;
import java.io.InputStream;
import java.net.InetAddress;
public class NetworkAssetLoader {
private final Context context;
private final String uniqueId;
public NetworkAssetLoader(Context context, String uniqueId) {
this.context = context;
this.uniqueId = uniqueId;
}
public InputStream getBitmapStream(CachedAppAssetLoader.LoaderTuple tuple) {
NvHTTP http = new NvHTTP(getCurrentAddress(tuple.computer), uniqueId, null, PlatformBinding.getCryptoProvider(context));
InputStream in = null;
try {
in = http.getBoxArt(tuple.app);
} catch (IOException ignored) {}
if (in != null) {
LimeLog.info("Network asset load complete: " + tuple);
}
else {
LimeLog.info("Network asset load failed: " + tuple);
}
return in;
}
private static InetAddress getCurrentAddress(ComputerDetails computer) {
if (computer.reachability == ComputerDetails.Reachability.LOCAL) {
return computer.localIp;
}
else {
return computer.remoteIp;
}
}
}
@@ -14,6 +14,7 @@ import com.limelight.utils.UiHelper;
import android.app.Activity;
import android.app.Service;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.content.res.Configuration;
@@ -21,15 +22,16 @@ import android.os.Bundle;
import android.os.IBinder;
import android.view.KeyEvent;
import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputMethodManager;
import android.widget.TextView;
import android.widget.Toast;
public class AddComputerManually extends Activity {
private TextView hostText;
private ComputerManagerService.ComputerManagerBinder managerBinder;
private LinkedBlockingQueue<String> computersToAdd = new LinkedBlockingQueue<String>();
private final LinkedBlockingQueue<String> computersToAdd = new LinkedBlockingQueue<String>();
private Thread addThread;
private ServiceConnection serviceConnection = new ServiceConnection() {
private final ServiceConnection serviceConnection = new ServiceConnection() {
public void onServiceConnected(ComponentName className, final IBinder binder) {
managerBinder = ((ComputerManagerService.ComputerManagerBinder)binder);
startAddThread();
@@ -161,6 +163,12 @@ public class AddComputerManually extends Activity {
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;
}
@@ -2,12 +2,12 @@ package com.limelight.preferences;
import android.content.Context;
import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.os.Build;
import android.preference.PreferenceManager;
public class PreferenceConfiguration {
static final String RESX_PREF_STRING = "resolution_x";
static final String RESY_PREF_STRING = "resolution_y";
private static final String FPS_PREF_STRING = "fps";
static final String RES_FPS_PREF_STRING = "list_resolution_fps";
private static final String DECODER_PREF_STRING = "list_decoders";
static final String BITRATE_PREF_STRING = "seekbar_bitrate";
private static final String STRETCH_PREF_STRING = "checkbox_stretch_video";
@@ -18,17 +18,16 @@ public class PreferenceConfiguration {
private static final String LANGUAGE_PREF_STRING = "list_languages";
private static final String LIST_MODE_PREF_STRING = "checkbox_list_mode";
private static final String SMALL_ICONS_PREF_STRING = "checkbox_small_icon_mode";
private static final String MULTI_CONTROLLER_PREF_STRING = "checkbox_multi_controller";
private static final int BITRATE_DEFAULT_720_30 = 5;
private static final int BITRATE_DEFAULT_720_60 = 10;
private static final int BITRATE_DEFAULT_1080_30 = 10;
private static final int BITRATE_DEFAULT_1080_60 = 20;
private static final int BITRATE_DEFAULT_MAX = 35;
private static final int DEFAULT_RESX = 1280;
private static final int DEFAULT_RESY = 720;
private static final String DEFAULT_FPS = "60";
private static final String DEFAULT_RES_FPS = "720p60";
private static final String DEFAULT_DECODER = "auto";
private static final int DEFAULT_BITRATE = BITRATE_DEFAULT_720_60;
private static final boolean DEFAULT_STRETCH = false;
private static final boolean DEFAULT_SOPS = true;
private static final boolean DEFAULT_DISABLE_TOASTS = false;
@@ -36,6 +35,7 @@ public class PreferenceConfiguration {
private static final int DEFAULT_DEADZONE = 15;
public static final String DEFAULT_LANGUAGE = "default";
private static final boolean DEFAULT_LIST_MODE = false;
private static final boolean DEFAULT_MULTI_CONTROLLER = true;
public static final int FORCE_HARDWARE_DECODER = -1;
public static final int AUTOSELECT_DECODER = 0;
@@ -47,42 +47,66 @@ public class PreferenceConfiguration {
public int deadzonePercentage;
public boolean stretchVideo, enableSops, playHostAudio, disableWarnings;
public String language;
public boolean listMode, smallIconMode;
public boolean listMode, smallIconMode, multiController;
public static int getDefaultBitrate(String resFpsString) {
if (resFpsString.equals("720p30")) {
return BITRATE_DEFAULT_720_30;
}
else if (resFpsString.equals("720p60")) {
return BITRATE_DEFAULT_720_60;
}
else if (resFpsString.equals("1080p30")) {
return BITRATE_DEFAULT_1080_30;
}
else if (resFpsString.equals("1080p60")) {
return BITRATE_DEFAULT_1080_60;
}
else {
// Should never get here
return DEFAULT_BITRATE;
}
}
public static boolean getDefaultSmallMode(Context context) {
// Use small mode on anything smaller than a 7" tablet
return context.getResources().getConfiguration().smallestScreenWidthDp < 600;
PackageManager manager = context.getPackageManager();
if (manager != null) {
// TVs shouldn't use small mode by default
if (manager.hasSystemFeature(PackageManager.FEATURE_TELEVISION)) {
return false;
}
private static int getFpsInt(SharedPreferences prefs) {
try {
return Integer.parseInt(prefs.getString(FPS_PREF_STRING, DEFAULT_FPS));
} catch (NumberFormatException e) {
return Integer.parseInt(DEFAULT_FPS);
// API 21 uses LEANBACK instead of TELEVISION
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP_MR1) {
if (manager.hasSystemFeature(PackageManager.FEATURE_LEANBACK)) {
return false;
}
}
}
// Use small mode on anything smaller than a 7" tablet
return context.getResources().getConfiguration().smallestScreenWidthDp < 600;
}
public static int getDefaultBitrate(Context context) {
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
long width = prefs.getInt(RESX_PREF_STRING, DEFAULT_RESX);
long height = prefs.getInt(RESY_PREF_STRING, DEFAULT_RESY);
long product = width * height * getFpsInt(prefs);
if (product <= 1280 * 720 * 30) {
String str = prefs.getString(RES_FPS_PREF_STRING, DEFAULT_RES_FPS);
if (str.equals("720p30")) {
return BITRATE_DEFAULT_720_30;
}
else if (product <= 1280 * 720 * 60) {
else if (str.equals("720p60")) {
return BITRATE_DEFAULT_720_60;
}
else if (product <= 1920 * 1080 * 30) {
else if (str.equals("1080p30")) {
return BITRATE_DEFAULT_1080_30;
}
else if (product <= 1920 * 1080 * 60) {
else if (str.equals("1080p60")) {
return BITRATE_DEFAULT_1080_60;
}
else {
return BITRATE_DEFAULT_MAX;
// Should never get here
return DEFAULT_BITRATE;
}
}
@@ -110,9 +134,33 @@ public class PreferenceConfiguration {
PreferenceConfiguration config = new PreferenceConfiguration();
config.bitrate = prefs.getInt(BITRATE_PREF_STRING, getDefaultBitrate(context));
config.width = prefs.getInt(RESX_PREF_STRING, DEFAULT_RESX);
config.height = prefs.getInt(RESY_PREF_STRING, DEFAULT_RESY);
config.fps = getFpsInt(prefs);
String str = prefs.getString(RES_FPS_PREF_STRING, DEFAULT_RES_FPS);
if (str.equals("720p30")) {
config.width = 1280;
config.height = 720;
config.fps = 30;
}
else if (str.equals("720p60")) {
config.width = 1280;
config.height = 720;
config.fps = 60;
}
else if (str.equals("1080p30")) {
config.width = 1920;
config.height = 1080;
config.fps = 30;
}
else if (str.equals("1080p60")) {
config.width = 1920;
config.height = 1080;
config.fps = 60;
}
else {
// Should never get here
config.width = 1280;
config.height = 720;
config.fps = 60;
}
config.decoder = getDecoderValue(context);
@@ -127,6 +175,7 @@ public class PreferenceConfiguration {
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;
}
@@ -1,208 +0,0 @@
package com.limelight.preferences;
import android.content.Context;
import android.content.SharedPreferences;
import android.preference.DialogPreference;
import android.util.AttributeSet;
import android.view.View;
import android.widget.CompoundButton;
import android.widget.RadioButton;
import android.widget.SeekBar;
import android.widget.TextView;
import com.limelight.R;
public class ResolutionPreference extends DialogPreference {
private RadioButton a4_3;
private RadioButton a16_9;
private RadioButton a16_10;
private SeekBar resolutionSeekBar;
private TextView resolutionText;
private int resolutionX;
private int resolutionY;
public ResolutionPreference(Context context, AttributeSet attrs) {
super(context, attrs);
setDialogLayoutResource(R.layout.dialog_resolution);
setPositiveButtonText(android.R.string.ok);
setNegativeButtonText(android.R.string.cancel);
setDialogIcon(null);
}
private int getPreferenceState() {
int checkboxState;
int sliderState;
if (a4_3.isChecked()) {
checkboxState = 1;
}
else if (a16_10.isChecked()) {
checkboxState = 2;
}
else {
checkboxState = 0;
}
sliderState = resolutionSeekBar.getProgress();
return checkboxState << 24 | sliderState;
}
private void setPreferenceState(int state) {
int checkboxState;
int sliderState;
checkboxState = state >> 24;
sliderState = state & 0x00FFFFFF;
a4_3.setChecked(false);
a16_10.setChecked(false);
a16_9.setChecked(false);
if (checkboxState == 1) {
a4_3.setChecked(true);
}
else if (checkboxState == 2) {
a16_10.setChecked(true);
}
else {
a16_9.setChecked(true);
}
resolutionSeekBar.setProgress(sliderState);
}
private void updateResolution() {
double aspectFactor;
int step = resolutionSeekBar.getProgress() + 1;
if (a4_3.isChecked()) {
aspectFactor = 4.0/3.0;
switch (step) {
case 1:
// 640x480
resolutionY = 480;
break;
case 2:
// 800x600
resolutionY = 600;
break;
case 3:
// 1024x768
resolutionY = 768;
break;
case 4:
// 1600x1200
resolutionY = 1200;
break;
}
}
else if (a16_9.isChecked()) {
aspectFactor = 16.0/9.0;
switch (step) {
case 1:
// 1280x720
resolutionY = 720;
break;
case 2:
// 1920x1080
resolutionY = 1080;
break;
case 3:
// 2560x1440
resolutionY = 1440;
break;
case 4:
// 3840x2160
resolutionY = 2160;
break;
}
}
else /* if (a16_10.isChecked() */ {
aspectFactor = 16.0/10.0;
switch (step) {
case 1:
// 1280x800
resolutionY = 800;
break;
case 2:
// 1920x1200
resolutionY = 1200;
break;
case 3:
// 2560x1600
resolutionY = 1600;
break;
case 4:
// 3840x2400
resolutionY = 2400;
break;
}
}
resolutionX = (int)(resolutionY * aspectFactor);
resolutionText.setText(resolutionX+"x"+resolutionY);
}
@Override
protected void onDialogClosed(boolean positiveResult) {
if (positiveResult) {
SharedPreferences.Editor e = getPreferenceManager().getSharedPreferences().edit();
e.putInt(PreferenceConfiguration.RESX_PREF_STRING, resolutionX);
e.putInt(PreferenceConfiguration.RESY_PREF_STRING, resolutionY);
persistInt(getPreferenceState());
e.commit();
}
}
@Override
protected View onCreateDialogView() {
View v = super.onCreateDialogView();
a4_3 = (RadioButton) v.findViewById(R.id.aspect_4_3);
a16_9 = (RadioButton) v.findViewById(R.id.aspect_16_9);
a16_10 = (RadioButton) v.findViewById(R.id.aspect_16_10);
resolutionText = (TextView) v.findViewById(R.id.resolutionText);
resolutionSeekBar = (SeekBar) v.findViewById(R.id.resolutionSeekbar);
CompoundButton.OnCheckedChangeListener ccListener = new CompoundButton.OnCheckedChangeListener() {
@Override
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
updateResolution();
}
};
a4_3.setOnCheckedChangeListener(ccListener);
a16_9.setOnCheckedChangeListener(ccListener);
a16_10.setOnCheckedChangeListener(ccListener);
resolutionSeekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
@Override
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
updateResolution();
}
@Override
public void onStartTrackingTouch(SeekBar seekBar) {
}
@Override
public void onStopTrackingTouch(SeekBar seekBar) {
}
});
// Check the correct checkbox
setPreferenceState(getPersistedInt(0));
// Set initial resolution value
updateResolution();
return v;
}
}
@@ -20,10 +20,14 @@ public class SeekBarPreference extends DialogPreference
private SeekBar seekBar;
private TextView valueText;
private Context context;
private final Context context;
private String dialogMessage, suffix;
private int defaultValue, maxValue, minValue, currentValue;
private final String dialogMessage;
private final String suffix;
private final int defaultValue;
private final int maxValue;
private final int minValue;
private int currentValue;
public SeekBarPreference(Context context, AttributeSet attrs) {
super(context, attrs);
@@ -127,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) {
this.currentValue = progress;
if (seekBar != null) {
@@ -51,6 +51,26 @@ public class StreamSettings extends Activity {
super.onCreate(savedInstanceState);
addPreferencesFromResource(R.xml.preferences);
// Add a listener to the FPS and resolution preference
// so the bitrate can be auto-adjusted
Preference pref = findPreference(PreferenceConfiguration.RES_FPS_PREF_STRING);
pref.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
@Override
public boolean onPreferenceChange(Preference preference, Object newValue) {
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(SettingsFragment.this.getActivity());
String valueStr = (String) newValue;
// Write the new bitrate value
prefs.edit()
.putInt(PreferenceConfiguration.BITRATE_PREF_STRING,
PreferenceConfiguration.getDefaultBitrate(valueStr))
.apply();
// Allow the original preference change to take place
return true;
}
});
}
}
}
@@ -1,5 +1,7 @@
package com.limelight.utils;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
@@ -9,8 +11,6 @@ import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.Reader;
import java.io.UnsupportedEncodingException;
import java.util.Scanner;
public class CacheHelper {
private static File openPath(boolean createPath, File root, String... path) {
@@ -30,12 +30,25 @@ public class CacheHelper {
return f;
}
public static FileInputStream openCacheFileForInput(File root, String... path) throws FileNotFoundException {
return new FileInputStream(openPath(false, root, path));
public static boolean cacheFileExists(File root, String... path) {
return openPath(false, root, path).exists();
}
public static FileOutputStream openCacheFileForOutput(File root, String... path) throws FileNotFoundException {
return new FileOutputStream(openPath(true, root, path));
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 {
@@ -48,6 +61,10 @@ public class CacheHelper {
sb.append(buf, 0, bytesRead);
}
try {
in.close();
} catch (IOException ignored) {}
return sb.toString();
}
@@ -7,15 +7,16 @@ import android.app.AlertDialog;
import android.content.DialogInterface;
public class Dialog implements Runnable {
private String title, message;
private Activity activity;
private boolean endAfterDismiss;
private final String title;
private final String message;
private final Activity activity;
private final boolean endAfterDismiss;
private AlertDialog alert;
private static final ArrayList<Dialog> rundownDialogs = new ArrayList<Dialog>();
public Dialog(Activity activity, String title, String message, boolean endAfterDismiss)
private Dialog(Activity activity, String title, String message, boolean endAfterDismiss)
{
this.activity = activity;
this.title = title;
@@ -0,0 +1,90 @@
package com.limelight.utils;
import android.app.Activity;
import android.content.Intent;
import android.widget.Toast;
import com.limelight.Game;
import com.limelight.R;
import com.limelight.binding.PlatformBinding;
import com.limelight.computers.ComputerManagerService;
import com.limelight.nvstream.http.ComputerDetails;
import com.limelight.nvstream.http.GfeHttpResponseException;
import com.limelight.nvstream.http.NvApp;
import com.limelight.nvstream.http.NvHTTP;
import java.io.FileNotFoundException;
import java.net.InetAddress;
import java.net.UnknownHostException;
public class ServerHelper {
public static InetAddress getCurrentAddressFromComputer(ComputerDetails computer) {
return computer.reachability == ComputerDetails.Reachability.LOCAL ?
computer.localIp : computer.remoteIp;
}
public static void doStart(Activity parent, NvApp app, ComputerDetails computer,
ComputerManagerService.ComputerManagerBinder managerBinder) {
Intent intent = new Intent(parent, Game.class);
intent.putExtra(Game.EXTRA_HOST,
computer.reachability == ComputerDetails.Reachability.LOCAL ?
computer.localIp.getHostAddress() : computer.remoteIp.getHostAddress());
intent.putExtra(Game.EXTRA_APP_NAME, app.getAppName());
intent.putExtra(Game.EXTRA_APP_ID, app.getAppId());
intent.putExtra(Game.EXTRA_UNIQUEID, managerBinder.getUniqueId());
intent.putExtra(Game.EXTRA_STREAMING_REMOTE,
computer.reachability != ComputerDetails.Reachability.LOCAL);
parent.startActivity(intent);
}
public static void doQuit(final Activity parent,
final InetAddress address,
final NvApp app,
final ComputerManagerService.ComputerManagerBinder managerBinder,
final Runnable onComplete) {
Toast.makeText(parent, parent.getResources().getString(R.string.applist_quit_app) + " " + app.getAppName() + "...", Toast.LENGTH_SHORT).show();
new Thread(new Runnable() {
@Override
public void run() {
NvHTTP httpConn;
String message;
try {
httpConn = new NvHTTP(address,
managerBinder.getUniqueId(), null, PlatformBinding.getCryptoProvider(parent));
if (httpConn.quitApp()) {
message = parent.getResources().getString(R.string.applist_quit_success) + " " + app.getAppName();
} else {
message = parent.getResources().getString(R.string.applist_quit_fail) + " " + app.getAppName();
}
} catch (GfeHttpResponseException e) {
if (e.getErrorCode() == 599) {
message = "This session wasn't started by this device," +
" so it cannot be quit. End streaming on the original " +
"device or the PC itself. (Error code: "+e.getErrorCode()+")";
}
else {
message = e.getMessage();
}
} catch (UnknownHostException e) {
message = parent.getResources().getString(R.string.error_unknown_host);
} catch (FileNotFoundException e) {
message = parent.getResources().getString(R.string.error_404);
} catch (Exception e) {
message = e.getMessage();
} finally {
if (onComplete != null) {
onComplete.run();
}
}
final String toastMessage = message;
parent.runOnUiThread(new Runnable() {
@Override
public void run() {
Toast.makeText(parent, toastMessage, Toast.LENGTH_LONG).show();
}
});
}
}).start();
}
}
@@ -9,14 +9,15 @@ import android.content.DialogInterface;
import android.content.DialogInterface.OnCancelListener;
public class SpinnerDialog implements Runnable,OnCancelListener {
private String title, message;
private Activity activity;
private final String title;
private final String message;
private final Activity activity;
private ProgressDialog progress;
private boolean finish;
private final boolean finish;
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.title = title;
@@ -1,11 +1,15 @@
package com.limelight.utils;
import android.app.Activity;
import android.app.AlertDialog;
import android.app.UiModeManager;
import android.content.Context;
import android.content.DialogInterface;
import android.content.res.Configuration;
import android.view.View;
import com.limelight.R;
public class UiHelper {
// Values from https://developer.android.com/training/tv/start/layouts.html
@@ -28,4 +32,31 @@ public class UiHelper {
horizontalPaddingPixels, verticalPaddingPixels);
}
}
public static void displayQuitConfirmationDialog(Activity parent, final Runnable onYes, final Runnable onNo) {
DialogInterface.OnClickListener dialogClickListener = new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
switch (which){
case DialogInterface.BUTTON_POSITIVE:
if (onYes != null) {
onYes.run();
}
break;
case DialogInterface.BUTTON_NEGATIVE:
if (onNo != null) {
onNo.run();
}
break;
}
}
};
AlertDialog.Builder builder = new AlertDialog.Builder(parent);
builder.setMessage(parent.getResources().getString(R.string.applist_quit_confirmation))
.setPositiveButton(parent.getResources().getString(R.string.yes), dialogClickListener)
.setNegativeButton(parent.getResources().getString(R.string.no), dialogClickListener)
.show();
}
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.4 KiB

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.6 KiB

After

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 66 KiB

After

Width:  |  Height:  |  Size: 99 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.6 KiB

After

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

After

Width:  |  Height:  |  Size: 65 KiB

+2 -2
View File
@@ -1,4 +1,4 @@
<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"
android:layout_width="match_parent"
android:layout_height="match_parent"
@@ -10,4 +10,4 @@
android:layout_height="match_parent"
android:layout_gravity="center" />
</FrameLayout>
</merge>
@@ -9,5 +9,7 @@
android:numColumns="auto_fit"
android:columnWidth="160dp"
android:stretchMode="spacingWidth"
android:focusable="true"
android:focusableInTouchMode="true"
android:gravity="center"/>
</LinearLayout>
@@ -9,5 +9,7 @@
android:numColumns="auto_fit"
android:columnWidth="105dp"
android:stretchMode="spacingWidth"
android:focusable="true"
android:focusableInTouchMode="true"
android:gravity="center"/>
</LinearLayout>
@@ -1,47 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/dialog_resolution"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<RadioGroup
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_centerHorizontal="true"
android:id="@+id/aspectGroup">
<RadioButton
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="5dp"
android:id="@+id/aspect_4_3"
android:text="@string/aspect_4_3"/>
<RadioButton
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="5dp"
android:id="@+id/aspect_16_9"
android:text="@string/aspect_16_9"/>
<RadioButton
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="5dp"
android:id="@+id/aspect_16_10"
android:text="@string/aspect_16_10"/>
</RadioGroup>
<SeekBar
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/aspectGroup"
android:max="3"
android:id="@+id/resolutionSeekbar"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerHorizontal="true"
android:textSize="20sp"
android:id="@+id/resolutionText"
android:layout_below="@+id/resolutionSeekbar"/>
</RelativeLayout>
+3 -1
View File
@@ -6,10 +6,12 @@
<ListView
android:id="@+id/fragmentView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
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>
+2
View File
@@ -8,5 +8,7 @@
android:layout_height="fill_parent"
android:numColumns="auto_fit"
android:columnWidth="160dp"
android:focusable="true"
android:focusableInTouchMode="true"
android:gravity="center"/>
</LinearLayout>
@@ -8,5 +8,7 @@
android:layout_height="fill_parent"
android:numColumns="auto_fit"
android:columnWidth="105dp"
android:focusable="true"
android:focusableInTouchMode="true"
android:gravity="center"/>
</LinearLayout>
+3 -1
View File
@@ -95,12 +95,14 @@
<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="summary_language_list">Lingua da usare in Moonlight</string>
<string name="title_checkbox_list_mode">Usa lista invece della griglia</string>
<string name="summary_checkbox_list_mode">Visualizza applicazioni e computers in una lista invece di una griglia</string>
<string name="title_checkbox_small_icon_mode">Usa icone piccole</string>
+10 -6
View File
@@ -1,12 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string-array name="fps_names">
<item>30 FPS</item>
<item>60 FPS</item>
<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="fps_values" translatable="false">
<item>30</item>
<item>60</item>
<string-array name="resolution_values" translatable="false">
<item>720p30</item>
<item>720p60</item>
<item>1080p30</item>
<item>1080p60</item>
</string-array>
<string-array name="language_names" translatable="false">
+7 -12
View File
@@ -44,9 +44,9 @@
<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">Average frame decoding 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_hardware_latency">Average hardware decoding 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>
@@ -85,10 +85,8 @@
<!-- Preferences -->
<string name="category_basic_settings">Basic Settings</string>
<string name="title_resolution">Select stream resolution</string>
<string name="summary_resolution">Some resolutions may not be supported on your device</string>
<string name="title_fps_list">Select target FPS</string>
<string name="summary_fps_list">Some FPS values may not be supported with high resolutions on your device</string>
<string name="title_resolution_list">Select resolution and FPS target</string>
<string name="summary_resolution_list">Setting values too high for your device may cause lag or crashing</string>
<string name="title_seekbar_bitrate">Select target video bitrate</string>
<string name="summary_seekbar_bitrate">Lower bitrate to reduce stuttering. Raise bitrate to increase image quality.</string>
<string name="suffix_seekbar_bitrate">Mbps</string>
@@ -97,12 +95,14 @@
<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="summary_language_list">Language to use for Moonlight</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>
@@ -117,9 +117,4 @@
<string name="category_advanced_settings">Advanced Settings</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>
<!-- Resolution Preference -->
<string name="aspect_4_3">4:3</string>
<string name="aspect_16_9">16:9</string>
<string name="aspect_16_10">16:10</string>
</resources>
+14 -13
View File
@@ -2,17 +2,13 @@
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
<PreferenceCategory android:title="@string/category_basic_settings">
<com.limelight.preferences.ResolutionPreference
android:key="resolution"
android:title="@string/title_resolution"
android:summary="@string/summary_resolution" />
<ListPreference
android:key="list_resolution_fps"
android:title="@string/title_fps_list"
android:summary="@string/summary_fps_list"
android:entries="@array/fps_names"
android:entryValues="@array/fps_values"
android:defaultValue="60" />
android:title="@string/title_resolution_list"
android:summary="@string/summary_resolution_list"
android:entries="@array/resolution_names"
android:entryValues="@array/resolution_values"
android:defaultValue="720p60" />
<com.limelight.preferences.SeekBarPreference
android:key="seekbar_bitrate"
android:dialogMessage="@string/summary_seekbar_bitrate"
@@ -30,14 +26,19 @@
android:summary="@string/summary_checkbox_disable_warnings"
android:defaultValue="false" />
</PreferenceCategory>
<!--PreferenceCategory android:title="@string/category_gamepad_settings">
<com.limelight.preferences.SeekBarPreference
<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"/>
</PreferenceCategory-->
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">
<CheckBoxPreference
android:key="checkbox_enable_sops"
+1 -1
View File
@@ -2,5 +2,5 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- Non-root application name -->
<application android:label="Limelight" />
<application android:label="Moonlight" />
</manifest>
+1 -1
View File
@@ -5,5 +5,5 @@
<uses-permission android:name="android.permission.ACCESS_SUPERUSER" />
<!-- Root application name -->
<application android:label="Limelight (Root)" />
<application android:label="Moonlight (Root)" />
</manifest>
+1 -1
View File
@@ -4,7 +4,7 @@ buildscript {
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:1.0.0'
classpath 'com.android.tools.build:gradle:1.2.3'
}
}
@@ -1,15 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<module external.linked.project.path="$MODULE_DIR$" external.root.project.path="$MODULE_DIR$" external.system.id="GRADLE" external.system.module.group="" external.system.module.version="unspecified" type="JAVA_MODULE" version="4">
<module external.linked.project.id="moonlight-android" external.linked.project.path="$MODULE_DIR$" external.root.project.path="$MODULE_DIR$" external.system.id="GRADLE" external.system.module.group="" external.system.module.version="unspecified" type="JAVA_MODULE" version="4">
<component name="FacetManager">
<facet type="java-gradle" name="Java-Gradle">
<configuration>
<option name="BUILD_FOLDER_PATH" value="$MODULE_DIR$/build" />
<option name="BUILDABLE" value="false" />
</configuration>
</facet>
</component>
<component name="NewModuleRootManager" inherit-compiler-output="false">
<output url="file://$MODULE_DIR$/build/classes/main" />
<output-test url="file://$MODULE_DIR$/build/classes/test" />
<component name="NewModuleRootManager" inherit-compiler-output="true">
<exclude-output />
<content url="file://$MODULE_DIR$">
<excludeFolder url="file://$MODULE_DIR$/.gradle" />
@@ -18,4 +17,3 @@
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>