Compare commits

..
104 Commits
Author SHA1 Message Date
Cameron Gutman 5519d92243 Disable the start key shortcut to start the keyboard because the keyboard can't receive input after it's started 2015-02-07 13:58:53 -05:00
Cameron Gutman 3d95ac1f93 Fix keyboard dismissal on Fire TV devices 2015-02-07 13:42:49 -05:00
Cameron Gutman 5c938535be Fix app list focus issues with remotes/gamepads 2015-02-07 13:20:01 -05:00
Cameron Gutman 2fdecc551a Tabs -> Spaces 2015-02-07 11:54:46 -05:00
Cameron Gutman 10204afdb4 Only add PCs to the computer list when they have been polled once to get a UUID for equality comparison. Fix equality comparison in PcView to avoid duplicate PCs enumerated over mDNS. 2015-02-07 11:44:56 -05:00
Cameron Gutman 55c800c2a5 Fade in box art when scrolling 2015-02-07 06:52:28 -05:00
Cameron Gutman 265b3f9963 Use image alpha to make images transparent while loading 2015-02-07 06:23:35 -05:00
Cameron Gutman a8bf2cd1cf Fix UI dropped frames when loading images 2015-02-07 06:08:00 -05:00
Cameron Gutman 4fcd8b3dfe Replace unpair option with delete PC 2015-02-07 05:57:30 -05:00
Cameron Gutman e1a1a6344d Fill the whole height with the list view 2015-02-06 13:38:32 -05:00
Cameron Gutman a095c10a25 Increment version to 3.1 and update build files 2015-02-05 16:15:16 -05:00
Cameron Gutman b1ea487e22 Use the mode (power) button on the Asus Nexus Player Gamepad as a select button 2015-02-05 16:06:55 -05:00
Cameron Gutman 47265d0d10 Add another SELinux policy change needed on Nexus 9 2015-02-05 16:06:22 -05:00
Cameron Gutman 6a41b41a38 Merge pull request #55 from Ansa89/italian-translation
Italian translation: update
2015-02-05 13:34:03 -05:00
Cameron Gutman 2247e43a48 Remove unused imports 2015-02-05 13:23:01 -05:00
Cameron Gutman d3986080a3 Tighten up a bunch of declarations to make Lint happier 2015-02-05 13:21:04 -05:00
Cameron Gutman 07277e1a5b Fix a few Lint warnings 2015-02-05 13:01:35 -05:00
Ansa89 39d7fc748f Italian translation: update 2015-02-03 01:06:36 +01:00
Cameron Gutman 4d3a69cf6a Fix GFE 2.1.x controller regression 2015-02-02 18:10:18 -05:00
Cameron Gutman b806522751 Unassign the controller number when a device is removed 2015-02-02 02:13:27 -05:00
Cameron Gutman 256fa897a7 Fix build issues 2015-02-01 18:31:34 -05:00
Cameron Gutman 5c812eed6c Beta 2 version update 2015-02-01 18:20:55 -05:00
Cameron Gutman f0b22f9119 Don't use small mode on TVs 2015-02-01 18:20:39 -05:00
Cameron Gutman 7e1884acb5 Trap Shield's back button as controller 0 2015-02-01 18:07:03 -05:00
Cameron Gutman 9512521783 Update common 2015-02-01 15:06:27 -05:00
Cameron Gutman da7904a767 Add multiple controller support 2015-02-01 15:06:18 -05:00
Cameron Gutman 3a0c1db168 Merge pull request #54 from Ansa89/italian-translation
Italian translation: update
2015-02-01 00:44:17 -05:00
Cameron Gutman bd21692323 Properly center text on the app view 2015-02-01 00:43:57 -05:00
Cameron Gutman 5ae245bdca Trim spaces from the IP address 2015-02-01 00:39:47 -05:00
Cameron Gutman d3052cd97d Set small icon by default on phones 2015-02-01 00:33:43 -05:00
Cameron Gutman 336f85a31c Fix loading bugs with uncached images 2015-01-31 22:14:12 -05:00
Cameron Gutman b01f7c796e Fix duplicated fragments 2015-01-31 17:19:54 -05:00
Cameron Gutman 56f438fe47 Fix some crashes and caching issues 2015-01-31 17:01:46 -05:00
Cameron Gutman baa5199b83 Load cached images in the background to avoid stalling the UI thread 2015-01-31 16:59:45 -05:00
Cameron Gutman 23ca62b304 Fix dp constant 2015-01-31 14:00:47 -05:00
Cameron Gutman 2c3511195c Use small mode by default on things smaller than 7 inch tablets 2015-01-31 13:21:21 -05:00
Ansa89 d31ef481f3 Italian translation: update 2015-01-31 10:03:37 +01:00
Cameron Gutman a490da5e5c Fix some caching bugs 2015-01-31 00:13:51 -05:00
Cameron Gutman 72d3576257 Fix a crash and a hang in the new computer manager code 2015-01-30 19:33:42 -05:00
Cameron Gutman ebd93a55a0 Fix small icon mode 2015-01-30 19:17:00 -05:00
Cameron Gutman 4d01e1afe6 Stub icon scaling and allow background updating of the applist 2015-01-30 18:49:01 -05:00
Cameron Gutman 9ff1386751 Add a quit confirmation dialog 2015-01-27 15:31:01 -05:00
Cameron Gutman 5fca35f0b1 Sort app list alphabetically 2015-01-26 20:58:33 -05:00
Cameron Gutman d23c763441 Remove unused imports 2015-01-26 20:50:14 -05:00
Cameron Gutman fa058c4783 Merge pull request #53 from Ansa89/italian-translation
Italian translation: update
2015-01-26 20:47:00 -05:00
Ansa89 e0ddd5f045 Italian translation: update 2015-01-26 10:53:30 +01:00
Cameron Gutman b7443451a4 Fix release build failure for beta 2015-01-25 23:38:33 -05:00
Cameron Gutman e90e4a22c4 Increment version 2015-01-25 23:35:24 -05:00
Cameron Gutman 3a53172145 Apply list mode preference immediately 2015-01-25 23:28:13 -05:00
Cameron Gutman 1dfcb7bc29 Fix root input device capture on the Nexus 9 2015-01-25 23:16:32 -05:00
Cameron Gutman 897bb76858 Forgot this file 2015-01-25 22:58:17 -05:00
Cameron Gutman bcc67269ab Add gestures to bring up the software keyboard - Long press start or tap with 3 fingers 2015-01-25 22:55:12 -05:00
Cameron Gutman 4d24c654b9 Remove the old fragment when adding the new one 2015-01-25 22:11:38 -05:00
Cameron Gutman cba44b091b Add common with GFE 2.1.x backwards compatibility 2015-01-25 21:35:03 -05:00
Cameron Gutman f2d8f8a41b Update preference text 2015-01-25 21:04:27 -05:00
Cameron Gutman 4b1c7e7e3c Fix state loss crashes 2015-01-25 21:04:13 -05:00
Cameron Gutman 1cba278876 Cache box art locally 2015-01-25 21:00:34 -05:00
Cameron Gutman 766898fdf9 Add list support back for users that don't like the grid 2015-01-25 20:23:35 -05:00
Cameron Gutman 13e91d594b Fix Lint and build issues 2015-01-25 18:50:31 -05:00
Cameron Gutman ca0a0da19f Fix fusion of computers that were re-added after becoming unreachable 2015-01-25 18:41:44 -05:00
Cameron Gutman 82cabce86e Merge pull request #33 from Ansa89/language_chooser
Add language chooser
2015-01-25 16:46:10 -05:00
Cameron Gutman 51a630995a Update common for GFE 2.2.2+ support 2015-01-22 15:29:58 -05:00
Cameron Gutman 3a74f0726c Updated libs 2015-01-22 15:29:46 -05:00
Cameron Gutman efa6c7bba0 Fix build of root package 2015-01-22 15:29:16 -05:00
Michelle Bergeron b8141542f8 Add wiki link 2015-01-17 18:44:31 -08:00
Cameron Gutman 8fc9a90207 Switch back to Maven repos of ion and androidasync packages 2014-12-13 20:41:32 -08:00
Cameron Gutman 13d707d98d Use final release of Gradle 1.0 2014-12-09 01:13:13 -08:00
Cameron Gutman aae0ff6e7a Migrate the project to Android Studio 1.0 RC4 2014-12-08 00:12:37 -08:00
Cameron Gutman 69c7b5a0d5 Update version 2014-12-03 20:52:52 -08:00
Cameron Gutman d1ad3115fa Add remote to stream config 2014-12-02 00:55:46 -08:00
Cameron Gutman 770af402a4 Reduce default 1080p60 bitrate to 20 Mbps 2014-12-02 00:55:31 -08:00
Cameron Gutman 3236c0b93a Lower the level_idc of the SPS to the minimum required for streaming at a given resolution 2014-12-01 22:58:52 -08:00
Cameron Gutman 51aacc3f38 Remove extra newlines 2014-12-01 22:39:17 -08:00
Cameron Gutman 397c6f46f9 Fix a security issue which caused input devices to remain world readable after the stream is ended 2014-12-01 22:29:16 -08:00
Cameron Gutman d00f78f859 Revert square to circle analog work since it seems to be handled correctly already 2014-12-01 22:27:02 -08:00
Cameron Gutman 29fec2e0de Add initial support for rooted devices running Lollipop with SELinux set to enforcing. This should really be improved in the future since we're modifying policies for untrusted_app. 2014-12-01 22:26:35 -08:00
Cameron Gutman 88d28665ef Attempt to fix IndexOutOfBoundsException (index 0 size 0) reported by a couple users 2014-11-30 18:34:34 -06:00
Cameron Gutman de1f4da258 Apply the square to circle plane mapping before evaluating the deadzone. Cleanup some dead code. 2014-11-30 15:52:49 -06:00
Cameron Gutman 7985be57ab Translate the analog stick values of controllers with "square" analog stick planes (DS3, DS4, and others) to the circular plane that XInput programs expect 2014-11-30 15:35:20 -06:00
Cameron Gutman a835e7aaa2 Increase DS4 controller responsiveness by ignoring historical values again 2014-11-30 12:34:30 -06:00
Ansa89 22958cfbb1 Language chooser: use constants 2014-11-29 14:40:03 +01:00
Cameron Gutman c4dc5eb9e1 Update common for faster IDR recovery 2014-11-28 22:17:42 -06:00
Cameron Gutman db758f386e Comment out unused variable 2014-11-28 22:16:46 -06:00
Cameron Gutman 3fb3eefa94 Fix Nyko Playpad input issue 2014-11-28 22:16:33 -06:00
Ansa89 9340dff45d PreferenceConfiguration.java: add language preference 2014-11-28 10:26:14 +01:00
Cameron Gutman 2d6c756e70 Always consider a PC to be remote if localIP == remoteIP 2014-11-27 21:56:20 -06:00
Cameron Gutman 03e965d449 Merge pull request #34 from Ansa89/italian-translation
Italian translation: better wording
2014-11-27 20:35:57 -06:00
Cameron Gutman 34f72544d8 Increment version 2014-11-25 14:56:40 -08:00
Cameron Gutman d839ea9781 Increase deadzone on triggers to Xinput defaults and add special handling of the Nexus Player Controller and Nexus Remote 2014-11-25 14:54:36 -08:00
Cameron Gutman 2b7f13fdbb Increase max frame time to improve accuracy of latency counter 2014-11-25 13:34:00 -08:00
Cameron Gutman 7557a3a4ae Don't capture the back button on remotes 2014-11-25 11:16:47 -08:00
Cameron Gutman fcecba484f Fix a crash caught by Monkey 2014-11-25 02:05:24 -08:00
Cameron Gutman fa85a0a0bd Improve CPU decoder frame latency when rendering speed is less than decoding speed 2014-11-25 02:04:51 -08:00
Cameron Gutman dc64bfeba2 Slightly reduce max packet size in an attempt to cut packet losses 2014-11-25 01:05:55 -08:00
Cameron Gutman 871b73c48d Fix PC duplication issue when multiple machines report the same remote IP address 2014-11-24 20:10:02 -08:00
Cameron Gutman 5dcff91d27 Only grab Fire TV remotes if a gamepad isn't attached 2014-11-24 18:43:08 -08:00
Cameron Gutman 0041fc1dab Fix broken de-duplication of computers 2014-11-24 18:25:58 -08:00
Cameron Gutman 314242ab08 Update to Ion with fixes for SSLContext and self-signed certificates 2014-11-24 18:10:23 -08:00
Cameron Gutman 09e8ddfd74 Use the bitstream restrictions fixup on Broadcom VideoCore IV devices 2014-11-24 18:03:47 -08:00
Ansa89 4cea483a87 Italian translation: better wording 2014-11-24 11:53:18 +01:00
Ansa89 99aa616188 Add language chooser
Implement limelight-stream/limelight-android#32.
2014-11-24 11:47:02 +01:00
Cameron Gutman 444c4602c1 Update libraries. Seems to improve image caching behavior with Ion. 2014-11-23 23:39:20 -08:00
Cameron Gutman 5b6eac7140 Update build.gradle for re-release 2014-11-23 02:07:03 -08:00
Cameron Gutman 7cdd184197 Fix null pointer exception on ATV emulator 2014-11-23 02:06:49 -08:00
60 changed files with 1876 additions and 616 deletions

No files matched your search

+2
View File
@@ -8,6 +8,8 @@ whether in your own home or over the internet.
[Limelight-pc](https://github.com/limelight-stream/limelight-pc) is also currently in development for Windows, OS X and Linux. Versions for [iOS](https://github.com/limelight-stream/limelight-ios) and [Windows and Windows Phone](https://github.com/limelight-stream/limelight-windows) are also in development. [Limelight-pc](https://github.com/limelight-stream/limelight-pc) is also currently in development for Windows, OS X and Linux. Versions for [iOS](https://github.com/limelight-stream/limelight-ios) and [Windows and Windows Phone](https://github.com/limelight-stream/limelight-windows) are also in development.
Check our [wiki](https://github.com/limelight-stream/limelight-android/wiki) for more detailed information or a troubleshooting guide.
##Features ##Features
* Streams any of your games from your PC to your Android device * Streams any of your games from your PC to your Android device
+10 -6
View File
@@ -9,6 +9,7 @@
<facet type="android" name="Android"> <facet type="android" name="Android">
<configuration> <configuration>
<option name="SELECTED_BUILD_VARIANT" value="nonRootDebug" /> <option name="SELECTED_BUILD_VARIANT" value="nonRootDebug" />
<option name="SELECTED_TEST_ARTIFACT" value="_android_test_" />
<option name="ASSEMBLE_TASK_NAME" value="assembleNonRootDebug" /> <option name="ASSEMBLE_TASK_NAME" value="assembleNonRootDebug" />
<option name="COMPILE_JAVA_TASK_NAME" value="compileNonRootDebugSources" /> <option name="COMPILE_JAVA_TASK_NAME" value="compileNonRootDebugSources" />
<option name="ASSEMBLE_TEST_TASK_NAME" value="assembleNonRootDebugTest" /> <option name="ASSEMBLE_TEST_TASK_NAME" value="assembleNonRootDebugTest" />
@@ -24,6 +25,7 @@
</component> </component>
<component name="NewModuleRootManager" inherit-compiler-output="false"> <component name="NewModuleRootManager" inherit-compiler-output="false">
<output url="file://$MODULE_DIR$/build/intermediates/classes/nonRoot/debug" /> <output url="file://$MODULE_DIR$/build/intermediates/classes/nonRoot/debug" />
<output-test url="file://$MODULE_DIR$/build/intermediates/classes/test/nonRoot/debug" />
<exclude-output /> <exclude-output />
<content url="file://$MODULE_DIR$"> <content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/r/nonRoot/debug" isTestSource="false" generated="true" /> <sourceFolder url="file://$MODULE_DIR$/build/generated/source/r/nonRoot/debug" isTestSource="false" generated="true" />
@@ -31,6 +33,7 @@
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/buildConfig/nonRoot/debug" isTestSource="false" generated="true" /> <sourceFolder url="file://$MODULE_DIR$/build/generated/source/buildConfig/nonRoot/debug" isTestSource="false" generated="true" />
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/rs/nonRoot/debug" isTestSource="false" generated="true" /> <sourceFolder url="file://$MODULE_DIR$/build/generated/source/rs/nonRoot/debug" isTestSource="false" generated="true" />
<sourceFolder url="file://$MODULE_DIR$/build/generated/res/rs/nonRoot/debug" type="java-resource" /> <sourceFolder url="file://$MODULE_DIR$/build/generated/res/rs/nonRoot/debug" type="java-resource" />
<sourceFolder url="file://$MODULE_DIR$/build/generated/res/generated/nonRoot/debug" type="java-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/nonRootDebug/res" type="java-resource" /> <sourceFolder url="file://$MODULE_DIR$/src/nonRootDebug/res" type="java-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/nonRootDebug/resources" type="java-resource" /> <sourceFolder url="file://$MODULE_DIR$/src/nonRootDebug/resources" type="java-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/nonRootDebug/assets" type="java-resource" /> <sourceFolder url="file://$MODULE_DIR$/src/nonRootDebug/assets" type="java-resource" />
@@ -43,6 +46,7 @@
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/buildConfig/test/nonRoot/debug" isTestSource="true" generated="true" /> <sourceFolder url="file://$MODULE_DIR$/build/generated/source/buildConfig/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/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/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$/src/nonRoot/res" type="java-resource" /> <sourceFolder url="file://$MODULE_DIR$/src/nonRoot/res" type="java-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/nonRoot/resources" type="java-resource" /> <sourceFolder url="file://$MODULE_DIR$/src/nonRoot/resources" type="java-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/nonRoot/assets" type="java-resource" /> <sourceFolder url="file://$MODULE_DIR$/src/nonRoot/assets" type="java-resource" />
@@ -101,18 +105,18 @@
</content> </content>
<orderEntry type="jdk" jdkName="Android API 21 Platform" jdkType="Android SDK" /> <orderEntry type="jdk" jdkName="Android API 21 Platform" jdkType="Android SDK" />
<orderEntry type="sourceFolder" forTests="false" /> <orderEntry type="sourceFolder" forTests="false" />
<orderEntry type="library" exported="" name="okhttp-2.1.0-RC1" level="project" />
<orderEntry type="library" exported="" name="bcprov-jdk15on-1.51" level="project" /> <orderEntry type="library" exported="" name="bcprov-jdk15on-1.51" level="project" />
<orderEntry type="library" exported="" name="androidasync-2.0.5" level="project" />
<orderEntry type="library" exported="" name="gson-2.3.1" level="project" /> <orderEntry type="library" exported="" name="gson-2.3.1" level="project" />
<orderEntry type="library" exported="" name="androidasync-1.3.7" 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="jmdns-fixed" level="project" />
<orderEntry type="library" exported="" name="jcodec-0.1.6-3" level="project" />
<orderEntry type="library" exported="" name="bcpkix-jdk15on-1.51" level="project" /> <orderEntry type="library" exported="" name="bcpkix-jdk15on-1.51" level="project" />
<orderEntry type="library" exported="" name="tinyrtsp" level="project" /> <orderEntry type="library" exported="" name="tinyrtsp" level="project" />
<orderEntry type="library" exported="" name="limelight-common" level="project" /> <orderEntry type="library" exported="" name="limelight-common" level="project" />
<orderEntry type="library" exported="" name="support-v4-r6" level="project" /> <orderEntry type="library" exported="" name="ion-2.0.5" level="project" />
<orderEntry type="library" exported="" name="okio-1.0.1" level="project" /> <orderEntry type="library" exported="" name="okhttp-2.2.0" level="project" />
<orderEntry type="library" exported="" name="ion-1.3.7" level="project" /> <orderEntry type="library" exported="" name="jcodec-0.1.9" level="project" />
<orderEntry type="library" exported="" name="okio-1.2.0" level="project" />
</component> </component>
</module> </module>
+14 -9
View File
@@ -5,14 +5,14 @@ apply plugin: 'com.android.application'
android { android {
compileSdkVersion 21 compileSdkVersion 21
buildToolsVersion "21.0.2" buildToolsVersion "21.1.2"
defaultConfig { defaultConfig {
minSdkVersion 16 minSdkVersion 16
targetSdkVersion 21 targetSdkVersion 21
versionName "3.0" versionName "3.1"
versionCode = 45 versionCode = 53
} }
productFlavors { productFlavors {
@@ -27,7 +27,7 @@ android {
buildTypes { buildTypes {
release { release {
runProguard false minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt' proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt'
} }
} }
@@ -62,13 +62,18 @@ android {
} }
dependencies { dependencies {
compile group: 'org.jcodec', name: 'jcodec', version: '0.1.6-3' compile group: 'org.jcodec', name: 'jcodec', version: '0.1.9'
compile group: 'org.bouncycastle', name: 'bcprov-jdk15on', version: '1.51' compile group: 'org.bouncycastle', name: 'bcprov-jdk15on', version: '1.51'
compile group: 'org.bouncycastle', name: 'bcpkix-jdk15on', version: '1.51' compile group: 'org.bouncycastle', name: 'bcpkix-jdk15on', version: '1.51'
compile group: 'com.google.android', name: 'support-v4', version:'r6'
compile group: 'com.koushikdutta.ion', name: 'ion', version:'1.3.7' compile group: 'com.google.android', name: 'support-v4', version:'r7'
compile group: 'com.squareup.okhttp', name: 'okhttp', version:'2.1.0-RC1' compile group: 'com.koushikdutta.ion', name: 'ion', version:'2.0.5'
compile group: 'com.squareup.okio', name:'okio', version:'1.0.1' compile group: 'com.google.code.gson', name: 'gson', version:'2.3.1'
compile group: 'com.squareup.okhttp', name: 'okhttp', version:'2.2.0'
compile group: 'com.squareup.okio', name:'okio', version:'1.2.0'
compile files('libs/jmdns-fixed.jar') compile files('libs/jmdns-fixed.jar')
compile files('libs/limelight-common.jar') compile files('libs/limelight-common.jar')
compile files('libs/tinyrtsp.jar') compile files('libs/tinyrtsp.jar')
Binary file not shown.
+1 -1
View File
@@ -68,7 +68,7 @@
android:configChanges="mcc|mnc|locale|touchscreen|keyboard|keyboardHidden|navigation|screenLayout|fontScale|uiMode|orientation|screenSize|smallestScreenSize|layoutDirection" > android:configChanges="mcc|mnc|locale|touchscreen|keyboard|keyboardHidden|navigation|screenLayout|fontScale|uiMode|orientation|screenSize|smallestScreenSize|layoutDirection" >
<meta-data <meta-data
android:name="android.support.PARENT_ACTIVITY" android:name="android.support.PARENT_ACTIVITY"
android:value="com.limelight.Connection" /> android:value="com.limelight.AppView" />
</activity> </activity>
<service <service
android:name=".discovery.DiscoveryService" android:name=".discovery.DiscoveryService"
+315 -101
View File
@@ -1,109 +1,237 @@
package com.limelight; package com.limelight;
import java.io.FileNotFoundException; import java.io.FileNotFoundException;
import java.io.IOException; import java.io.StringReader;
import java.net.InetAddress; import java.net.InetAddress;
import java.net.UnknownHostException; import java.net.UnknownHostException;
import java.util.List; import java.util.List;
import java.util.Locale;
import org.xmlpull.v1.XmlPullParserException; import java.util.UUID;
import com.limelight.binding.PlatformBinding; import com.limelight.binding.PlatformBinding;
import com.limelight.computers.ComputerManagerListener;
import com.limelight.computers.ComputerManagerService;
import com.limelight.grid.AppGridAdapter; import com.limelight.grid.AppGridAdapter;
import com.limelight.nvstream.http.GfeHttpResponseException; import com.limelight.nvstream.http.ComputerDetails;
import com.limelight.nvstream.http.NvApp; import com.limelight.nvstream.http.NvApp;
import com.limelight.nvstream.http.NvHTTP; import com.limelight.nvstream.http.NvHTTP;
import com.limelight.preferences.PreferenceConfiguration;
import com.limelight.ui.AdapterFragment;
import com.limelight.ui.AdapterFragmentCallbacks;
import com.limelight.utils.CacheHelper;
import com.limelight.utils.Dialog; import com.limelight.utils.Dialog;
import com.limelight.utils.SpinnerDialog; import com.limelight.utils.SpinnerDialog;
import com.limelight.utils.UiHelper; import com.limelight.utils.UiHelper;
import android.app.Activity; import android.app.Activity;
import android.app.AlertDialog;
import android.app.Service;
import android.content.ComponentName;
import android.content.DialogInterface;
import android.content.Intent; import android.content.Intent;
import android.content.ServiceConnection;
import android.content.res.Configuration;
import android.os.Bundle; import android.os.Bundle;
import android.os.IBinder;
import android.view.ContextMenu; import android.view.ContextMenu;
import android.view.Menu; import android.view.Menu;
import android.view.MenuItem; import android.view.MenuItem;
import android.view.View; import android.view.View;
import android.view.ContextMenu.ContextMenuInfo; import android.view.ContextMenu.ContextMenuInfo;
import android.widget.AbsListView;
import android.widget.AdapterView; import android.widget.AdapterView;
import android.widget.AdapterView.OnItemClickListener; import android.widget.AdapterView.OnItemClickListener;
import android.widget.GridView;
import android.widget.TextView; import android.widget.TextView;
import android.widget.Toast; import android.widget.Toast;
import android.widget.AdapterView.AdapterContextMenuInfo; import android.widget.AdapterView.AdapterContextMenuInfo;
public class AppView extends Activity { public class AppView extends Activity implements AdapterFragmentCallbacks {
private AppGridAdapter appGridAdapter; private AppGridAdapter appGridAdapter;
private InetAddress ipAddress; private String uuidString;
private String uniqueId;
private boolean remote;
private boolean firstLoad = true;
private final static int RESUME_ID = 1; private ComputerDetails computer;
private ComputerManagerService.ApplistPoller poller;
private SpinnerDialog blockingLoadSpinner;
private String lastRawApplist;
private int consecutiveAppListFailures = 0;
private final static int CONSECUTIVE_FAILURE_LIMIT = 3;
private final static int START_OR_RESUME_ID = 1;
private final static int QUIT_ID = 2; private final static int QUIT_ID = 2;
private final static int CANCEL_ID = 3; private final static int CANCEL_ID = 3;
private final static int START_WTIH_QUIT = 4;
public final static String ADDRESS_EXTRA = "Address";
public final static String UNIQUEID_EXTRA = "UniqueId";
public final static String NAME_EXTRA = "Name"; public final static String NAME_EXTRA = "Name";
public final static String REMOTE_EXTRA = "Remote"; public final static String UUID_EXTRA = "UUID";
private ComputerManagerService.ComputerManagerBinder managerBinder;
private final ServiceConnection serviceConnection = new ServiceConnection() {
public void onServiceConnected(ComponentName className, IBinder binder) {
final ComputerManagerService.ComputerManagerBinder localBinder =
((ComputerManagerService.ComputerManagerBinder)binder);
// Wait in a separate thread to avoid stalling the UI
new Thread() {
@Override
public void run() {
// Wait for the binder to be ready
localBinder.waitForReady();
// Now make the binder visible
managerBinder = localBinder;
// Get the computer object
computer = managerBinder.getComputer(UUID.fromString(uuidString));
try {
appGridAdapter = new AppGridAdapter(AppView.this,
PreferenceConfiguration.readPreferences(AppView.this).listMode,
PreferenceConfiguration.readPreferences(AppView.this).smallIconMode,
computer, managerBinder.getUniqueId());
} catch (Exception e) {
e.printStackTrace();
finish();
return;
}
// Start updates
startComputerUpdates();
// Load the app grid with cached data (if possible)
populateAppGridWithCache();
getFragmentManager().beginTransaction()
.replace(R.id.appFragmentContainer, new AdapterFragment())
.commitAllowingStateLoss();
}
}.start();
}
public void onServiceDisconnected(ComponentName className) {
managerBinder = null;
}
};
private InetAddress getAddress() {
return computer.reachability == ComputerDetails.Reachability.LOCAL ?
computer.localIp : computer.remoteIp;
}
private void startComputerUpdates() {
if (managerBinder == null) {
return;
}
managerBinder.startPolling(new ComputerManagerListener() {
@Override
public void notifyComputerUpdated(ComputerDetails details) {
// Don't care about other computers
if (details != computer) {
return;
}
if (details.state != ComputerDetails.State.ONLINE) {
consecutiveAppListFailures++;
if (consecutiveAppListFailures >= CONSECUTIVE_FAILURE_LIMIT) {
// The PC is unreachable now
AppView.this.runOnUiThread(new Runnable() {
@Override
public void run() {
// Display a toast to the user and quit the activity
Toast.makeText(AppView.this, getResources().getText(R.string.lost_connection), Toast.LENGTH_SHORT).show();
finish();
}
});
}
return;
}
consecutiveAppListFailures = 0;
// App list is the same or empty; nothing to do
if (details.rawAppList == null || details.rawAppList.equals(lastRawApplist)) {
return;
}
try {
lastRawApplist = details.rawAppList;
updateUiWithAppList(NvHTTP.getAppListByReader(new StringReader(details.rawAppList)));
if (blockingLoadSpinner != null) {
blockingLoadSpinner.dismiss();
blockingLoadSpinner = null;
}
} catch (Exception ignored) {}
}
});
if (poller == null) {
poller = managerBinder.createAppListPoller(computer);
}
poller.start();
}
private void stopComputerUpdates() {
if (poller != null) {
poller.stop();
}
if (managerBinder != null) {
managerBinder.stopPolling();
}
}
@Override @Override
protected void onCreate(Bundle savedInstanceState) { protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
String locale = PreferenceConfiguration.readPreferences(this).language;
if (!locale.equals(PreferenceConfiguration.DEFAULT_LANGUAGE)) {
Configuration config = new Configuration(getResources().getConfiguration());
config.locale = new Locale(locale);
getResources().updateConfiguration(config, getResources().getDisplayMetrics());
}
setContentView(R.layout.activity_app_view); setContentView(R.layout.activity_app_view);
UiHelper.notifyNewRootView(this); UiHelper.notifyNewRootView(this);
byte[] address = getIntent().getByteArrayExtra(ADDRESS_EXTRA); uuidString = getIntent().getStringExtra(UUID_EXTRA);
uniqueId = getIntent().getStringExtra(UNIQUEID_EXTRA);
remote = getIntent().getBooleanExtra(REMOTE_EXTRA, false);
if (address == null || uniqueId == null) {
finish();
return;
}
String labelText = getResources().getString(R.string.title_applist)+" "+getIntent().getStringExtra(NAME_EXTRA); String labelText = getResources().getString(R.string.title_applist)+" "+getIntent().getStringExtra(NAME_EXTRA);
TextView label = (TextView) findViewById(R.id.appListText); TextView label = (TextView) findViewById(R.id.appListText);
setTitle(labelText); setTitle(labelText);
label.setText(labelText); label.setText(labelText);
try { // Bind to the computer manager service
ipAddress = InetAddress.getByAddress(address); bindService(new Intent(this, ComputerManagerService.class), serviceConnection,
} catch (UnknownHostException e) { Service.BIND_AUTO_CREATE);
e.printStackTrace();
finish();
return;
} }
// Setup the list view private void populateAppGridWithCache() {
GridView appGrid = (GridView) findViewById(R.id.appGridView);
try { try {
appGridAdapter = new AppGridAdapter(this, ipAddress, uniqueId); // Try to load from cache
lastRawApplist = CacheHelper.readInputStreamToString(CacheHelper.openCacheFileForInput(getCacheDir(), "applist", uuidString));
List<NvApp> applist = NvHTTP.getAppListByReader(new StringReader(lastRawApplist));
updateUiWithAppList(applist);
LimeLog.info("Loaded applist from cache");
} catch (Exception e) { } catch (Exception e) {
if (lastRawApplist != null) {
LimeLog.warning("Saved applist corrupted: "+lastRawApplist);
e.printStackTrace(); e.printStackTrace();
finish();
return;
} }
appGrid.setAdapter(appGridAdapter); LimeLog.info("Loading applist from the network");
appGrid.setOnItemClickListener(new OnItemClickListener() { // We'll need to load from the network
@Override loadAppsBlocking();
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 private void loadAppsBlocking() {
if (getRunningAppId() != -1) { blockingLoadSpinner = SpinnerDialog.displayDialog(this, getResources().getString(R.string.applist_refresh_title),
openContextMenu(arg1); getResources().getString(R.string.applist_refresh_msg), true);
} else {
doStart(app.app);
}
}
});
registerForContextMenu(appGrid);
} }
@Override @Override
@@ -112,17 +240,24 @@ public class AppView extends Activity {
SpinnerDialog.closeDialogs(this); SpinnerDialog.closeDialogs(this);
Dialog.closeDialogs(); Dialog.closeDialogs();
if (managerBinder != null) {
unbindService(serviceConnection);
}
} }
@Override @Override
protected void onResume() { protected void onResume() {
super.onResume(); super.onResume();
// Display the error message if it was the startComputerUpdates();
// first load, but just kill the activity }
// on subsequent errors
updateAppList(firstLoad); @Override
firstLoad = false; protected void onPause() {
super.onPause();
stopComputerUpdates();
} }
private int getRunningAppId() { private int getRunningAppId() {
@@ -154,11 +289,11 @@ public class AppView extends Activity {
int runningAppId = getRunningAppId(); int runningAppId = getRunningAppId();
if (runningAppId != -1) { if (runningAppId != -1) {
if (runningAppId == selectedApp.app.getAppId()) { if (runningAppId == selectedApp.app.getAppId()) {
menu.add(Menu.NONE, RESUME_ID, 1, getResources().getString(R.string.applist_menu_resume)); menu.add(Menu.NONE, START_OR_RESUME_ID, 1, getResources().getString(R.string.applist_menu_resume));
menu.add(Menu.NONE, QUIT_ID, 2, getResources().getString(R.string.applist_menu_quit)); menu.add(Menu.NONE, QUIT_ID, 2, getResources().getString(R.string.applist_menu_quit));
} }
else { else {
menu.add(Menu.NONE, RESUME_ID, 1, getResources().getString(R.string.applist_menu_quit_and_start)); menu.add(Menu.NONE, START_WTIH_QUIT, 1, getResources().getString(R.string.applist_menu_quit_and_start));
menu.add(Menu.NONE, CANCEL_ID, 2, getResources().getString(R.string.applist_menu_cancel)); menu.add(Menu.NONE, CANCEL_ID, 2, getResources().getString(R.string.applist_menu_cancel));
} }
} }
@@ -168,18 +303,61 @@ public class AppView extends Activity {
public void onContextMenuClosed(Menu menu) { public void onContextMenuClosed(Menu menu) {
} }
private void displayQuitConfirmationDialog(final Runnable onYes, final Runnable onNo) {
DialogInterface.OnClickListener dialogClickListener = new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
switch (which){
case DialogInterface.BUTTON_POSITIVE:
if (onYes != null) {
onYes.run();
}
break;
case DialogInterface.BUTTON_NEGATIVE:
if (onNo != null) {
onNo.run();
}
break;
}
}
};
AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setMessage(getResources().getString(R.string.applist_quit_confirmation))
.setPositiveButton(getResources().getString(R.string.yes), dialogClickListener)
.setNegativeButton(getResources().getString(R.string.no), dialogClickListener)
.show();
}
@Override @Override
public boolean onContextItemSelected(MenuItem item) { public boolean onContextItemSelected(MenuItem item) {
AdapterContextMenuInfo info = (AdapterContextMenuInfo) item.getMenuInfo(); AdapterContextMenuInfo info = (AdapterContextMenuInfo) item.getMenuInfo();
AppObject app = (AppObject) appGridAdapter.getItem(info.position); final AppObject app = (AppObject) appGridAdapter.getItem(info.position);
switch (item.getItemId()) { switch (item.getItemId()) {
case RESUME_ID: case START_WTIH_QUIT:
// Display a confirmation dialog first
displayQuitConfirmationDialog(new Runnable() {
@Override
public void run() {
doStart(app.app);
}
}, null);
return true;
case START_OR_RESUME_ID:
// Resume is the same as start for us // Resume is the same as start for us
doStart(app.app); doStart(app.app);
return true; return true;
case QUIT_ID: case QUIT_ID:
// Display a confirmation dialog first
displayQuitConfirmationDialog(new Runnable() {
@Override
public void run() {
doQuit(app.app); doQuit(app.app);
}
}, null);
return true; return true;
case CANCEL_ID: case CANCEL_ID:
@@ -190,61 +368,61 @@ public class AppView extends Activity {
} }
} }
private void updateAppList(final boolean displayError) { private void updateUiWithAppList(final List<NvApp> appList) {
final SpinnerDialog spinner = SpinnerDialog.displayDialog(this, getResources().getString(R.string.applist_refresh_title),
getResources().getString(R.string.applist_refresh_msg), true);
new Thread() {
@Override
public void run() {
NvHTTP httpConn = new NvHTTP(ipAddress, uniqueId, null, PlatformBinding.getCryptoProvider(AppView.this));
try {
final List<NvApp> appList = httpConn.getAppList();
AppView.this.runOnUiThread(new Runnable() { AppView.this.runOnUiThread(new Runnable() {
@Override @Override
public void run() { public void run() {
appGridAdapter.clear(); boolean updated = false;
for (NvApp app : appList) { for (NvApp app : appList) {
appGridAdapter.addApp(new AppObject(app)); 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());
updated = true;
}
if (!existingApp.app.getAppName().equals(app.getAppName())) {
existingApp.app.setAppName(app.getAppName());
updated = true;
}
foundExistingApp = true;
break;
}
}
if (!foundExistingApp) {
// This app must be new
appGridAdapter.addApp(new AppObject(app));
updated = true;
}
}
if (updated) {
appGridAdapter.notifyDataSetChanged(); appGridAdapter.notifyDataSetChanged();
} }
});
// Success case
return;
} catch (GfeHttpResponseException ignored) {
} catch (IOException ignored) {
} catch (XmlPullParserException ignored) {
} finally {
spinner.dismiss();
}
if (displayError) {
Dialog.displayDialog(AppView.this, getResources().getString(R.string.applist_refresh_error_title),
getResources().getString(R.string.applist_refresh_error_msg), true);
}
else {
// Just finish the activity immediately
AppView.this.runOnUiThread(new Runnable() {
@Override
public void run() {
finish();
} }
}); });
} }
}
}.start();
}
private void doStart(NvApp app) { private void doStart(NvApp app) {
Intent intent = new Intent(this, Game.class); Intent intent = new Intent(this, Game.class);
intent.putExtra(Game.EXTRA_HOST, ipAddress.getHostAddress()); 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_APP, app.getAppName());
intent.putExtra(Game.EXTRA_UNIQUEID, uniqueId); intent.putExtra(Game.EXTRA_UNIQUEID, managerBinder.getUniqueId());
intent.putExtra(Game.EXTRA_STREAMING_REMOTE, remote); intent.putExtra(Game.EXTRA_STREAMING_REMOTE,
computer.reachability != ComputerDetails.Reachability.LOCAL);
startActivity(intent); startActivity(intent);
} }
@@ -256,20 +434,25 @@ public class AppView extends Activity {
NvHTTP httpConn; NvHTTP httpConn;
String message; String message;
try { try {
httpConn = new NvHTTP(ipAddress, uniqueId, null, PlatformBinding.getCryptoProvider(AppView.this)); httpConn = new NvHTTP(getAddress(),
managerBinder.getUniqueId(), null, PlatformBinding.getCryptoProvider(AppView.this));
if (httpConn.quitApp()) { if (httpConn.quitApp()) {
message = getResources().getString(R.string.applist_quit_success)+" "+app.getAppName(); message = getResources().getString(R.string.applist_quit_success)+" "+app.getAppName();
} }
else { else {
message = getResources().getString(R.string.applist_quit_fail)+" "+app.getAppName(); message = getResources().getString(R.string.applist_quit_fail)+" "+app.getAppName();
} }
updateAppList(true);
} catch (UnknownHostException e) { } catch (UnknownHostException e) {
message = getResources().getString(R.string.error_unknown_host); message = getResources().getString(R.string.error_unknown_host);
} catch (FileNotFoundException e) { } catch (FileNotFoundException e) {
message = getResources().getString(R.string.error_404); message = getResources().getString(R.string.error_404);
} catch (Exception e) { } catch (Exception e) {
message = e.getMessage(); message = e.getMessage();
} finally {
// Trigger a poll immediately
if (poller != null) {
poller.pollNow();
}
} }
final String toastMessage = message; final String toastMessage = message;
@@ -283,8 +466,39 @@ public class AppView extends Activity {
}).start(); }).start();
} }
@Override
public int getAdapterFragmentLayoutId() {
return PreferenceConfiguration.readPreferences(this).listMode ?
R.layout.list_view : (PreferenceConfiguration.readPreferences(AppView.this).smallIconMode ?
R.layout.app_grid_view_small : R.layout.app_grid_view);
}
@Override
public void receiveAbsListView(AbsListView listView) {
listView.setAdapter(appGridAdapter);
listView.setOnItemClickListener(new OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> arg0, View arg1, int pos,
long id) {
AppObject app = (AppObject) appGridAdapter.getItem(pos);
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);
}
}
});
registerForContextMenu(listView);
listView.requestFocus();
}
public class AppObject { public class AppObject {
public NvApp app; public final NvApp app;
public AppObject(NvApp app) { public AppObject(NvApp app) {
this.app = app; this.app = app;
+62 -8
View File
@@ -16,18 +16,22 @@ import com.limelight.nvstream.av.video.VideoDecoderRenderer;
import com.limelight.nvstream.input.KeyboardPacket; import com.limelight.nvstream.input.KeyboardPacket;
import com.limelight.nvstream.input.MouseButtonPacket; import com.limelight.nvstream.input.MouseButtonPacket;
import com.limelight.preferences.PreferenceConfiguration; import com.limelight.preferences.PreferenceConfiguration;
import com.limelight.ui.GameGestures;
import com.limelight.utils.Dialog; import com.limelight.utils.Dialog;
import com.limelight.utils.SpinnerDialog; import com.limelight.utils.SpinnerDialog;
import android.annotation.SuppressLint; import android.annotation.SuppressLint;
import android.app.Activity; import android.app.Activity;
import android.content.Context; import android.content.Context;
import android.content.res.Configuration;
import android.graphics.Point; import android.graphics.Point;
import android.hardware.input.InputManager;
import android.media.AudioManager; import android.media.AudioManager;
import android.net.ConnectivityManager; import android.net.ConnectivityManager;
import android.net.wifi.WifiManager; import android.net.wifi.WifiManager;
import android.os.Bundle; import android.os.Bundle;
import android.os.Handler; import android.os.Handler;
import android.os.SystemClock;
import android.view.Display; import android.view.Display;
import android.view.InputDevice; import android.view.InputDevice;
import android.view.KeyEvent; import android.view.KeyEvent;
@@ -41,25 +45,31 @@ import android.view.View.OnTouchListener;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.view.Window; import android.view.Window;
import android.view.WindowManager; import android.view.WindowManager;
import android.view.inputmethod.InputMethodManager;
import android.widget.Toast; import android.widget.Toast;
import java.util.Locale;
public class Game extends Activity implements SurfaceHolder.Callback, public class Game extends Activity implements SurfaceHolder.Callback,
OnGenericMotionListener, OnTouchListener, NvConnectionListener, EvdevListener, OnGenericMotionListener, OnTouchListener, NvConnectionListener, EvdevListener,
OnSystemUiVisibilityChangeListener OnSystemUiVisibilityChangeListener, GameGestures
{ {
private int lastMouseX = Integer.MIN_VALUE; private int lastMouseX = Integer.MIN_VALUE;
private int lastMouseY = Integer.MIN_VALUE; private int lastMouseY = Integer.MIN_VALUE;
private int lastButtonState = 0; private int lastButtonState = 0;
// Only 2 touches are supported // 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;
private ControllerHandler controllerHandler; private ControllerHandler controllerHandler;
private KeyboardTranslator keybTranslator; private KeyboardTranslator keybTranslator;
private PreferenceConfiguration prefConfig; private PreferenceConfiguration prefConfig;
private Point screenSize = new Point(0, 0); private final Point screenSize = new Point(0, 0);
private NvConnection conn; private NvConnection conn;
private SpinnerDialog spinner; private SpinnerDialog spinner;
@@ -87,6 +97,13 @@ public class Game extends Activity implements SurfaceHolder.Callback,
protected void onCreate(Bundle savedInstanceState) { protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
String locale = PreferenceConfiguration.readPreferences(this).language;
if (!locale.equals(PreferenceConfiguration.DEFAULT_LANGUAGE)) {
Configuration config = new Configuration(getResources().getConfiguration());
config.locale = new Locale(locale);
getResources().updateConfiguration(config, getResources().getDisplayMetrics());
}
// We don't want a title bar // We don't want a title bar
requestWindowFeature(Window.FEATURE_NO_TITLE); requestWindowFeature(Window.FEATURE_NO_TITLE);
@@ -170,13 +187,17 @@ public class Game extends Activity implements SurfaceHolder.Callback,
.enableAdaptiveResolution((decoderRenderer.getCapabilities() & .enableAdaptiveResolution((decoderRenderer.getCapabilities() &
VideoDecoderRenderer.CAPABILITY_ADAPTIVE_RESOLUTION) != 0) VideoDecoderRenderer.CAPABILITY_ADAPTIVE_RESOLUTION) != 0)
.enableLocalAudioPlayback(prefConfig.playHostAudio) .enableLocalAudioPlayback(prefConfig.playHostAudio)
.setMaxPacketSize(remote ? 1024 : 1392) .setMaxPacketSize(remote ? 1024 : 1292)
.setRemote(remote)
.build(); .build();
// Initialize the connection // Initialize the connection
conn = new NvConnection(host, uniqueId, Game.this, config, PlatformBinding.getCryptoProvider(this)); conn = new NvConnection(host, uniqueId, Game.this, config, PlatformBinding.getCryptoProvider(this));
keybTranslator = new KeyboardTranslator(conn); keybTranslator = new KeyboardTranslator(conn);
controllerHandler = new ControllerHandler(conn, 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(); SurfaceHolder sh = sv.getHolder();
if (prefConfig.stretchVideo || !decoderRenderer.isHardwareAccelerated()) { if (prefConfig.stretchVideo || !decoderRenderer.isHardwareAccelerated()) {
@@ -225,7 +246,7 @@ public class Game extends Activity implements SurfaceHolder.Callback,
} }
@SuppressLint("InlinedApi") @SuppressLint("InlinedApi")
private Runnable hideSystemUi = new Runnable() { private final Runnable hideSystemUi = new Runnable() {
@Override @Override
public void run() { public void run() {
// Use immersive mode on 4.4+ or standard low profile on previous builds // Use immersive mode on 4.4+ or standard low profile on previous builds
@@ -261,6 +282,9 @@ public class Game extends Activity implements SurfaceHolder.Callback,
SpinnerDialog.closeDialogs(this); SpinnerDialog.closeDialogs(this);
Dialog.closeDialogs(); Dialog.closeDialogs();
InputManager inputManager = (InputManager) getSystemService(Context.INPUT_SERVICE);
inputManager.unregisterInputDeviceListener(controllerHandler);
displayedFailureDialog = true; displayedFailureDialog = true;
stopConnection(); stopConnection();
@@ -291,7 +315,7 @@ public class Game extends Activity implements SurfaceHolder.Callback,
wifiLock.release(); wifiLock.release();
} }
private Runnable toggleGrab = new Runnable() { private final Runnable toggleGrab = new Runnable() {
@Override @Override
public void run() { public void run() {
@@ -467,6 +491,13 @@ public class Game extends Activity implements SurfaceHolder.Callback,
} }
} }
@Override
public void showKeyboard() {
LimeLog.info("Showing keyboard overlay");
InputMethodManager inputManager = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
inputManager.toggleSoftInput(InputMethodManager.SHOW_FORCED, InputMethodManager.HIDE_IMPLICIT_ONLY);
}
// Returns true if the event was consumed // Returns true if the event was consumed
private boolean handleMotionEvent(MotionEvent event) { private boolean handleMotionEvent(MotionEvent event) {
// Pass through keyboard input if we're not grabbing // Pass through keyboard input if we're not grabbing
@@ -490,6 +521,21 @@ public class Game extends Activity implements SurfaceHolder.Callback,
int eventX = (int)event.getX(actionIndex); int eventX = (int)event.getX(actionIndex);
int eventY = (int)event.getY(actionIndex); int eventY = (int)event.getY(actionIndex);
// Special handling for 3 finger gesture
if (event.getActionMasked() == MotionEvent.ACTION_POINTER_DOWN &&
event.getPointerCount() == 3) {
// Three fingers down
threeFingerDownTime = SystemClock.uptimeMillis();
// Cancel the first and second touches to avoid
// erroneous events
for (TouchContext aTouchContext : touchContextMap) {
aTouchContext.cancelTouch();
}
return true;
}
TouchContext context = getTouchContext(actionIndex); TouchContext context = getTouchContext(actionIndex);
if (context == null) { if (context == null) {
return false; return false;
@@ -503,8 +549,16 @@ public class Game extends Activity implements SurfaceHolder.Callback,
break; break;
case MotionEvent.ACTION_POINTER_UP: case MotionEvent.ACTION_POINTER_UP:
case MotionEvent.ACTION_UP: case MotionEvent.ACTION_UP:
if (event.getPointerCount() == 1) {
// All fingers up
if (SystemClock.uptimeMillis() - threeFingerDownTime < THREE_FINGER_TAP_THRESHOLD) {
// This is a 3 finger tap to bring up the keyboard
showKeyboard();
return true;
}
}
context.touchUpEvent(eventX, eventY); context.touchUpEvent(eventX, eventY);
if (actionIndex == 0 && event.getPointerCount() > 1) { if (actionIndex == 0 && event.getPointerCount() > 1 && !context.isCancelled()) {
// The original secondary touch now becomes primary // The original secondary touch now becomes primary
context.touchDownEvent((int)event.getX(1), (int)event.getY(1)); context.touchDownEvent((int)event.getX(1), (int)event.getY(1));
} }
+59 -37
View File
@@ -4,6 +4,7 @@ import java.io.FileNotFoundException;
import java.io.IOException; import java.io.IOException;
import java.net.InetAddress; import java.net.InetAddress;
import java.net.UnknownHostException; import java.net.UnknownHostException;
import java.util.Locale;
import com.limelight.binding.PlatformBinding; import com.limelight.binding.PlatformBinding;
import com.limelight.binding.crypto.AndroidCryptoProvider; import com.limelight.binding.crypto.AndroidCryptoProvider;
@@ -16,7 +17,10 @@ import com.limelight.nvstream.http.PairingManager;
import com.limelight.nvstream.http.PairingManager.PairState; import com.limelight.nvstream.http.PairingManager.PairState;
import com.limelight.nvstream.wol.WakeOnLanSender; import com.limelight.nvstream.wol.WakeOnLanSender;
import com.limelight.preferences.AddComputerManually; import com.limelight.preferences.AddComputerManually;
import com.limelight.preferences.PreferenceConfiguration;
import com.limelight.preferences.StreamSettings; import com.limelight.preferences.StreamSettings;
import com.limelight.ui.AdapterFragment;
import com.limelight.ui.AdapterFragmentCallbacks;
import com.limelight.utils.Dialog; import com.limelight.utils.Dialog;
import com.limelight.utils.UiHelper; import com.limelight.utils.UiHelper;
@@ -35,20 +39,20 @@ import android.view.MenuItem;
import android.view.View; import android.view.View;
import android.view.ContextMenu.ContextMenuInfo; import android.view.ContextMenu.ContextMenuInfo;
import android.view.View.OnClickListener; import android.view.View.OnClickListener;
import android.widget.AbsListView;
import android.widget.AdapterView; import android.widget.AdapterView;
import android.widget.AdapterView.OnItemClickListener; import android.widget.AdapterView.OnItemClickListener;
import android.widget.GridView;
import android.widget.ImageButton; import android.widget.ImageButton;
import android.widget.RelativeLayout; import android.widget.RelativeLayout;
import android.widget.Toast; import android.widget.Toast;
import android.widget.AdapterView.AdapterContextMenuInfo; import android.widget.AdapterView.AdapterContextMenuInfo;
public class PcView extends Activity { public class PcView extends Activity implements AdapterFragmentCallbacks {
private RelativeLayout noPcFoundLayout; private RelativeLayout noPcFoundLayout;
private PcGridAdapter pcGridAdapter; private PcGridAdapter pcGridAdapter;
private ComputerManagerService.ComputerManagerBinder managerBinder; private ComputerManagerService.ComputerManagerBinder managerBinder;
private boolean freezeUpdates, runningPolling; private boolean freezeUpdates, runningPolling;
private ServiceConnection serviceConnection = new ServiceConnection() { private final ServiceConnection serviceConnection = new ServiceConnection() {
public void onServiceConnected(ComponentName className, IBinder binder) { public void onServiceConnected(ComponentName className, IBinder binder) {
final ComputerManagerService.ComputerManagerBinder localBinder = final ComputerManagerService.ComputerManagerBinder localBinder =
((ComputerManagerService.ComputerManagerBinder)binder); ((ComputerManagerService.ComputerManagerBinder)binder);
@@ -102,27 +106,6 @@ public class PcView extends Activity {
ImageButton settingsButton = (ImageButton) findViewById(R.id.settingsButton); ImageButton settingsButton = (ImageButton) findViewById(R.id.settingsButton);
ImageButton addComputerButton = (ImageButton) findViewById(R.id.manuallyAddPc); ImageButton addComputerButton = (ImageButton) findViewById(R.id.manuallyAddPc);
GridView pcGrid = (GridView) findViewById(R.id.pcGridView);
pcGrid.setAdapter(pcGridAdapter);
pcGrid.setOnItemClickListener(new OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> arg0, View arg1, int pos,
long id) {
ComputerObject computer = (ComputerObject) pcGridAdapter.getItem(pos);
if (computer.details.reachability == ComputerDetails.Reachability.UNKNOWN) {
// Do nothing
} else if (computer.details.reachability == ComputerDetails.Reachability.OFFLINE) {
// Open the context menu if a PC is offline
openContextMenu(arg1);
} else if (computer.details.pairState != PairState.PAIRED) {
// Pair an unpaired machine by default
doPair(computer.details);
} else {
doAppList(computer.details);
}
}
});
registerForContextMenu(pcGrid);
settingsButton.setOnClickListener(new OnClickListener() { settingsButton.setOnClickListener(new OnClickListener() {
@Override @Override
public void onClick(View v) { public void onClick(View v) {
@@ -137,6 +120,10 @@ public class PcView extends Activity {
} }
}); });
getFragmentManager().beginTransaction()
.replace(R.id.pcFragmentContainer, new AdapterFragment())
.commitAllowingStateLoss();
noPcFoundLayout = (RelativeLayout) findViewById(R.id.no_pc_found_layout); noPcFoundLayout = (RelativeLayout) findViewById(R.id.no_pc_found_layout);
if (pcGridAdapter.getCount() == 0) { if (pcGridAdapter.getCount() == 0) {
noPcFoundLayout.setVisibility(View.VISIBLE); noPcFoundLayout.setVisibility(View.VISIBLE);
@@ -151,11 +138,20 @@ public class PcView extends Activity {
protected void onCreate(Bundle savedInstanceState) { protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
String locale = PreferenceConfiguration.readPreferences(this).language;
if (!locale.equals(PreferenceConfiguration.DEFAULT_LANGUAGE)) {
Configuration config = new Configuration(getResources().getConfiguration());
config.locale = new Locale(locale);
getResources().updateConfiguration(config, getResources().getDisplayMetrics());
}
// Bind to the computer manager service // Bind to the computer manager service
bindService(new Intent(PcView.this, ComputerManagerService.class), serviceConnection, bindService(new Intent(PcView.this, ComputerManagerService.class), serviceConnection,
Service.BIND_AUTO_CREATE); Service.BIND_AUTO_CREATE);
pcGridAdapter = new PcGridAdapter(this); pcGridAdapter = new PcGridAdapter(this,
PreferenceConfiguration.readPreferences(this).listMode,
PreferenceConfiguration.readPreferences(this).smallIconMode);
initializeViews(); initializeViews();
} }
@@ -258,7 +254,10 @@ public class PcView extends Activity {
} }
else { else {
menu.add(Menu.NONE, APP_LIST_ID, 1, getResources().getString(R.string.pcview_menu_app_list)); 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));
// 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, 2, getResources().getString(R.string.pcview_menu_delete_pc));
} }
} }
@@ -472,16 +471,7 @@ public class PcView extends Activity {
Intent i = new Intent(this, AppView.class); Intent i = new Intent(this, AppView.class);
i.putExtra(AppView.NAME_EXTRA, computer.name); i.putExtra(AppView.NAME_EXTRA, computer.name);
i.putExtra(AppView.UNIQUEID_EXTRA, managerBinder.getUniqueId()); i.putExtra(AppView.UUID_EXTRA, computer.uuid.toString());
if (computer.reachability == ComputerDetails.Reachability.LOCAL) {
i.putExtra(AppView.ADDRESS_EXTRA, computer.localIp.getAddress());
i.putExtra(AppView.REMOTE_EXTRA, false);
}
else {
i.putExtra(AppView.ADDRESS_EXTRA, computer.remoteIp.getAddress());
i.putExtra(AppView.REMOTE_EXTRA, true);
}
startActivity(i); startActivity(i);
} }
@@ -527,6 +517,7 @@ public class PcView extends Activity {
if (details.equals(computer.details)) { if (details.equals(computer.details)) {
pcGridAdapter.removeComputer(computer); pcGridAdapter.removeComputer(computer);
pcGridAdapter.notifyDataSetChanged();
break; break;
} }
} }
@@ -539,7 +530,7 @@ public class PcView extends Activity {
ComputerObject computer = (ComputerObject) pcGridAdapter.getItem(i); ComputerObject computer = (ComputerObject) pcGridAdapter.getItem(i);
// Check if this is the same computer // Check if this is the same computer
if (details.equals(computer.details)) { if (details.uuid.equals(computer.details.uuid)) {
existingEntry = computer; existingEntry = computer;
break; break;
} }
@@ -561,6 +552,37 @@ public class PcView extends Activity {
pcGridAdapter.notifyDataSetChanged(); pcGridAdapter.notifyDataSetChanged();
} }
@Override
public int getAdapterFragmentLayoutId() {
return PreferenceConfiguration.readPreferences(this).listMode ?
R.layout.list_view : (PreferenceConfiguration.readPreferences(this).smallIconMode ?
R.layout.pc_grid_view_small : R.layout.pc_grid_view);
}
@Override
public void receiveAbsListView(AbsListView listView) {
listView.setAdapter(pcGridAdapter);
listView.setOnItemClickListener(new OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> arg0, View arg1, int pos,
long id) {
ComputerObject computer = (ComputerObject) pcGridAdapter.getItem(pos);
if (computer.details.reachability == ComputerDetails.Reachability.UNKNOWN) {
// Do nothing
} else if (computer.details.reachability == ComputerDetails.Reachability.OFFLINE) {
// Open the context menu if a PC is offline
openContextMenu(arg1);
} else if (computer.details.pairState != PairState.PAIRED) {
// Pair an unpaired machine by default
doPair(computer.details);
} else {
doAppList(computer.details);
}
}
});
registerForContextMenu(listView);
}
public class ComputerObject { public class ComputerObject {
public ComputerDetails details; public ComputerDetails details;
@@ -9,7 +9,7 @@ import com.limelight.nvstream.av.audio.AudioRenderer;
public class AndroidAudioRenderer implements AudioRenderer { public class AndroidAudioRenderer implements AudioRenderer {
public static final int FRAME_SIZE = 960; private static final int FRAME_SIZE = 960;
private AudioTrack track; private AudioTrack track;
@@ -45,8 +45,8 @@ import com.limelight.nvstream.http.LimelightCryptoProvider;
public class AndroidCryptoProvider implements LimelightCryptoProvider { public class AndroidCryptoProvider implements LimelightCryptoProvider {
private File certFile; private final File certFile;
private File keyFile; private final File keyFile;
private X509Certificate cert; private X509Certificate cert;
private RSAPrivateKey key; private RSAPrivateKey key;
@@ -1,7 +1,9 @@
package com.limelight.binding.input; package com.limelight.binding.input;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map;
import android.hardware.input.InputManager;
import android.os.SystemClock; import android.os.SystemClock;
import android.view.InputDevice; import android.view.InputDevice;
import android.view.KeyEvent; import android.view.KeyEvent;
@@ -10,27 +12,15 @@ import android.view.MotionEvent;
import com.limelight.LimeLog; import com.limelight.LimeLog;
import com.limelight.nvstream.NvConnection; import com.limelight.nvstream.NvConnection;
import com.limelight.nvstream.input.ControllerPacket; import com.limelight.nvstream.input.ControllerPacket;
import com.limelight.ui.GameGestures;
import com.limelight.utils.Vector2d; import com.limelight.utils.Vector2d;
public class ControllerHandler { public class ControllerHandler implements InputManager.InputDeviceListener {
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;
// 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 static final int MAXIMUM_BUMPER_UP_DELAY_MS = 100;
private static final int START_DOWN_TIME_KEYB_MS = 750;
private static final int MINIMUM_BUTTON_DOWN_TIME_MS = 25; private static final int MINIMUM_BUTTON_DOWN_TIME_MS = 25;
private static final int EMULATING_SPECIAL = 0x1; private static final int EMULATING_SPECIAL = 0x1;
@@ -39,22 +29,42 @@ public class ControllerHandler {
private static final int EMULATED_SPECIAL_UP_DELAY_MS = 100; private static final int EMULATED_SPECIAL_UP_DELAY_MS = 100;
private static final int EMULATED_SELECT_UP_DELAY_MS = 30; private static final int EMULATED_SELECT_UP_DELAY_MS = 30;
private Vector2d inputVector = new Vector2d(); private final Vector2d inputVector = new Vector2d();
private Vector2d normalizedInputVector = new Vector2d();
private HashMap<String, ControllerMapping> mappings = new HashMap<String, ControllerMapping>(); private final HashMap<String, ControllerContext> contexts = new HashMap<String, ControllerContext>();
private NvConnection conn; private final NvConnection conn;
private double stickDeadzone; private final double stickDeadzone;
private final ControllerMapping defaultMapping = new ControllerMapping(); private final ControllerContext defaultContext = new ControllerContext();
private final GameGestures gestures;
private boolean hasGameController;
public ControllerHandler(NvConnection conn, int deadzonePercentage) { private final boolean multiControllerEnabled;
private short currentControllers;
public ControllerHandler(NvConnection conn, GameGestures gestures, boolean multiControllerEnabled, int deadzonePercentage) {
this.conn = conn; this.conn = conn;
this.gestures = gestures;
this.multiControllerEnabled = multiControllerEnabled;
// HACK: For now we're hardcoding a 10% deadzone. Some deadzone // HACK: For now we're hardcoding a 10% deadzone. Some deadzone
// is required for controller batching support to work. // is required for controller batching support to work.
deadzonePercentage = 10; deadzonePercentage = 10;
int[] ids = InputDevice.getDeviceIds();
for (int i = 0; i < ids.length; i++) {
InputDevice dev = InputDevice.getDevice(ids[i]);
if ((dev.getSources() & InputDevice.SOURCE_JOYSTICK) != 0 ||
(dev.getSources() & InputDevice.SOURCE_GAMEPAD) != 0) {
// This looks like a gamepad, but we'll check X and Y to be sure
if (getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_X) != null &&
getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_Y) != null) {
// This is a gamepad
hasGameController = true;
}
}
}
// 1% is the lowest possible deadzone we support // 1% is the lowest possible deadzone we support
if (deadzonePercentage <= 0) { if (deadzonePercentage <= 0) {
deadzonePercentage = 1; deadzonePercentage = 1;
@@ -62,15 +72,16 @@ public class ControllerHandler {
this.stickDeadzone = (double)deadzonePercentage / 100.0; this.stickDeadzone = (double)deadzonePercentage / 100.0;
// Initialize the default mapping for events with no device // Initialize the default context for events with no device
defaultMapping.leftStickXAxis = MotionEvent.AXIS_X; defaultContext.leftStickXAxis = MotionEvent.AXIS_X;
defaultMapping.leftStickYAxis = MotionEvent.AXIS_Y; defaultContext.leftStickYAxis = MotionEvent.AXIS_Y;
defaultMapping.leftStickDeadzoneRadius = (float) stickDeadzone; defaultContext.leftStickDeadzoneRadius = (float) stickDeadzone;
defaultMapping.rightStickXAxis = MotionEvent.AXIS_Z; defaultContext.rightStickXAxis = MotionEvent.AXIS_Z;
defaultMapping.rightStickYAxis = MotionEvent.AXIS_RZ; defaultContext.rightStickYAxis = MotionEvent.AXIS_RZ;
defaultMapping.rightStickDeadzoneRadius = (float) stickDeadzone; defaultContext.rightStickDeadzoneRadius = (float) stickDeadzone;
defaultMapping.leftTriggerAxis = MotionEvent.AXIS_BRAKE; defaultContext.leftTriggerAxis = MotionEvent.AXIS_BRAKE;
defaultMapping.rightTriggerAxis = MotionEvent.AXIS_GAS; defaultContext.rightTriggerAxis = MotionEvent.AXIS_GAS;
defaultContext.controllerNumber = (short) 0;
} }
private static InputDevice.MotionRange getMotionRangeForJoystickAxis(InputDevice dev, int axis) { private static InputDevice.MotionRange getMotionRangeForJoystickAxis(InputDevice dev, int axis) {
@@ -86,14 +97,64 @@ public class ControllerHandler {
return range; return range;
} }
private ControllerMapping createMappingForDevice(InputDevice dev) { private short assignNewControllerNumber() {
ControllerMapping mapping = new ControllerMapping(); for (short i = 0; i < 4; i++) {
if ((currentControllers & (1 << i)) == 0) {
// Found an unused controller value
currentControllers |= (1 << i);
return i;
}
}
return 0;
}
@Override
public void onInputDeviceAdded(int deviceId) {
// Nothing happening here yet
}
@Override
public void onInputDeviceRemoved(int deviceId) {
for (Map.Entry<String, ControllerContext> device : contexts.entrySet()) {
if (device.getValue().id == deviceId) {
LimeLog.info("Removed controller: "+device.getValue().name);
releaseControllerNumber(device.getValue().controllerNumber);
contexts.remove(device.getKey());
return;
}
}
}
@Override
public void onInputDeviceChanged(int deviceId) {
// Remove and re-add
onInputDeviceRemoved(deviceId);
onInputDeviceAdded(deviceId);
}
private void releaseControllerNumber(int controllerNumber) {
LimeLog.info("Controller number "+controllerNumber+" is now available");
currentControllers &= ~(1 << controllerNumber);
}
private ControllerContext createContextForDevice(InputDevice dev) {
ControllerContext context = new ControllerContext();
String devName = dev.getName(); 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; context.name = devName;
mapping.leftStickYAxis = MotionEvent.AXIS_Y; context.id = dev.getId();
context.leftStickXAxis = MotionEvent.AXIS_X;
context.leftStickYAxis = MotionEvent.AXIS_Y;
if (getMotionRangeForJoystickAxis(dev, context.leftStickXAxis) != null &&
getMotionRangeForJoystickAxis(dev, context.leftStickYAxis) != null) {
// This is a gamepad
hasGameController = true;
context.hasJoystickAxes = true;
}
InputDevice.MotionRange leftTriggerRange = getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_LTRIGGER); InputDevice.MotionRange leftTriggerRange = getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_LTRIGGER);
InputDevice.MotionRange rightTriggerRange = getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_RTRIGGER); InputDevice.MotionRange rightTriggerRange = getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_RTRIGGER);
@@ -102,14 +163,14 @@ public class ControllerHandler {
if (leftTriggerRange != null && rightTriggerRange != null) if (leftTriggerRange != null && rightTriggerRange != null)
{ {
// Some controllers use LTRIGGER and RTRIGGER (like Ouya) // Some controllers use LTRIGGER and RTRIGGER (like Ouya)
mapping.leftTriggerAxis = MotionEvent.AXIS_LTRIGGER; context.leftTriggerAxis = MotionEvent.AXIS_LTRIGGER;
mapping.rightTriggerAxis = MotionEvent.AXIS_RTRIGGER; context.rightTriggerAxis = MotionEvent.AXIS_RTRIGGER;
} }
else if (brakeRange != null && gasRange != null) else if (brakeRange != null && gasRange != null)
{ {
// Others use GAS and BRAKE (like Moga) // Others use GAS and BRAKE (like Moga)
mapping.leftTriggerAxis = MotionEvent.AXIS_BRAKE; context.leftTriggerAxis = MotionEvent.AXIS_BRAKE;
mapping.rightTriggerAxis = MotionEvent.AXIS_GAS; context.rightTriggerAxis = MotionEvent.AXIS_GAS;
} }
else else
{ {
@@ -118,34 +179,34 @@ public class ControllerHandler {
if (rxRange != null && ryRange != null && devName != null) { if (rxRange != null && ryRange != null && devName != null) {
if (devName.contains("Xbox") || devName.contains("XBox") || devName.contains("X-Box")) { if (devName.contains("Xbox") || devName.contains("XBox") || devName.contains("X-Box")) {
// Xbox controllers use RX and RY for right stick // Xbox controllers use RX and RY for right stick
mapping.rightStickXAxis = MotionEvent.AXIS_RX; context.rightStickXAxis = MotionEvent.AXIS_RX;
mapping.rightStickYAxis = MotionEvent.AXIS_RY; context.rightStickYAxis = MotionEvent.AXIS_RY;
// Xbox controllers use Z and RZ for triggers // Xbox controllers use Z and RZ for triggers
mapping.leftTriggerAxis = MotionEvent.AXIS_Z; context.leftTriggerAxis = MotionEvent.AXIS_Z;
mapping.rightTriggerAxis = MotionEvent.AXIS_RZ; context.rightTriggerAxis = MotionEvent.AXIS_RZ;
mapping.triggersIdleNegative = true; context.triggersIdleNegative = true;
mapping.isXboxController = true; context.isXboxController = true;
} }
else { else {
// DS4 controller uses RX and RY for triggers // DS4 controller uses RX and RY for triggers
mapping.leftTriggerAxis = MotionEvent.AXIS_RX; context.leftTriggerAxis = MotionEvent.AXIS_RX;
mapping.rightTriggerAxis = MotionEvent.AXIS_RY; context.rightTriggerAxis = MotionEvent.AXIS_RY;
mapping.triggersIdleNegative = true; 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 zRange = getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_Z);
InputDevice.MotionRange rzRange = getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_RZ); InputDevice.MotionRange rzRange = getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_RZ);
// Most other controllers use Z and RZ for the right stick // Most other controllers use Z and RZ for the right stick
if (zRange != null && rzRange != null) { if (zRange != null && rzRange != null) {
mapping.rightStickXAxis = MotionEvent.AXIS_Z; context.rightStickXAxis = MotionEvent.AXIS_Z;
mapping.rightStickYAxis = MotionEvent.AXIS_RZ; context.rightStickYAxis = MotionEvent.AXIS_RZ;
} }
else { else {
InputDevice.MotionRange rxRange = getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_RX); InputDevice.MotionRange rxRange = getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_RX);
@@ -153,8 +214,8 @@ public class ControllerHandler {
// Try RX and RY now // Try RX and RY now
if (rxRange != null && ryRange != null) { if (rxRange != null && ryRange != null) {
mapping.rightStickXAxis = MotionEvent.AXIS_RX; context.rightStickXAxis = MotionEvent.AXIS_RX;
mapping.rightStickYAxis = MotionEvent.AXIS_RY; context.rightStickYAxis = MotionEvent.AXIS_RY;
} }
} }
} }
@@ -163,78 +224,123 @@ public class ControllerHandler {
InputDevice.MotionRange hatXRange = getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_HAT_X); InputDevice.MotionRange hatXRange = getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_HAT_X);
InputDevice.MotionRange hatYRange = getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_HAT_Y); InputDevice.MotionRange hatYRange = getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_HAT_Y);
if (hatXRange != null && hatYRange != null) { if (hatXRange != null && hatYRange != null) {
mapping.hatXAxis = MotionEvent.AXIS_HAT_X; context.hatXAxis = MotionEvent.AXIS_HAT_X;
mapping.hatYAxis = MotionEvent.AXIS_HAT_Y; context.hatYAxis = MotionEvent.AXIS_HAT_Y;
} }
if (mapping.leftStickXAxis != -1 && mapping.leftStickYAxis != -1) { if (context.leftStickXAxis != -1 && context.leftStickYAxis != -1) {
mapping.leftStickDeadzoneRadius = (float) stickDeadzone; context.leftStickDeadzoneRadius = (float) stickDeadzone;
} }
if (mapping.rightStickXAxis != -1 && mapping.rightStickYAxis != -1) { if (context.rightStickXAxis != -1 && context.rightStickYAxis != -1) {
mapping.rightStickDeadzoneRadius = (float) stickDeadzone; context.rightStickDeadzoneRadius = (float) stickDeadzone;
} }
if (mapping.leftTriggerAxis != -1 && mapping.rightTriggerAxis != -1) { if (context.leftTriggerAxis != -1 && context.rightTriggerAxis != -1) {
InputDevice.MotionRange ltRange = getMotionRangeForJoystickAxis(dev, mapping.leftTriggerAxis); InputDevice.MotionRange ltRange = getMotionRangeForJoystickAxis(dev, context.leftTriggerAxis);
InputDevice.MotionRange rtRange = getMotionRangeForJoystickAxis(dev, mapping.rightTriggerAxis); InputDevice.MotionRange rtRange = getMotionRangeForJoystickAxis(dev, context.rightTriggerAxis);
// It's important to have a valid deadzone so controller packet batching works properly // 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 10% // For triggers without (valid) deadzones, we'll use 13% (around XInput's default)
if (mapping.triggerDeadzone <= 0.02 || if (context.triggerDeadzone < 0.13f ||
mapping.triggerDeadzone > 0.30) context.triggerDeadzone > 0.30f)
{ {
mapping.triggerDeadzone = 0.1f; context.triggerDeadzone = 0.13f;
} }
} }
if (devName != null) {
// For the Nexus Player (and probably other ATV devices), we should // For the Nexus Player (and probably other ATV devices), we should
// use the back button as start since it doesn't have a start/menu button // use the back button as start since it doesn't have a start/menu button
// on the controller // on the controller
if (devName != null && devName.contains("ASUS Gamepad")) { if (devName.contains("ASUS Gamepad")) {
// We can only do this check on KitKat or higher, but it doesn't matter since ATV // We can only do this check on KitKat or higher, but it doesn't matter since ATV
// is Android 5.0 anyway // is Android 5.0 anyway
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.KITKAT) { 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); boolean[] hasStartKey = dev.hasKeys(KeyEvent.KEYCODE_BUTTON_START, KeyEvent.KEYCODE_MENU, 0);
if (!hasStartKey[0] && !hasStartKey[1]) { if (!hasStartKey[0] && !hasStartKey[1]) {
mapping.backIsStart = true; context.backIsStart = true;
} context.modeIsSelect = true;
} }
} }
return mapping; // The ASUS Gamepad has triggers that sit far forward and are prone to false presses
// so we increase the deadzone on them to minimize this
context.triggerDeadzone = 0.30f;
}
// Classify this device as a remote by name
else if (devName.contains("Fire TV Remote") || devName.contains("Nexus Remote")) {
// It's only a remote if it doesn't any sticks
if (!context.hasJoystickAxes) {
context.isRemote = true;
}
}
// NYKO Playpad has a fake hat that mimics the left stick for some reason
else if (devName.contains("NYKO PLAYPAD")) {
context.hatXAxis = -1;
context.hatYAxis = -1;
}
} }
private ControllerMapping getMappingForDevice(InputDevice dev) { LimeLog.info("Analog stick deadzone: "+context.leftStickDeadzoneRadius+" "+context.rightStickDeadzoneRadius);
// Unknown devices use the default mapping LimeLog.info("Trigger deadzone: "+context.triggerDeadzone);
if (devName != null && devName.equals("gpio-keys")) {
// This is the back button on Shield portable consoles
context.controllerNumber = 0;
}
else if (multiControllerEnabled) {
context.controllerNumber = assignNewControllerNumber();
}
else {
context.controllerNumber = 0;
}
LimeLog.info("Assigned as controller "+context.controllerNumber);
return context;
}
private ControllerContext getContextForDevice(InputDevice dev) {
// Unknown devices use the default context
if (dev == null) { if (dev == null) {
return defaultMapping; return defaultContext;
} }
String descriptor = dev.getDescriptor(); String descriptor = dev.getDescriptor();
// Return the existing mapping if it exists // Return the existing context if it exists
ControllerMapping mapping = mappings.get(descriptor); ControllerContext context = contexts.get(descriptor);
if (mapping != null) { if (context != null) {
return mapping; return context;
} }
// Otherwise create a new mapping // Otherwise create a new context
mapping = createMappingForDevice(dev); context = createContextForDevice(dev);
mappings.put(descriptor, mapping); contexts.put(descriptor, context);
return mapping; return context;
} }
private void sendControllerInputPacket() { private void sendControllerInputPacket(ControllerContext context) {
conn.sendControllerInput(inputMap, leftTrigger, rightTrigger, conn.sendControllerInput(context.controllerNumber, context.inputMap,
leftStickX, leftStickY, rightStickX, rightStickY); context.leftTrigger, context.rightTrigger,
context.leftStickX, context.leftStickY,
context.rightStickX, context.rightStickY);
} }
private static int handleRemapping(ControllerMapping mapping, KeyEvent event) { // Return a valid keycode, 0 to consume, or -1 to not consume the event
if (mapping.isDualShock4) { // Device MAY BE NULL
private int handleRemapping(ControllerContext context, KeyEvent event) {
// For remotes, don't capture the back button
if (context.isRemote) {
if (event.getKeyCode() == KeyEvent.KEYCODE_BACK) {
return -1;
}
}
if (context.isDualShock4) {
switch (event.getKeyCode()) { switch (event.getKeyCode()) {
case KeyEvent.KEYCODE_BUTTON_Y: case KeyEvent.KEYCODE_BUTTON_Y:
return KeyEvent.KEYCODE_BUTTON_L1; return KeyEvent.KEYCODE_BUTTON_L1;
@@ -273,7 +379,7 @@ public class ControllerHandler {
} }
} }
if (mapping.hatXAxis != -1 && mapping.hatYAxis != -1) { if (context.hatXAxis != -1 && context.hatYAxis != -1) {
switch (event.getKeyCode()) { switch (event.getKeyCode()) {
// These are duplicate dpad events for hat input // These are duplicate dpad events for hat input
case KeyEvent.KEYCODE_DPAD_LEFT: case KeyEvent.KEYCODE_DPAD_LEFT:
@@ -284,9 +390,9 @@ public class ControllerHandler {
return 0; return 0;
} }
} }
else if (mapping.hatXAxis == -1 && else if (context.hatXAxis == -1 &&
mapping.hatYAxis == -1 && context.hatYAxis == -1 &&
mapping.isXboxController && context.isXboxController &&
event.getKeyCode() == KeyEvent.KEYCODE_UNKNOWN) { event.getKeyCode() == KeyEvent.KEYCODE_UNKNOWN) {
// If there's not a proper Xbox controller mapping, we'll translate the raw d-pad // If there's not a proper Xbox controller mapping, we'll translate the raw d-pad
// scan codes into proper key codes // scan codes into proper key codes
@@ -319,171 +425,136 @@ public class ControllerHandler {
if (keyCode == KeyEvent.KEYCODE_BUTTON_START || if (keyCode == KeyEvent.KEYCODE_BUTTON_START ||
keyCode == KeyEvent.KEYCODE_MENU) { keyCode == KeyEvent.KEYCODE_MENU) {
// Ensure that we never use back as start if we have a real start // 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 // Emulate the start button with back
return KeyEvent.KEYCODE_BUTTON_START; 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; return keyCode;
} }
private Vector2d handleDeadZone(float x, float y, float deadzoneRadius) { private Vector2d populateCachedVector(float x, float y) {
// Reinitialize our cached Vector2d object // Reinitialize our cached Vector2d object
inputVector.initialize(x, y); inputVector.initialize(x, y);
if (inputVector.getMagnitude() <= deadzoneRadius) {
// Deadzone -- return the zero vector
return Vector2d.ZERO;
}
else {
/*
FIXME: We're not normalizing here because we let the computer handle the deadzones.
Normalizing can make the deadzones larger than they should be after the computer also
evaluates the deadzone
// Scale the input based on the distance from the deadzone
inputVector.getNormalized(normalizedInputVector);
normalizedInputVector.scalarMultiply((inputVector.getMagnitude() - deadzoneRadius) / (1.0f - deadzoneRadius));
// Bound the X value to -1.0 to 1.0
if (normalizedInputVector.getX() > 1.0f) {
normalizedInputVector.setX(1.0f);
}
else if (normalizedInputVector.getX() < -1.0f) {
normalizedInputVector.setX(-1.0f);
}
// Bound the Y value to -1.0 to 1.0
if (normalizedInputVector.getY() > 1.0f) {
normalizedInputVector.setY(1.0f);
}
else if (normalizedInputVector.getY() < -1.0f) {
normalizedInputVector.setY(-1.0f);
}
return normalizedInputVector;
*/
return inputVector; return inputVector;
} }
private void handleDeadZone(Vector2d stickVector, float deadzoneRadius) {
if (stickVector.getMagnitude() <= deadzoneRadius) {
// Deadzone
stickVector.initialize(0, 0);
} }
private void handleAxisSet(ControllerMapping mapping, float lsX, float lsY, float rsX, // We're not normalizing here because we let the computer handle the deadzones.
// Normalizing can make the deadzones larger than they should be after the computer also
// evaluates the deadzone.
}
private void handleAxisSet(ControllerContext context, float lsX, float lsY, float rsX,
float rsY, float lt, float rt, float hatX, float hatY) { 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 = handleDeadZone(lsX, lsY, mapping.leftStickDeadzoneRadius); Vector2d leftStickVector = populateCachedVector(lsX, lsY);
leftStickX = (short) (leftStickVector.getX() * 0x7FFE); handleDeadZone(leftStickVector, context.leftStickDeadzoneRadius);
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 = handleDeadZone(rsX, rsY, mapping.rightStickDeadzoneRadius); Vector2d rightStickVector = populateCachedVector(rsX, rsY);
rightStickX = (short) (rightStickVector.getX() * 0x7FFE); handleDeadZone(rightStickVector, context.rightStickDeadzoneRadius);
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 (context.leftTriggerAxis != -1 && context.rightTriggerAxis != -1) {
if (mapping.triggersIdleNegative) { if (context.triggersIdleNegative) {
lt = (lt + 1) / 2; lt = (lt + 1) / 2;
rt = (rt + 1) / 2; rt = (rt + 1) / 2;
} }
if (lt <= mapping.triggerDeadzone) { if (lt <= context.triggerDeadzone) {
lt = 0; lt = 0;
} }
if (rt <= mapping.triggerDeadzone) { if (rt <= context.triggerDeadzone) {
rt = 0; rt = 0;
} }
leftTrigger = (byte)(lt * 0xFF); context.leftTrigger = (byte)(lt * 0xFF);
rightTrigger = (byte)(rt * 0xFF); context.rightTrigger = (byte)(rt * 0xFF);
} }
if (mapping.hatXAxis != -1 && mapping.hatYAxis != -1) { if (context.hatXAxis != -1 && context.hatYAxis != -1) {
inputMap &= ~(ControllerPacket.LEFT_FLAG | ControllerPacket.RIGHT_FLAG); context.inputMap &= ~(ControllerPacket.LEFT_FLAG | ControllerPacket.RIGHT_FLAG);
if (hatX < -0.5) { if (hatX < -0.5) {
inputMap |= ControllerPacket.LEFT_FLAG; context.inputMap |= ControllerPacket.LEFT_FLAG;
} }
else if (hatX > 0.5) { 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) { if (hatY < -0.5) {
inputMap |= ControllerPacket.UP_FLAG; context.inputMap |= ControllerPacket.UP_FLAG;
} }
else if (hatY > 0.5) { else if (hatY > 0.5) {
inputMap |= ControllerPacket.DOWN_FLAG; context.inputMap |= ControllerPacket.DOWN_FLAG;
} }
} }
sendControllerInputPacket(); sendControllerInputPacket(context);
} }
public boolean handleMotionEvent(MotionEvent event) { 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; float lsX = 0, lsY = 0, rsX = 0, rsY = 0, rt = 0, lt = 0, hatX = 0, hatY = 0;
// Replay the full history before getting the current values // We purposefully ignore the historical values in the motion event as it makes
for (int i = 0; i < event.getHistorySize(); i++) { // the controller feel sluggish for some users.
if (mapping.leftStickXAxis != -1 && mapping.leftStickYAxis != -1) {
lsX = event.getHistoricalAxisValue(mapping.leftStickXAxis, i); if (context.leftStickXAxis != -1 && context.leftStickYAxis != -1) {
lsY = event.getHistoricalAxisValue(mapping.leftStickYAxis, i); lsX = event.getAxisValue(context.leftStickXAxis);
lsY = event.getAxisValue(context.leftStickYAxis);
} }
if (mapping.rightStickXAxis != -1 && mapping.rightStickYAxis != -1) { if (context.rightStickXAxis != -1 && context.rightStickYAxis != -1) {
rsX = event.getHistoricalAxisValue(mapping.rightStickXAxis, i); rsX = event.getAxisValue(context.rightStickXAxis);
rsY = event.getHistoricalAxisValue(mapping.rightStickYAxis, i); rsY = event.getAxisValue(context.rightStickYAxis);
} }
if (mapping.leftTriggerAxis != -1 && mapping.rightTriggerAxis != -1) { if (context.leftTriggerAxis != -1 && context.rightTriggerAxis != -1) {
lt = event.getHistoricalAxisValue(mapping.leftTriggerAxis, i); lt = event.getAxisValue(context.leftTriggerAxis);
rt = event.getHistoricalAxisValue(mapping.rightTriggerAxis, i); rt = event.getAxisValue(context.rightTriggerAxis);
} }
if (mapping.hatXAxis != -1 && mapping.hatYAxis != -1) { if (context.hatXAxis != -1 && context.hatYAxis != -1) {
hatX = event.getHistoricalAxisValue(MotionEvent.AXIS_HAT_X, i);
hatY = event.getHistoricalAxisValue(MotionEvent.AXIS_HAT_Y, i);
}
handleAxisSet(mapping, lsX, lsY, rsX, rsY, lt, rt, hatX, hatY);
}
// Now handle the current set of values
if (mapping.leftStickXAxis != -1 && mapping.leftStickYAxis != -1) {
lsX = event.getAxisValue(mapping.leftStickXAxis);
lsY = event.getAxisValue(mapping.leftStickYAxis);
}
if (mapping.rightStickXAxis != -1 && mapping.rightStickYAxis != -1) {
rsX = event.getAxisValue(mapping.rightStickXAxis);
rsY = event.getAxisValue(mapping.rightStickYAxis);
}
if (mapping.leftTriggerAxis != -1 && mapping.rightTriggerAxis != -1) {
lt = event.getAxisValue(mapping.leftTriggerAxis);
rt = event.getAxisValue(mapping.rightTriggerAxis);
}
if (mapping.hatXAxis != -1 && mapping.hatYAxis != -1) {
hatX = event.getAxisValue(MotionEvent.AXIS_HAT_X); hatX = event.getAxisValue(MotionEvent.AXIS_HAT_X);
hatY = event.getAxisValue(MotionEvent.AXIS_HAT_Y); 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; return true;
} }
public boolean handleButtonUp(KeyEvent event) { 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) { if (keyCode == 0) {
return true; return true;
} }
@@ -502,75 +573,80 @@ public class ControllerHandler {
switch (keyCode) { switch (keyCode) {
case KeyEvent.KEYCODE_BUTTON_MODE: case KeyEvent.KEYCODE_BUTTON_MODE:
inputMap &= ~ControllerPacket.SPECIAL_BUTTON_FLAG; context.inputMap &= ~ControllerPacket.SPECIAL_BUTTON_FLAG;
break; break;
case KeyEvent.KEYCODE_BUTTON_START: case KeyEvent.KEYCODE_BUTTON_START:
case KeyEvent.KEYCODE_MENU: case KeyEvent.KEYCODE_MENU:
inputMap &= ~ControllerPacket.PLAY_FLAG; if (SystemClock.uptimeMillis() - context.startDownTime > ControllerHandler.START_DOWN_TIME_KEYB_MS) {
// FIXME: The stock keyboard doesn't have controller focus so isn't usable. I'm not enabling this shortcut
// until we have a custom keyboard or some other fix
//gestures.showKeyboard();
}
context.inputMap &= ~ControllerPacket.PLAY_FLAG;
break; break;
case KeyEvent.KEYCODE_BACK: case KeyEvent.KEYCODE_BACK:
case KeyEvent.KEYCODE_BUTTON_SELECT: case KeyEvent.KEYCODE_BUTTON_SELECT:
inputMap &= ~ControllerPacket.BACK_FLAG; context.inputMap &= ~ControllerPacket.BACK_FLAG;
break; break;
case KeyEvent.KEYCODE_DPAD_LEFT: case KeyEvent.KEYCODE_DPAD_LEFT:
inputMap &= ~ControllerPacket.LEFT_FLAG; context.inputMap &= ~ControllerPacket.LEFT_FLAG;
break; break;
case KeyEvent.KEYCODE_DPAD_RIGHT: case KeyEvent.KEYCODE_DPAD_RIGHT:
inputMap &= ~ControllerPacket.RIGHT_FLAG; context.inputMap &= ~ControllerPacket.RIGHT_FLAG;
break; break;
case KeyEvent.KEYCODE_DPAD_UP: case KeyEvent.KEYCODE_DPAD_UP:
inputMap &= ~ControllerPacket.UP_FLAG; context.inputMap &= ~ControllerPacket.UP_FLAG;
break; break;
case KeyEvent.KEYCODE_DPAD_DOWN: case KeyEvent.KEYCODE_DPAD_DOWN:
inputMap &= ~ControllerPacket.DOWN_FLAG; context.inputMap &= ~ControllerPacket.DOWN_FLAG;
break; break;
case KeyEvent.KEYCODE_BUTTON_B: case KeyEvent.KEYCODE_BUTTON_B:
inputMap &= ~ControllerPacket.B_FLAG; context.inputMap &= ~ControllerPacket.B_FLAG;
break; break;
case KeyEvent.KEYCODE_DPAD_CENTER: case KeyEvent.KEYCODE_DPAD_CENTER:
case KeyEvent.KEYCODE_BUTTON_A: case KeyEvent.KEYCODE_BUTTON_A:
inputMap &= ~ControllerPacket.A_FLAG; context.inputMap &= ~ControllerPacket.A_FLAG;
break; break;
case KeyEvent.KEYCODE_BUTTON_X: case KeyEvent.KEYCODE_BUTTON_X:
inputMap &= ~ControllerPacket.X_FLAG; context.inputMap &= ~ControllerPacket.X_FLAG;
break; break;
case KeyEvent.KEYCODE_BUTTON_Y: case KeyEvent.KEYCODE_BUTTON_Y:
inputMap &= ~ControllerPacket.Y_FLAG; context.inputMap &= ~ControllerPacket.Y_FLAG;
break; break;
case KeyEvent.KEYCODE_BUTTON_L1: case KeyEvent.KEYCODE_BUTTON_L1:
inputMap &= ~ControllerPacket.LB_FLAG; context.inputMap &= ~ControllerPacket.LB_FLAG;
lastLbUpTime = SystemClock.uptimeMillis(); context.lastLbUpTime = SystemClock.uptimeMillis();
break; break;
case KeyEvent.KEYCODE_BUTTON_R1: case KeyEvent.KEYCODE_BUTTON_R1:
inputMap &= ~ControllerPacket.RB_FLAG; context.inputMap &= ~ControllerPacket.RB_FLAG;
lastRbUpTime = SystemClock.uptimeMillis(); context.lastRbUpTime = SystemClock.uptimeMillis();
break; break;
case KeyEvent.KEYCODE_BUTTON_THUMBL: case KeyEvent.KEYCODE_BUTTON_THUMBL:
inputMap &= ~ControllerPacket.LS_CLK_FLAG; context.inputMap &= ~ControllerPacket.LS_CLK_FLAG;
break; break;
case KeyEvent.KEYCODE_BUTTON_THUMBR: case KeyEvent.KEYCODE_BUTTON_THUMBR:
inputMap &= ~ControllerPacket.RS_CLK_FLAG; context.inputMap &= ~ControllerPacket.RS_CLK_FLAG;
break; break;
case KeyEvent.KEYCODE_BUTTON_L2: case KeyEvent.KEYCODE_BUTTON_L2:
leftTrigger = 0; context.leftTrigger = 0;
break; break;
case KeyEvent.KEYCODE_BUTTON_R2: case KeyEvent.KEYCODE_BUTTON_R2:
rightTrigger = 0; context.rightTrigger = 0;
break; break;
default: default:
return false; return false;
} }
// Check if we're emulating the select button // 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 either start or LB is up, select comes up too
if ((inputMap & ControllerPacket.PLAY_FLAG) == 0 || if ((context.inputMap & ControllerPacket.PLAY_FLAG) == 0 ||
(inputMap & ControllerPacket.LB_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 { try {
Thread.sleep(EMULATED_SELECT_UP_DELAY_MS); Thread.sleep(EMULATED_SELECT_UP_DELAY_MS);
@@ -579,16 +655,16 @@ public class ControllerHandler {
} }
// Check if we're emulating the special button // 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 either start or select and RB is up, the special button comes up too
if ((inputMap & ControllerPacket.PLAY_FLAG) == 0 || if ((context.inputMap & ControllerPacket.PLAY_FLAG) == 0 ||
((inputMap & ControllerPacket.BACK_FLAG) == 0 && ((context.inputMap & ControllerPacket.BACK_FLAG) == 0 &&
(inputMap & ControllerPacket.RB_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 { try {
Thread.sleep(EMULATED_SPECIAL_UP_DELAY_MS); Thread.sleep(EMULATED_SPECIAL_UP_DELAY_MS);
@@ -596,109 +672,115 @@ public class ControllerHandler {
} }
} }
sendControllerInputPacket(); sendControllerInputPacket(context);
return true; return true;
} }
public boolean handleButtonDown(KeyEvent event) { 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) { if (keyCode == 0) {
return true; return true;
} }
switch (keyCode) { switch (keyCode) {
case KeyEvent.KEYCODE_BUTTON_MODE: case KeyEvent.KEYCODE_BUTTON_MODE:
inputMap |= ControllerPacket.SPECIAL_BUTTON_FLAG; context.inputMap |= ControllerPacket.SPECIAL_BUTTON_FLAG;
break; break;
case KeyEvent.KEYCODE_BUTTON_START: case KeyEvent.KEYCODE_BUTTON_START:
case KeyEvent.KEYCODE_MENU: case KeyEvent.KEYCODE_MENU:
inputMap |= ControllerPacket.PLAY_FLAG; if (event.getRepeatCount() == 0) {
context.startDownTime = SystemClock.uptimeMillis();
}
context.inputMap |= ControllerPacket.PLAY_FLAG;
break; break;
case KeyEvent.KEYCODE_BACK: case KeyEvent.KEYCODE_BACK:
case KeyEvent.KEYCODE_BUTTON_SELECT: case KeyEvent.KEYCODE_BUTTON_SELECT:
inputMap |= ControllerPacket.BACK_FLAG; context.inputMap |= ControllerPacket.BACK_FLAG;
break; break;
case KeyEvent.KEYCODE_DPAD_LEFT: case KeyEvent.KEYCODE_DPAD_LEFT:
inputMap |= ControllerPacket.LEFT_FLAG; context.inputMap |= ControllerPacket.LEFT_FLAG;
break; break;
case KeyEvent.KEYCODE_DPAD_RIGHT: case KeyEvent.KEYCODE_DPAD_RIGHT:
inputMap |= ControllerPacket.RIGHT_FLAG; context.inputMap |= ControllerPacket.RIGHT_FLAG;
break; break;
case KeyEvent.KEYCODE_DPAD_UP: case KeyEvent.KEYCODE_DPAD_UP:
inputMap |= ControllerPacket.UP_FLAG; context.inputMap |= ControllerPacket.UP_FLAG;
break; break;
case KeyEvent.KEYCODE_DPAD_DOWN: case KeyEvent.KEYCODE_DPAD_DOWN:
inputMap |= ControllerPacket.DOWN_FLAG; context.inputMap |= ControllerPacket.DOWN_FLAG;
break; break;
case KeyEvent.KEYCODE_BUTTON_B: case KeyEvent.KEYCODE_BUTTON_B:
inputMap |= ControllerPacket.B_FLAG; context.inputMap |= ControllerPacket.B_FLAG;
break; break;
case KeyEvent.KEYCODE_DPAD_CENTER: case KeyEvent.KEYCODE_DPAD_CENTER:
case KeyEvent.KEYCODE_BUTTON_A: case KeyEvent.KEYCODE_BUTTON_A:
inputMap |= ControllerPacket.A_FLAG; context.inputMap |= ControllerPacket.A_FLAG;
break; break;
case KeyEvent.KEYCODE_BUTTON_X: case KeyEvent.KEYCODE_BUTTON_X:
inputMap |= ControllerPacket.X_FLAG; context.inputMap |= ControllerPacket.X_FLAG;
break; break;
case KeyEvent.KEYCODE_BUTTON_Y: case KeyEvent.KEYCODE_BUTTON_Y:
inputMap |= ControllerPacket.Y_FLAG; context.inputMap |= ControllerPacket.Y_FLAG;
break; break;
case KeyEvent.KEYCODE_BUTTON_L1: case KeyEvent.KEYCODE_BUTTON_L1:
inputMap |= ControllerPacket.LB_FLAG; context.inputMap |= ControllerPacket.LB_FLAG;
break; break;
case KeyEvent.KEYCODE_BUTTON_R1: case KeyEvent.KEYCODE_BUTTON_R1:
inputMap |= ControllerPacket.RB_FLAG; context.inputMap |= ControllerPacket.RB_FLAG;
break; break;
case KeyEvent.KEYCODE_BUTTON_THUMBL: case KeyEvent.KEYCODE_BUTTON_THUMBL:
inputMap |= ControllerPacket.LS_CLK_FLAG; context.inputMap |= ControllerPacket.LS_CLK_FLAG;
break; break;
case KeyEvent.KEYCODE_BUTTON_THUMBR: case KeyEvent.KEYCODE_BUTTON_THUMBR:
inputMap |= ControllerPacket.RS_CLK_FLAG; context.inputMap |= ControllerPacket.RS_CLK_FLAG;
break; break;
case KeyEvent.KEYCODE_BUTTON_L2: case KeyEvent.KEYCODE_BUTTON_L2:
leftTrigger = (byte)0xFF; context.leftTrigger = (byte)0xFF;
break; break;
case KeyEvent.KEYCODE_BUTTON_R2: case KeyEvent.KEYCODE_BUTTON_R2:
rightTrigger = (byte)0xFF; context.rightTrigger = (byte)0xFF;
break; break;
default: default:
return false; return false;
} }
// Start+LB acts like select for controllers with one button // Start+LB acts like select for controllers with one button
if ((inputMap & ControllerPacket.PLAY_FLAG) != 0 && if ((context.inputMap & ControllerPacket.PLAY_FLAG) != 0 &&
((inputMap & ControllerPacket.LB_FLAG) != 0 || ((context.inputMap & ControllerPacket.LB_FLAG) != 0 ||
SystemClock.uptimeMillis() - lastLbUpTime <= MAXIMUM_BUMPER_UP_DELAY_MS)) SystemClock.uptimeMillis() - context.lastLbUpTime <= MAXIMUM_BUMPER_UP_DELAY_MS))
{ {
inputMap &= ~(ControllerPacket.PLAY_FLAG | ControllerPacket.LB_FLAG); context.inputMap &= ~(ControllerPacket.PLAY_FLAG | ControllerPacket.LB_FLAG);
inputMap |= ControllerPacket.BACK_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 // We detect select+start or start+RB as the special button combo
if (((inputMap & ControllerPacket.RB_FLAG) != 0 || if (((context.inputMap & ControllerPacket.RB_FLAG) != 0 ||
(SystemClock.uptimeMillis() - lastRbUpTime <= MAXIMUM_BUMPER_UP_DELAY_MS) || (SystemClock.uptimeMillis() - context.lastRbUpTime <= MAXIMUM_BUMPER_UP_DELAY_MS) ||
(inputMap & ControllerPacket.BACK_FLAG) != 0) && (context.inputMap & ControllerPacket.BACK_FLAG) != 0) &&
(inputMap & ControllerPacket.PLAY_FLAG) != 0) (context.inputMap & ControllerPacket.PLAY_FLAG) != 0)
{ {
inputMap &= ~(ControllerPacket.BACK_FLAG | ControllerPacket.PLAY_FLAG | ControllerPacket.RB_FLAG); context.inputMap &= ~(ControllerPacket.BACK_FLAG | ControllerPacket.PLAY_FLAG | ControllerPacket.RB_FLAG);
inputMap |= ControllerPacket.SPECIAL_BUTTON_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 // Send a new input packet if this is the first instance of a button down event
// or anytime if we're emulating a button // or anytime if we're emulating a button
if (event.getRepeatCount() == 0 || emulatingButtonFlags != 0) { if (event.getRepeatCount() == 0 || context.emulatingButtonFlags != 0) {
sendControllerInputPacket(); sendControllerInputPacket(context);
} }
return true; return true;
} }
class ControllerMapping { class ControllerContext {
public String name;
public int id;
public int leftStickXAxis = -1; public int leftStickXAxis = -1;
public int leftStickYAxis = -1; public int leftStickYAxis = -1;
public float leftStickDeadzoneRadius; public float leftStickDeadzoneRadius;
@@ -718,5 +800,29 @@ public class ControllerHandler {
public boolean isDualShock4; public boolean isDualShock4;
public boolean isXboxController; public boolean isXboxController;
public boolean backIsStart; public boolean backIsStart;
public boolean modeIsSelect;
public boolean isRemote;
public boolean hasJoystickAxes;
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 * GFE's prefix for every key code
*/ */
public static final short KEY_PREFIX = (short) 0x80; private static final short KEY_PREFIX = (short) 0x80;
public static final int VK_0 = 48; public static final int VK_0 = 48;
public static final int VK_9 = 57; public static final int VK_9 = 57;
@@ -9,10 +9,12 @@ public class TouchContext {
private int originalTouchX = 0; private int originalTouchX = 0;
private int originalTouchY = 0; private int originalTouchY = 0;
private long originalTouchTime = 0; private long originalTouchTime = 0;
private boolean cancelled;
private NvConnection conn; private final NvConnection conn;
private int actionIndex; private final int actionIndex;
private double xFactor, yFactor; private final double xFactor;
private final double yFactor;
private static final int TAP_MOVEMENT_THRESHOLD = 10; private static final int TAP_MOVEMENT_THRESHOLD = 10;
private static final int TAP_TIME_THRESHOLD = 250; private static final int TAP_TIME_THRESHOLD = 250;
@@ -56,12 +58,17 @@ public class TouchContext {
originalTouchX = lastTouchX = eventX; originalTouchX = lastTouchX = eventX;
originalTouchY = lastTouchY = eventY; originalTouchY = lastTouchY = eventY;
originalTouchTime = System.currentTimeMillis(); originalTouchTime = System.currentTimeMillis();
cancelled = false;
return true; return true;
} }
public void touchUpEvent(int eventX, int eventY) public void touchUpEvent(int eventX, int eventY)
{ {
if (cancelled) {
return;
}
if (isTap()) if (isTap())
{ {
byte buttonIndex = getMouseButtonIndex(); byte buttonIndex = getMouseButtonIndex();
@@ -102,4 +109,12 @@ public class TouchContext {
return true; return true;
} }
public void cancelTouch() {
cancelled = true;
}
public boolean isCancelled() {
return cancelled;
}
} }
@@ -29,9 +29,9 @@ public class EvdevEvent {
/* Keys */ /* Keys */
public static final short KEY_Q = 16; public static final short KEY_Q = 16;
public short type; public final short type;
public short code; public final short code;
public int value; public final int value;
public EvdevEvent(short type, short code, int value) { public EvdevEvent(short type, short code, int value) {
this.type = type; this.type = type;
@@ -7,12 +7,12 @@ import com.limelight.LimeLog;
public class EvdevHandler { public class EvdevHandler {
private String absolutePath; private final String absolutePath;
private EvdevListener listener; private final EvdevListener listener;
private boolean shutdown = false; private boolean shutdown = false;
private int fd = -1; private int fd = -1;
private Thread handlerThread = new Thread() { private final Thread handlerThread = new Thread() {
@Override @Override
public void run() { public void run() {
// All the finally blocks here make this code look like a mess // All the finally blocks here make this code look like a mess
@@ -1,7 +1,7 @@
package com.limelight.binding.input.evdev; package com.limelight.binding.input.evdev;
import java.io.IOException; import android.os.Build;
import java.io.OutputStream;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.util.Locale; import java.util.Locale;
@@ -12,30 +12,32 @@ public class EvdevReader {
System.loadLibrary("evdev_reader"); System.loadLibrary("evdev_reader");
} }
public static void patchSeLinuxPolicies() {
//
// FIXME: We REALLY shouldn't being changing permissions on the input devices like this.
// We should probably do something clever with a separate daemon and talk via a localhost
// socket. We don't return the SELinux policies back to default after we're done which I feel
// bad about, but we do chmod the input devices back so I don't think any additional attack surface
// remains opened after streaming other than listing the /dev/input directory which you wouldn't
// normally be able to do with SELinux enforcing on Lollipop.
//
// We need to modify SELinux policies to allow us to capture input devices on Lollipop and possibly other
// more restrictive ROMs. Per Chainfire's SuperSU documentation, the supolicy binary is provided on
// 4.4 and later to do live SELinux policy changes.
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
EvdevShell shell = EvdevShell.getInstance();
shell.runCommand("supolicy --live \"allow untrusted_app input_device dir { open getattr read search }\" " +
"\"allow untrusted_app input_device chr_file { open read write ioctl }\"");
}
}
// Requires root to chmod /dev/input/eventX // Requires root to chmod /dev/input/eventX
public static boolean setPermissions(String[] files, int octalPermissions) { public static void setPermissions(String[] files, int octalPermissions) {
ProcessBuilder builder = new ProcessBuilder("su"); EvdevShell shell = EvdevShell.getInstance();
try {
Process p = builder.start();
OutputStream stdin = p.getOutputStream();
for (String file : files) { for (String file : files) {
stdin.write(String.format((Locale)null, "chmod %o %s\n", octalPermissions, file).getBytes("UTF-8")); shell.runCommand(String.format((Locale)null, "chmod %o %s", octalPermissions, file));
} }
stdin.write("exit\n".getBytes("UTF-8"));
stdin.flush();
p.waitFor();
p.destroy();
return true;
} catch (IOException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
}
return false;
} }
// Returns the fd to be passed to other function or -1 on error // Returns the fd to be passed to other function or -1 on error
@@ -0,0 +1,116 @@
package com.limelight.binding.input.evdev;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Scanner;
import java.util.UUID;
public class EvdevShell {
private OutputStream stdin;
private InputStream stdout;
private Process shell;
private final String uuidString = UUID.randomUUID().toString();
private static final EvdevShell globalShell = new EvdevShell();
public static EvdevShell getInstance() {
return globalShell;
}
public void startShell() {
ProcessBuilder builder = new ProcessBuilder("su");
try {
// Redirect stderr to stdout
builder.redirectErrorStream(true);
shell = builder.start();
stdin = shell.getOutputStream();
stdout = shell.getInputStream();
} catch (IOException e) {
// This is unexpected
e.printStackTrace();
// Kill the shell if it spawned
if (stdin != null) {
try {
stdin.close();
} catch (IOException e1) {
e1.printStackTrace();
} finally {
stdin = null;
}
}
if (stdout != null) {
try {
stdout.close();
} catch (IOException e1) {
e1.printStackTrace();
} finally {
stdout = null;
}
}
if (shell != null) {
shell.destroy();
shell = null;
}
}
}
public void runCommand(String command) {
if (shell == null) {
// Shell never started
return;
}
try {
// Write the command followed by an echo with our UUID
stdin.write((command+'\n').getBytes("UTF-8"));
stdin.write(("echo "+uuidString+'\n').getBytes("UTF-8"));
stdin.flush();
// This is the only command in flight so we can use a scanner
// without worrying about it eating too many characters
Scanner scanner = new Scanner(stdout);
while (scanner.hasNext()) {
if (scanner.next().contains(uuidString)) {
// Our command ran
return;
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
public void stopShell() throws InterruptedException {
boolean exitWritten = false;
if (shell == null) {
// Shell never started
return;
}
try {
stdin.write("exit\n".getBytes("UTF-8"));
exitWritten = true;
} catch (IOException e) {
// We'll destroy the process without
// waiting for it to terminate since
// we don't know whether our exit command made it
e.printStackTrace();
}
if (exitWritten) {
try {
shell.waitFor();
} finally {
shell.destroy();
}
}
else {
shell.destroy();
}
}
}
@@ -4,7 +4,7 @@ import android.view.KeyEvent;
public class EvdevTranslator { public class EvdevTranslator {
public static final short EVDEV_KEY_CODES[] = { private static final short[] EVDEV_KEY_CODES = {
0, //KeyEvent.VK_RESERVED 0, //KeyEvent.VK_RESERVED
KeyEvent.KEYCODE_ESCAPE, KeyEvent.KEYCODE_ESCAPE,
KeyEvent.KEYCODE_1, KeyEvent.KEYCODE_1,
@@ -20,6 +20,8 @@ public class EvdevWatcher {
private EvdevListener listener; private EvdevListener listener;
private Thread startThread; private Thread startThread;
private static boolean patchedSeLinuxPolicies = false;
private FileObserver observer = new FileObserver(PATH, FileObserver.CREATE | FileObserver.DELETE) { private FileObserver observer = new FileObserver(PATH, FileObserver.CREATE | FileObserver.DELETE) {
@Override @Override
public void onEvent(int event, String fileName) { public void onEvent(int event, String fileName) {
@@ -117,6 +119,15 @@ public class EvdevWatcher {
startThread = new Thread() { startThread = new Thread() {
@Override @Override
public void run() { public void run() {
// Initialize the root shell
EvdevShell.getInstance().startShell();
// Patch SELinux policies (if needed)
if (!patchedSeLinuxPolicies) {
EvdevReader.patchSeLinuxPolicies();
patchedSeLinuxPolicies = true;
}
// List all files and allow us access // List all files and allow us access
File[] files = rundownWithPermissionsChange(0666); File[] files = rundownWithPermissionsChange(0666);
@@ -139,7 +150,12 @@ public class EvdevWatcher {
} }
// Giveup eventX permissions // Giveup eventX permissions
rundownWithPermissionsChange(066); rundownWithPermissionsChange(0660);
// Kill the root shell
try {
EvdevShell.getInstance().stopShell();
} catch (InterruptedException e) {}
} }
}; };
startThread.start(); startThread.start();
@@ -36,7 +36,7 @@ public class AndroidCpuDecoderRenderer extends EnhancedDecoderRenderer {
private int totalFrames; private int totalFrames;
private long totalTimeMs; private long totalTimeMs;
private int cpuCount = Runtime.getRuntime().availableProcessors(); private final int cpuCount = Runtime.getRuntime().availableProcessors();
@SuppressWarnings("unused") @SuppressWarnings("unused")
private int findOptimalPerformanceLevel() { private int findOptimalPerformanceLevel() {
@@ -141,7 +141,9 @@ public class AndroidCpuDecoderRenderer extends EnhancedDecoderRenderer {
throw new IllegalStateException("AVC decoder initialization failure: "+err); throw new IllegalStateException("AVC decoder initialization failure: "+err);
} }
AvcDecoder.setRenderTarget(sh.getSurface()); if (!AvcDecoder.setRenderTarget(sh.getSurface())) {
return false;
}
decoderBuffer = ByteBuffer.allocate(DECODER_BUFFER_SIZE + AvcDecoder.getInputPaddingSize()); decoderBuffer = ByteBuffer.allocate(DECODER_BUFFER_SIZE + AvcDecoder.getInputPaddingSize());
@@ -252,7 +254,7 @@ public class AndroidCpuDecoderRenderer extends EnhancedDecoderRenderer {
// Add delta time to the totals (excluding probable outliers) // Add delta time to the totals (excluding probable outliers)
long delta = timeAfterDecode - decodeUnit.getReceiveTimestamp(); long delta = timeAfterDecode - decodeUnit.getReceiveTimestamp();
if (delta >= 0 && delta < 300) { if (delta >= 0 && delta < 1000) {
totalTimeMs += delta; totalTimeMs += delta;
totalFrames++; totalFrames++;
} }
@@ -273,7 +273,7 @@ public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer {
// Add delta time to the totals (excluding probable outliers) // Add delta time to the totals (excluding probable outliers)
long delta = System.currentTimeMillis()-(presentationTimeUs/1000); long delta = System.currentTimeMillis()-(presentationTimeUs/1000);
if (delta >= 0 && delta < 300) { if (delta >= 0 && delta < 1000) {
decoderTimeMs += delta; decoderTimeMs += delta;
totalTimeMs += delta; totalTimeMs += delta;
} }
@@ -371,7 +371,7 @@ public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer {
private void submitDecodeUnit(DecodeUnit decodeUnit, ByteBuffer buf, int inputBufferIndex) { private void submitDecodeUnit(DecodeUnit decodeUnit, ByteBuffer buf, int inputBufferIndex) {
long currentTime = System.currentTimeMillis(); long currentTime = System.currentTimeMillis();
long delta = currentTime-decodeUnit.getReceiveTimestamp(); long delta = currentTime-decodeUnit.getReceiveTimestamp();
if (delta >= 0 && delta < 300) { if (delta >= 0 && delta < 1000) {
totalTimeMs += currentTime-decodeUnit.getReceiveTimestamp(); totalTimeMs += currentTime-decodeUnit.getReceiveTimestamp();
totalFrames++; totalFrames++;
} }
@@ -411,6 +411,23 @@ public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer {
SeqParameterSet sps = SeqParameterSet.read(spsBuf); SeqParameterSet sps = SeqParameterSet.read(spsBuf);
// Some decoders rely on H264 level to decide how many buffers are needed
// Since we only need one frame buffered, we'll set the level as low as we can
// for known resolution combinations
if (initialWidth == 1280 && initialHeight == 720) {
// Max 5 buffered frames at 1280x720x60
LimeLog.info("Patching level_idc to 32");
sps.level_idc = 32;
}
else if (initialWidth == 1920 && initialHeight == 1080) {
// Max 4 buffered frames at 1920x1080x64
LimeLog.info("Patching level_idc to 42");
sps.level_idc = 42;
}
else {
// Leave the profile alone (currently 5.0)
}
// TI OMAP4 requires a reference frame count of 1 to decode successfully. Exynos 4 // TI OMAP4 requires a reference frame count of 1 to decode successfully. Exynos 4
// also requires this fixup. // also requires this fixup.
// //
@@ -541,8 +558,8 @@ public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer {
public class RendererException extends RuntimeException { public class RendererException extends RuntimeException {
private static final long serialVersionUID = 8985937536997012406L; private static final long serialVersionUID = 8985937536997012406L;
private Exception originalException; private final Exception originalException;
private MediaCodecDecoderRenderer renderer; private final MediaCodecDecoderRenderer renderer;
private ByteBuffer currentBuffer; private ByteBuffer currentBuffer;
private int currentCodecFlags; private int currentCodecFlags;
@@ -20,12 +20,12 @@ import com.limelight.LimeLog;
public class MediaCodecHelper { public class MediaCodecHelper {
public static final List<String> preferredDecoders; private static final List<String> preferredDecoders;
public static final List<String> blacklistedDecoderPrefixes; private static final List<String> blacklistedDecoderPrefixes;
public static final List<String> spsFixupBitstreamFixupDecoderPrefixes; private static final List<String> spsFixupBitstreamFixupDecoderPrefixes;
public static final List<String> whitelistedAdaptiveResolutionPrefixes; private static final List<String> whitelistedAdaptiveResolutionPrefixes;
public static final List<String> baselineProfileHackPrefixes; private static final List<String> baselineProfileHackPrefixes;
static { static {
preferredDecoders = new LinkedList<String>(); preferredDecoders = new LinkedList<String>();
@@ -44,6 +44,7 @@ public class MediaCodecHelper {
spsFixupBitstreamFixupDecoderPrefixes.add("omx.nvidia"); spsFixupBitstreamFixupDecoderPrefixes.add("omx.nvidia");
spsFixupBitstreamFixupDecoderPrefixes.add("omx.qcom"); spsFixupBitstreamFixupDecoderPrefixes.add("omx.qcom");
spsFixupBitstreamFixupDecoderPrefixes.add("omx.mtk"); spsFixupBitstreamFixupDecoderPrefixes.add("omx.mtk");
spsFixupBitstreamFixupDecoderPrefixes.add("omx.brcm");
baselineProfileHackPrefixes = new LinkedList<String>(); baselineProfileHackPrefixes = new LinkedList<String>();
baselineProfileHackPrefixes.add("omx.intel"); baselineProfileHackPrefixes.add("omx.intel");
@@ -145,7 +146,7 @@ public class MediaCodecHelper {
return str; return str;
} }
public static MediaCodecInfo findPreferredDecoder() { private static MediaCodecInfo findPreferredDecoder() {
// This is a different algorithm than the other findXXXDecoder functions, // This is a different algorithm than the other findXXXDecoder functions,
// because we want to evaluate the decoders in our list's order // because we want to evaluate the decoders in our list's order
// rather than MediaCodecList's order // rather than MediaCodecList's order
@@ -216,7 +217,7 @@ public class MediaCodecHelper {
// since some bad decoders can throw IllegalArgumentExceptions unexpectedly // since some bad decoders can throw IllegalArgumentExceptions unexpectedly
// and we want to be sure all callers are handling this possibility // and we want to be sure all callers are handling this possibility
@SuppressWarnings("RedundantThrows") @SuppressWarnings("RedundantThrows")
public static MediaCodecInfo findKnownSafeDecoder() throws Exception { private static MediaCodecInfo findKnownSafeDecoder() throws Exception {
for (MediaCodecInfo codecInfo : getMediaCodecList()) { for (MediaCodecInfo codecInfo : getMediaCodecList()) {
// Skip encoders // Skip encoders
if (codecInfo.isEncoder()) { if (codecInfo.isEncoder()) {
@@ -1,16 +1,23 @@
package com.limelight.computers; package com.limelight.computers;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.StringReader;
import java.net.InetAddress; import java.net.InetAddress;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicInteger;
import com.limelight.LimeLog; import com.limelight.LimeLog;
import com.limelight.binding.PlatformBinding; import com.limelight.binding.PlatformBinding;
import com.limelight.discovery.DiscoveryService; import com.limelight.discovery.DiscoveryService;
import com.limelight.nvstream.http.ComputerDetails; import com.limelight.nvstream.http.ComputerDetails;
import com.limelight.nvstream.http.NvApp;
import com.limelight.nvstream.http.NvHTTP; import com.limelight.nvstream.http.NvHTTP;
import com.limelight.nvstream.mdns.MdnsComputer; import com.limelight.nvstream.mdns.MdnsComputer;
import com.limelight.nvstream.mdns.MdnsDiscoveryListener; import com.limelight.nvstream.mdns.MdnsDiscoveryListener;
import com.limelight.utils.CacheHelper;
import android.app.Service; import android.app.Service;
import android.content.ComponentName; import android.content.ComponentName;
@@ -19,19 +26,21 @@ import android.content.ServiceConnection;
import android.os.Binder; import android.os.Binder;
import android.os.IBinder; import android.os.IBinder;
import org.xmlpull.v1.XmlPullParserException;
public class ComputerManagerService extends Service { public class ComputerManagerService extends Service {
private static final int POLLING_PERIOD_MS = 3000; private static final int POLLING_PERIOD_MS = 3000;
private static final int MDNS_QUERY_PERIOD_MS = 1000; private static final int MDNS_QUERY_PERIOD_MS = 1000;
private ComputerManagerBinder binder = new ComputerManagerBinder(); private final ComputerManagerBinder binder = new ComputerManagerBinder();
private ComputerDatabaseManager dbManager; private ComputerDatabaseManager dbManager;
private AtomicInteger dbRefCount = new AtomicInteger(0); private final AtomicInteger dbRefCount = new AtomicInteger(0);
private IdentityManager idManager; private IdentityManager idManager;
private final LinkedList<PollingTuple> pollingTuples = new LinkedList<PollingTuple>(); private final LinkedList<PollingTuple> pollingTuples = new LinkedList<PollingTuple>();
private ComputerManagerListener listener = null; private ComputerManagerListener listener = null;
private AtomicInteger activePolls = new AtomicInteger(0); private final AtomicInteger activePolls = new AtomicInteger(0);
private boolean pollingActive = false; private boolean pollingActive = false;
private DiscoveryService.DiscoveryBinder discoveryBinder; private DiscoveryService.DiscoveryBinder discoveryBinder;
@@ -55,10 +64,8 @@ public class ComputerManagerService extends Service {
}; };
// Returns true if the details object was modified // Returns true if the details object was modified
private boolean runPoll(ComputerDetails details) private boolean runPoll(ComputerDetails details, boolean newPc)
{ {
boolean newPc = details.name.isEmpty();
if (!getLocalDatabaseReference()) { if (!getLocalDatabaseReference()) {
return false; return false;
} }
@@ -103,7 +110,7 @@ public class ComputerManagerService extends Service {
public void run() { public void run() {
while (!isInterrupted() && pollingActive) { while (!isInterrupted() && pollingActive) {
// Check if this poll has modified the details // Check if this poll has modified the details
runPoll(details); runPoll(details, false);
// Wait until the next polling interval // Wait until the next polling interval
try { try {
@@ -167,10 +174,6 @@ public class ComputerManagerService extends Service {
return ComputerManagerService.this.addComputerBlocking(addr); return ComputerManagerService.this.addComputerBlocking(addr);
} }
public void addComputer(InetAddress addr) {
ComputerManagerService.this.addComputer(addr);
}
public void removeComputer(String name) { public void removeComputer(String name) {
ComputerManagerService.this.removeComputer(name); ComputerManagerService.this.removeComputer(name);
} }
@@ -180,9 +183,25 @@ public class ComputerManagerService extends Service {
ComputerManagerService.this.onUnbind(null); ComputerManagerService.this.onUnbind(null);
} }
public ApplistPoller createAppListPoller(ComputerDetails computer) {
return new ApplistPoller(computer);
}
public String getUniqueId() { public String getUniqueId() {
return idManager.getUniqueId(); return idManager.getUniqueId();
} }
public ComputerDetails getComputer(UUID uuid) {
synchronized (pollingTuples) {
for (PollingTuple tuple : pollingTuples) {
if (uuid.equals(tuple.computer.uuid)) {
return tuple.computer;
}
}
}
return null;
}
} }
@Override @Override
@@ -213,7 +232,7 @@ public class ComputerManagerService extends Service {
@Override @Override
public void notifyComputerAdded(MdnsComputer computer) { public void notifyComputerAdded(MdnsComputer computer) {
// Kick off a serverinfo poll on this machine // Kick off a serverinfo poll on this machine
addComputer(computer.getAddress()); addComputerBlocking(computer.getAddress());
} }
@Override @Override
@@ -229,24 +248,15 @@ 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) { private void addTuple(ComputerDetails details) {
synchronized (pollingTuples) { synchronized (pollingTuples) {
for (PollingTuple tuple : pollingTuples) { for (PollingTuple tuple : pollingTuples) {
// Check if this is the same computer // Check if this is the same computer
if (tuple.computer == details || if (tuple.computer.uuid.equals(details.uuid)) {
tuple.computer.localIp.equals(details.localIp) || // Update details anyway in case this machine has been re-added by IP
tuple.computer.remoteIp.equals(details.remoteIp) || // after not being reachable by our existing information
tuple.computer.name.equals(details.name)) { tuple.computer.localIp = details.localIp;
tuple.computer.remoteIp = details.remoteIp;
// Start a polling thread if polling is active // Start a polling thread if polling is active
if (pollingActive && tuple.thread == null) { if (pollingActive && tuple.thread == null) {
@@ -273,13 +283,14 @@ public class ComputerManagerService extends Service {
ComputerDetails fakeDetails = new ComputerDetails(); ComputerDetails fakeDetails = new ComputerDetails();
fakeDetails.localIp = addr; fakeDetails.localIp = addr;
fakeDetails.remoteIp = addr; fakeDetails.remoteIp = addr;
fakeDetails.name = "";
// Block while we try to fill the details // Block while we try to fill the details
runPoll(fakeDetails); runPoll(fakeDetails, true);
// If the machine is reachable, it was successful // If the machine is reachable, it was successful
if (fakeDetails.state == ComputerDetails.State.ONLINE) { if (fakeDetails.state == ComputerDetails.State.ONLINE) {
LimeLog.info("New PC ("+fakeDetails.name+") is UUID "+fakeDetails.uuid);
// Start a polling thread for this machine // Start a polling thread for this machine
addTuple(fakeDetails); addTuple(fakeDetails);
return true; return true;
@@ -329,12 +340,22 @@ public class ComputerManagerService extends Service {
} }
} }
private ComputerDetails tryPollIp(InetAddress ipAddr) { private ComputerDetails tryPollIp(ComputerDetails details, InetAddress ipAddr) {
try { try {
NvHTTP http = new NvHTTP(ipAddr, idManager.getUniqueId(), NvHTTP http = new NvHTTP(ipAddr, idManager.getUniqueId(),
null, PlatformBinding.getCryptoProvider(ComputerManagerService.this)); null, PlatformBinding.getCryptoProvider(ComputerManagerService.this));
return http.getComputerDetails(); ComputerDetails newDetails = http.getComputerDetails();
// Check if this is the PC we expected
if (details.uuid != null && newDetails.uuid != null &&
!details.uuid.equals(newDetails.uuid)) {
// We got the wrong PC!
LimeLog.info("Polling returned the wrong PC!");
return null;
}
return newDetails;
} catch (Exception e) { } catch (Exception e) {
return null; return null;
} }
@@ -343,20 +364,26 @@ public class ComputerManagerService extends Service {
private boolean pollComputer(ComputerDetails details, boolean localFirst) { private boolean pollComputer(ComputerDetails details, boolean localFirst) {
ComputerDetails polledDetails; ComputerDetails polledDetails;
// 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;
}
if (localFirst) { if (localFirst) {
polledDetails = tryPollIp(details.localIp); polledDetails = tryPollIp(details, details.localIp);
} }
else { else {
polledDetails = tryPollIp(details.remoteIp); polledDetails = tryPollIp(details, details.remoteIp);
} }
if (polledDetails == null && !details.localIp.equals(details.remoteIp)) { if (polledDetails == null && !details.localIp.equals(details.remoteIp)) {
// Failed, so let's try the fallback // Failed, so let's try the fallback
if (!localFirst) { if (!localFirst) {
polledDetails = tryPollIp(details.localIp); polledDetails = tryPollIp(details, details.localIp);
} }
else { else {
polledDetails = tryPollIp(details.remoteIp); polledDetails = tryPollIp(details, details.remoteIp);
} }
// The fallback poll worked // The fallback poll worked
@@ -414,8 +441,8 @@ public class ComputerManagerService extends Service {
} }
for (ComputerDetails computer : dbManager.getAllComputers()) { for (ComputerDetails computer : dbManager.getAllComputers()) {
// Add this computer without a thread // Add tuples for each computer
pollingTuples.add(new PollingTuple(computer, null)); addTuple(computer);
} }
releaseLocalDatabaseReference(); releaseLocalDatabaseReference();
@@ -438,11 +465,112 @@ public class ComputerManagerService extends Service {
public IBinder onBind(Intent intent) { public IBinder onBind(Intent intent) {
return binder; return binder;
} }
public class ApplistPoller {
private Thread thread;
private final ComputerDetails computer;
private final Object pollEvent = new Object();
public ApplistPoller(ComputerDetails computer) {
this.computer = computer;
}
public void pollNow() {
synchronized (pollEvent) {
pollEvent.notify();
}
}
private boolean waitPollingDelay() {
try {
synchronized (pollEvent) {
pollEvent.wait(POLLING_PERIOD_MS);
}
} catch (InterruptedException e) {
return false;
}
return thread != null && !thread.isInterrupted();
}
public void start() {
thread = new Thread() {
@Override
public void run() {
do {
InetAddress selectedAddr;
// Can't poll if it's not online
if (computer.state != ComputerDetails.State.ONLINE) {
if (listener != null) {
listener.notifyComputerUpdated(computer);
}
continue;
}
// Can't poll if there's no UUID yet
if (computer.uuid == null) {
continue;
}
if (computer.reachability == ComputerDetails.Reachability.LOCAL) {
selectedAddr = computer.localIp;
}
else {
selectedAddr = computer.remoteIp;
}
NvHTTP http = new NvHTTP(selectedAddr, idManager.getUniqueId(),
null, PlatformBinding.getCryptoProvider(ComputerManagerService.this));
try {
// Query the app list from the server
String appList = http.getAppListRaw();
List<NvApp> list = NvHTTP.getAppListByReader(new StringReader(appList));
if (appList != null && !appList.isEmpty() && !list.isEmpty()) {
// Open the cache file
FileOutputStream cacheOut = CacheHelper.openCacheFileForOutput(getCacheDir(), "applist", computer.uuid.toString());
CacheHelper.writeStringToOutputStream(cacheOut, appList);
cacheOut.close();
// Update the computer
computer.rawAppList = appList;
// Notify that the app list has been updated
// and ensure that the thread is still active
if (listener != null && thread != null) {
listener.notifyComputerUpdated(computer);
}
}
else {
LimeLog.warning("Empty app list received from "+computer.uuid);
}
} catch (IOException e) {
e.printStackTrace();
} catch (XmlPullParserException e) {
e.printStackTrace();
}
} while (waitPollingDelay());
}
};
thread.start();
}
public void stop() {
if (thread != null) {
thread.interrupt();
// Don't join here because we might be blocked on network I/O
thread = null;
}
}
}
} }
class PollingTuple { class PollingTuple {
public Thread thread; public Thread thread;
public ComputerDetails computer; public final ComputerDetails computer;
public PollingTuple(ComputerDetails computer, Thread thread) { public PollingTuple(ComputerDetails computer, Thread thread) {
this.computer = computer; this.computer = computer;
@@ -70,7 +70,7 @@ public class DiscoveryService extends Service {
}); });
} }
private DiscoveryBinder binder = new DiscoveryBinder(); private final DiscoveryBinder binder = new DiscoveryBinder();
@Override @Override
public IBinder onBind(Intent intent) { public IBinder onBind(Intent intent) {
@@ -1,16 +1,25 @@
package com.limelight.grid; package com.limelight.grid;
import android.content.Context; import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.os.AsyncTask;
import android.widget.ImageView; import android.widget.ImageView;
import android.widget.TextView; import android.widget.TextView;
import com.koushikdutta.async.future.FutureCallback; import com.koushikdutta.async.future.FutureCallback;
import com.koushikdutta.ion.Ion; import com.koushikdutta.ion.Ion;
import com.limelight.AppView; import com.limelight.AppView;
import com.limelight.LimeLog;
import com.limelight.R; import com.limelight.R;
import com.limelight.binding.PlatformBinding; import com.limelight.binding.PlatformBinding;
import com.limelight.nvstream.http.ComputerDetails;
import com.limelight.nvstream.http.LimelightCryptoProvider; import com.limelight.nvstream.http.LimelightCryptoProvider;
import com.limelight.utils.CacheHelper;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.InetAddress; import java.net.InetAddress;
import java.net.Socket; import java.net.Socket;
import java.security.KeyManagementException; import java.security.KeyManagementException;
@@ -18,7 +27,10 @@ import java.security.NoSuchAlgorithmException;
import java.security.Principal; import java.security.Principal;
import java.security.PrivateKey; import java.security.PrivateKey;
import java.security.SecureRandom; import java.security.SecureRandom;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap; import java.util.HashMap;
import java.util.UUID;
import java.util.concurrent.Future; import java.util.concurrent.Future;
import javax.net.ssl.HostnameVerifier; import javax.net.ssl.HostnameVerifier;
@@ -32,16 +44,16 @@ import java.security.cert.X509Certificate;
public class AppGridAdapter extends GenericGridAdapter<AppView.AppObject> { public class AppGridAdapter extends GenericGridAdapter<AppView.AppObject> {
private InetAddress address; private final ComputerDetails computer;
private String uniqueId; private final String uniqueId;
private LimelightCryptoProvider cryptoProvider; private final LimelightCryptoProvider cryptoProvider;
private SSLContext sslContext; private final SSLContext sslContext;
private final HashMap<ImageView, Future> pendingRequests = new HashMap<ImageView, Future>(); private final HashMap<ImageView, Future> pendingRequests = new HashMap<ImageView, Future>();
public AppGridAdapter(Context context, InetAddress address, String uniqueId) throws NoSuchAlgorithmException, KeyManagementException { public AppGridAdapter(Context context, boolean listMode, boolean small, ComputerDetails computer, String uniqueId) throws NoSuchAlgorithmException, KeyManagementException {
super(context, R.layout.app_grid_item, R.drawable.image_loading); super(context, listMode ? R.layout.simple_row : (small ? R.layout.app_grid_item_small : R.layout.app_grid_item), R.drawable.image_loading);
this.address = address; this.computer = computer;
this.uniqueId = uniqueId; this.uniqueId = uniqueId;
cryptoProvider = PlatformBinding.getCryptoProvider(context); cryptoProvider = PlatformBinding.getCryptoProvider(context);
@@ -50,7 +62,7 @@ public class AppGridAdapter extends GenericGridAdapter<AppView.AppObject> {
sslContext.init(ourKeyman, trustAllCerts, new SecureRandom()); sslContext.init(ourKeyman, trustAllCerts, new SecureRandom());
} }
TrustManager[] trustAllCerts = new TrustManager[] { private final TrustManager[] trustAllCerts = new TrustManager[] {
new X509TrustManager() { new X509TrustManager() {
public X509Certificate[] getAcceptedIssuers() { public X509Certificate[] getAcceptedIssuers() {
return new X509Certificate[0]; return new X509Certificate[0];
@@ -59,7 +71,7 @@ public class AppGridAdapter extends GenericGridAdapter<AppView.AppObject> {
public void checkServerTrusted(X509Certificate[] certs, String authType) {} public void checkServerTrusted(X509Certificate[] certs, String authType) {}
}}; }};
KeyManager[] ourKeyman = new KeyManager[] { private final KeyManager[] ourKeyman = new KeyManager[] {
new X509KeyManager() { new X509KeyManager() {
public String chooseClientAlias(String[] keyTypes, public String chooseClientAlias(String[] keyTypes,
Principal[] issuers, Socket socket) { Principal[] issuers, Socket socket) {
@@ -94,8 +106,27 @@ public class AppGridAdapter extends GenericGridAdapter<AppView.AppObject> {
public boolean verify(String hostname, SSLSession session) { return true; } public boolean verify(String hostname, SSLSession session) { return true; }
}; };
private void sortList() {
Collections.sort(itemList, new Comparator<AppView.AppObject>() {
@Override
public int compare(AppView.AppObject lhs, AppView.AppObject rhs) {
return lhs.app.getAppName().compareTo(rhs.app.getAppName());
}
});
}
private InetAddress getCurrentAddress() {
if (computer.reachability == ComputerDetails.Reachability.LOCAL) {
return computer.localIp;
}
else {
return computer.remoteIp;
}
}
public void addApp(AppView.AppObject app) { public void addApp(AppView.AppObject app) {
itemList.add(app); itemList.add(app);
sortList();
} }
public void abortPendingRequests() { public void abortPendingRequests() {
@@ -107,8 +138,10 @@ public class AppGridAdapter extends GenericGridAdapter<AppView.AppObject> {
} }
for (Future f : tempMap.values()) { for (Future f : tempMap.values()) {
if (!f.isCancelled() && !f.isDone()) {
f.cancel(true); f.cancel(true);
} }
}
synchronized (pendingRequests) { synchronized (pendingRequests) {
// Remove cancelled requests // Remove cancelled requests
@@ -118,30 +151,25 @@ public class AppGridAdapter extends GenericGridAdapter<AppView.AppObject> {
} }
} }
@Override // TODO: Handle pruning of bitmap cache
public boolean populateImageView(final ImageView imgView, AppView.AppObject obj) { 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();
}
}
// Set SSL contexts correctly to allow us to authenticate
Ion.getDefault(imgView.getContext()).getHttpClient().getSSLSocketMiddleware().setTrustManagers(trustAllCerts);
Ion.getDefault(imgView.getContext()).getHttpClient().getSSLSocketMiddleware().setSSLContext(sslContext);
// Set off the deferred image load
synchronized (pendingRequests) {
Future f = Ion.with(imgView)
.placeholder(defaultImageRes)
.error(defaultImageRes)
.load("https://" + address.getHostAddress() + ":47984/appasset?uniqueid=" + uniqueId + "&appid=" +
obj.app.getAppId() + "&AssetType=2&AssetIdx=0")
.setCallback(new FutureCallback<ImageView>() {
@Override @Override
public void onCompleted(Exception e, ImageView result) { public boolean populateImageView(final ImageView imgView, final AppView.AppObject obj) {
synchronized (pendingRequests) { // Clear existing contents of the image view
pendingRequests.remove(imgView); imgView.setAlpha(0.0f);
}
} // Check the on-disk cache
}); new ImageCacheRequest(imgView, obj.app.getAppId()).execute();
pendingRequests.put(imgView, f);
}
return true; return true;
} }
@@ -166,4 +194,96 @@ public class AppGridAdapter extends GenericGridAdapter<AppView.AppObject> {
// No overlay // No overlay
return false; return false;
} }
private class ImageCacheRequest extends AsyncTask<Void, Void, Bitmap> {
private final ImageView view;
private final 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 ignored) {}
}
}
return null;
}
private void fadeInImage(ImageView view) {
view.animate().alpha(1.0f).setDuration(250).start();
}
@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);
fadeInImage(view);
}
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);
fadeInImage(view);
// Set SSL contexts correctly to allow us to authenticate
Ion.getDefault(context).getHttpClient().getSSLSocketMiddleware().setTrustManagers(trustAllCerts);
Ion.getDefault(context).getHttpClient().getSSLSocketMiddleware().setSSLContext(sslContext);
Ion.getDefault(context).getHttpClient().getSSLSocketMiddleware().setHostnameVerifier(hv);
// 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, final Bitmap result) {
synchronized (pendingRequests) {
pendingRequests.remove(view);
}
if (result != null) {
// Make the view visible now
view.setImageBitmap(result);
fadeInImage(view);
// Populate the disk cache if we got an image back.
// We do it in a new thread because it can be very expensive, especially
// when we do the initial load where lots of disk I/O is happening at once.
new Thread() {
@Override
public void run() {
populateBitmapCache(computer.uuid, appId, result);
}
}.start();
}
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; import java.util.ArrayList;
public abstract class GenericGridAdapter<T> extends BaseAdapter { public abstract class GenericGridAdapter<T> extends BaseAdapter {
protected Context context; protected final Context context;
protected int defaultImageRes; protected final int defaultImageRes;
protected int layoutId; protected final int layoutId;
protected ArrayList<T> itemList = new ArrayList<T>(); protected final ArrayList<T> itemList = new ArrayList<T>();
protected LayoutInflater inflater; protected final LayoutInflater inflater;
public GenericGridAdapter(Context context, int layoutId, int defaultImageRes) { public GenericGridAdapter(Context context, int layoutId, int defaultImageRes) {
this.context = context; this.context = context;
@@ -60,18 +60,22 @@ public abstract class GenericGridAdapter<T> extends BaseAdapter {
ImageView overlayView = (ImageView) convertView.findViewById(R.id.grid_overlay); ImageView overlayView = (ImageView) convertView.findViewById(R.id.grid_overlay);
TextView txtView = (TextView) convertView.findViewById(R.id.grid_text); TextView txtView = (TextView) convertView.findViewById(R.id.grid_text);
if (imgView != null) {
if (!populateImageView(imgView, itemList.get(i))) { if (!populateImageView(imgView, itemList.get(i))) {
imgView.setImageResource(defaultImageRes); imgView.setImageResource(defaultImageRes);
} }
}
if (!populateTextView(txtView, itemList.get(i))) { if (!populateTextView(txtView, itemList.get(i))) {
txtView.setText(itemList.get(i).toString()); txtView.setText(itemList.get(i).toString());
} }
if (overlayView != null) {
if (!populateOverlayView(overlayView, itemList.get(i))) { if (!populateOverlayView(overlayView, itemList.get(i))) {
overlayView.setVisibility(View.INVISIBLE); overlayView.setVisibility(View.INVISIBLE);
} }
else { else {
overlayView.setVisibility(View.VISIBLE); overlayView.setVisibility(View.VISIBLE);
} }
}
return convertView; return convertView;
} }
@@ -8,14 +8,27 @@ import com.limelight.PcView;
import com.limelight.R; import com.limelight.R;
import com.limelight.nvstream.http.ComputerDetails; import com.limelight.nvstream.http.ComputerDetails;
import java.util.Collections;
import java.util.Comparator;
public class PcGridAdapter extends GenericGridAdapter<PcView.ComputerObject> { public class PcGridAdapter extends GenericGridAdapter<PcView.ComputerObject> {
public PcGridAdapter(Context context) { public PcGridAdapter(Context context, boolean listMode, boolean small) {
super(context, R.layout.pc_grid_item, R.drawable.computer); super(context, listMode ? R.layout.simple_row : (small ? R.layout.pc_grid_item_small : R.layout.pc_grid_item), R.drawable.computer);
} }
public void addComputer(PcView.ComputerObject computer) { public void addComputer(PcView.ComputerObject computer) {
itemList.add(computer); itemList.add(computer);
sortList();
}
private void sortList() {
Collections.sort(itemList, new Comparator<PcView.ComputerObject>() {
@Override
public int compare(PcView.ComputerObject lhs, PcView.ComputerObject rhs) {
return lhs.details.name.compareTo(rhs.details.name);
}
});
} }
public boolean removeComputer(PcView.ComputerObject computer) { public boolean removeComputer(PcView.ComputerObject computer) {
@@ -2,6 +2,7 @@ package com.limelight.preferences;
import java.net.InetAddress; import java.net.InetAddress;
import java.net.UnknownHostException; import java.net.UnknownHostException;
import java.util.Locale;
import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.LinkedBlockingQueue;
import com.limelight.computers.ComputerManagerService; import com.limelight.computers.ComputerManagerService;
@@ -13,21 +14,24 @@ import com.limelight.utils.UiHelper;
import android.app.Activity; import android.app.Activity;
import android.app.Service; import android.app.Service;
import android.content.ComponentName; import android.content.ComponentName;
import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.content.ServiceConnection; import android.content.ServiceConnection;
import android.content.res.Configuration;
import android.os.Bundle; import android.os.Bundle;
import android.os.IBinder; import android.os.IBinder;
import android.view.KeyEvent; import android.view.KeyEvent;
import android.view.inputmethod.EditorInfo; import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputMethodManager;
import android.widget.TextView; import android.widget.TextView;
import android.widget.Toast; import android.widget.Toast;
public class AddComputerManually extends Activity { public class AddComputerManually extends Activity {
private TextView hostText; private TextView hostText;
private ComputerManagerService.ComputerManagerBinder managerBinder; private ComputerManagerService.ComputerManagerBinder managerBinder;
private LinkedBlockingQueue<String> computersToAdd = new LinkedBlockingQueue<String>(); private final LinkedBlockingQueue<String> computersToAdd = new LinkedBlockingQueue<String>();
private Thread addThread; private Thread addThread;
private ServiceConnection serviceConnection = new ServiceConnection() { private final ServiceConnection serviceConnection = new ServiceConnection() {
public void onServiceConnected(ComponentName className, final IBinder binder) { public void onServiceConnected(ComponentName className, final IBinder binder) {
managerBinder = ((ComputerManagerService.ComputerManagerBinder)binder); managerBinder = ((ComputerManagerService.ComputerManagerBinder)binder);
startAddThread(); startAddThread();
@@ -132,6 +136,13 @@ public class AddComputerManually extends Activity {
protected void onCreate(Bundle savedInstanceState) { protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
String locale = PreferenceConfiguration.readPreferences(this).language;
if (!locale.equals(PreferenceConfiguration.DEFAULT_LANGUAGE)) {
Configuration config = new Configuration(getResources().getConfiguration());
config.locale = new Locale(locale);
getResources().updateConfiguration(config, getResources().getDisplayMetrics());
}
setContentView(R.layout.activity_add_computer_manually); setContentView(R.layout.activity_add_computer_manually);
UiHelper.notifyNewRootView(this); UiHelper.notifyNewRootView(this);
@@ -142,14 +153,21 @@ public class AddComputerManually extends Activity {
@Override @Override
public boolean onEditorAction(TextView textView, int actionId, KeyEvent keyEvent) { public boolean onEditorAction(TextView textView, int actionId, KeyEvent keyEvent) {
if (actionId == EditorInfo.IME_ACTION_DONE || if (actionId == EditorInfo.IME_ACTION_DONE ||
(keyEvent != null &&
keyEvent.getAction() == KeyEvent.ACTION_DOWN && keyEvent.getAction() == KeyEvent.ACTION_DOWN &&
keyEvent.getKeyCode() == KeyEvent.KEYCODE_ENTER) { keyEvent.getKeyCode() == KeyEvent.KEYCODE_ENTER)) {
if (hostText.getText().length() == 0) { if (hostText.getText().length() == 0) {
Toast.makeText(AddComputerManually.this, getResources().getString(R.string.addpc_enter_ip), Toast.LENGTH_LONG).show(); Toast.makeText(AddComputerManually.this, getResources().getString(R.string.addpc_enter_ip), Toast.LENGTH_LONG).show();
return true; return true;
} }
computersToAdd.add(hostText.getText().toString()); 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; return false;
@@ -2,6 +2,7 @@ package com.limelight.preferences;
import android.content.Context; import android.content.Context;
import android.content.SharedPreferences; import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.preference.PreferenceManager; import android.preference.PreferenceManager;
public class PreferenceConfiguration { public class PreferenceConfiguration {
@@ -13,11 +14,15 @@ public class PreferenceConfiguration {
private static final String DISABLE_TOASTS_PREF_STRING = "checkbox_disable_warnings"; private static final String DISABLE_TOASTS_PREF_STRING = "checkbox_disable_warnings";
private static final String HOST_AUDIO_PREF_STRING = "checkbox_host_audio"; private static final String HOST_AUDIO_PREF_STRING = "checkbox_host_audio";
private static final String DEADZONE_PREF_STRING = "seekbar_deadzone"; private static final String DEADZONE_PREF_STRING = "seekbar_deadzone";
private static final String LANGUAGE_PREF_STRING = "list_languages";
private static final String LIST_MODE_PREF_STRING = "checkbox_list_mode";
private static final String SMALL_ICONS_PREF_STRING = "checkbox_small_icon_mode";
private static final String MULTI_CONTROLLER_PREF_STRING = "checkbox_multi_controller";
private static final int BITRATE_DEFAULT_720_30 = 5; private static final int BITRATE_DEFAULT_720_30 = 5;
private static final int BITRATE_DEFAULT_720_60 = 10; private static final int BITRATE_DEFAULT_720_60 = 10;
private static final int BITRATE_DEFAULT_1080_30 = 10; private static final int BITRATE_DEFAULT_1080_30 = 10;
private static final int BITRATE_DEFAULT_1080_60 = 30; private static final int BITRATE_DEFAULT_1080_60 = 20;
private static final String DEFAULT_RES_FPS = "720p60"; private static final String DEFAULT_RES_FPS = "720p60";
private static final String DEFAULT_DECODER = "auto"; private static final String DEFAULT_DECODER = "auto";
@@ -27,6 +32,9 @@ public class PreferenceConfiguration {
private static final boolean DEFAULT_DISABLE_TOASTS = false; private static final boolean DEFAULT_DISABLE_TOASTS = false;
private static final boolean DEFAULT_HOST_AUDIO = false; private static final boolean DEFAULT_HOST_AUDIO = false;
private static final int DEFAULT_DEADZONE = 15; private static final int DEFAULT_DEADZONE = 15;
public static final String DEFAULT_LANGUAGE = "default";
private static final boolean DEFAULT_LIST_MODE = false;
private static final boolean DEFAULT_MULTI_CONTROLLER = true;
public static final int FORCE_HARDWARE_DECODER = -1; public static final int FORCE_HARDWARE_DECODER = -1;
public static final int AUTOSELECT_DECODER = 0; public static final int AUTOSELECT_DECODER = 0;
@@ -37,6 +45,8 @@ public class PreferenceConfiguration {
public int decoder; public int decoder;
public int deadzonePercentage; public int deadzonePercentage;
public boolean stretchVideo, enableSops, playHostAudio, disableWarnings; public boolean stretchVideo, enableSops, playHostAudio, disableWarnings;
public String language;
public boolean listMode, smallIconMode, multiController;
public static int getDefaultBitrate(String resFpsString) { public static int getDefaultBitrate(String resFpsString) {
if (resFpsString.equals("720p30")) { if (resFpsString.equals("720p30")) {
@@ -57,6 +67,17 @@ public class PreferenceConfiguration {
} }
} }
public static boolean getDefaultSmallMode(Context context) {
PackageManager manager = context.getPackageManager();
if (manager != null && manager.hasSystemFeature(PackageManager.FEATURE_TELEVISION)) {
// TVs shouldn't use small mode by default
return false;
}
// Use small mode on anything smaller than a 7" tablet
return context.getResources().getConfiguration().smallestScreenWidthDp < 600;
}
public static int getDefaultBitrate(Context context) { public static int getDefaultBitrate(Context context) {
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
@@ -135,11 +156,16 @@ public class PreferenceConfiguration {
config.deadzonePercentage = prefs.getInt(DEADZONE_PREF_STRING, DEFAULT_DEADZONE); config.deadzonePercentage = prefs.getInt(DEADZONE_PREF_STRING, DEFAULT_DEADZONE);
config.language = prefs.getString(LANGUAGE_PREF_STRING, DEFAULT_LANGUAGE);
// Checkbox preferences // Checkbox preferences
config.disableWarnings = prefs.getBoolean(DISABLE_TOASTS_PREF_STRING, DEFAULT_DISABLE_TOASTS); config.disableWarnings = prefs.getBoolean(DISABLE_TOASTS_PREF_STRING, DEFAULT_DISABLE_TOASTS);
config.enableSops = prefs.getBoolean(SOPS_PREF_STRING, DEFAULT_SOPS); config.enableSops = prefs.getBoolean(SOPS_PREF_STRING, DEFAULT_SOPS);
config.stretchVideo = prefs.getBoolean(STRETCH_PREF_STRING, DEFAULT_STRETCH); config.stretchVideo = prefs.getBoolean(STRETCH_PREF_STRING, DEFAULT_STRETCH);
config.playHostAudio = prefs.getBoolean(HOST_AUDIO_PREF_STRING, DEFAULT_HOST_AUDIO); config.playHostAudio = prefs.getBoolean(HOST_AUDIO_PREF_STRING, DEFAULT_HOST_AUDIO);
config.listMode = prefs.getBoolean(LIST_MODE_PREF_STRING, DEFAULT_LIST_MODE);
config.smallIconMode = prefs.getBoolean(SMALL_ICONS_PREF_STRING, getDefaultSmallMode(context));
config.multiController = prefs.getBoolean(MULTI_CONTROLLER_PREF_STRING, DEFAULT_MULTI_CONTROLLER);
return config; return config;
} }
@@ -20,10 +20,14 @@ public class SeekBarPreference extends DialogPreference
private SeekBar seekBar; private SeekBar seekBar;
private TextView valueText; private TextView valueText;
private Context context; private final Context context;
private String dialogMessage, suffix; private final String dialogMessage;
private int defaultValue, maxValue, minValue, currentValue; private final String suffix;
private final int defaultValue;
private final int maxValue;
private final int minValue;
private int currentValue;
public SeekBarPreference(Context context, AttributeSet attrs) { public SeekBarPreference(Context context, AttributeSet attrs) {
super(context, attrs); super(context, attrs);
@@ -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) { public void setProgress(int progress) {
this.currentValue = progress; this.currentValue = progress;
if (seekBar != null) { if (seekBar != null) {
@@ -0,0 +1,21 @@
package com.limelight.preferences;
import android.content.Context;
import android.content.res.TypedArray;
import android.preference.CheckBoxPreference;
import android.util.AttributeSet;
public class SmallIconCheckboxPreference extends CheckBoxPreference {
public SmallIconCheckboxPreference(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
public SmallIconCheckboxPreference(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
protected Object onGetDefaultValue(TypedArray a, int index) {
return PreferenceConfiguration.getDefaultSmallMode(getContext());
}
}
@@ -1,20 +1,32 @@
package com.limelight.preferences; package com.limelight.preferences;
import android.content.Intent;
import android.content.SharedPreferences; import android.content.SharedPreferences;
import android.content.res.Configuration;
import android.os.Bundle; import android.os.Bundle;
import android.app.Activity; import android.app.Activity;
import android.preference.Preference; import android.preference.Preference;
import android.preference.PreferenceFragment; import android.preference.PreferenceFragment;
import android.preference.PreferenceManager; import android.preference.PreferenceManager;
import com.limelight.PcView;
import com.limelight.R; import com.limelight.R;
import com.limelight.utils.UiHelper; import com.limelight.utils.UiHelper;
import java.util.Locale;
public class StreamSettings extends Activity { public class StreamSettings extends Activity {
@Override @Override
protected void onCreate(Bundle savedInstanceState) { protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
String locale = PreferenceConfiguration.readPreferences(this).language;
if (!locale.equals(PreferenceConfiguration.DEFAULT_LANGUAGE)) {
Configuration config = new Configuration(getResources().getConfiguration());
config.locale = new Locale(locale);
getResources().updateConfiguration(config, getResources().getDisplayMetrics());
}
setContentView(R.layout.activity_stream_settings); setContentView(R.layout.activity_stream_settings);
getFragmentManager().beginTransaction().replace( getFragmentManager().beginTransaction().replace(
R.id.stream_settings, new SettingsFragment() R.id.stream_settings, new SettingsFragment()
@@ -23,6 +35,16 @@ public class StreamSettings extends Activity {
UiHelper.notifyNewRootView(this); UiHelper.notifyNewRootView(this);
} }
@Override
public void onBackPressed() {
finish();
// Restart the PC view to apply UI changes
Intent intent = new Intent(this, PcView.class);
intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK | Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(intent, null);
}
public static class SettingsFragment extends PreferenceFragment { public static class SettingsFragment extends PreferenceFragment {
@Override @Override
public void onCreate(Bundle savedInstanceState) { public void onCreate(Bundle savedInstanceState) {
@@ -0,0 +1,35 @@
package com.limelight.ui;
import android.app.Activity;
import android.app.Fragment;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AbsListView;
import com.limelight.R;
public class AdapterFragment extends Fragment {
private AdapterFragmentCallbacks callbacks;
@Override
public void onAttach(Activity activity) {
super.onAttach(activity);
callbacks = (AdapterFragmentCallbacks) activity;
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
return inflater.inflate(callbacks.getAdapterFragmentLayoutId(), container, false);
}
@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
callbacks.receiveAbsListView((AbsListView) getView().findViewById(R.id.fragmentView));
}
}
@@ -0,0 +1,8 @@
package com.limelight.ui;
import android.widget.AbsListView;
public interface AdapterFragmentCallbacks {
public int getAdapterFragmentLayoutId();
public void receiveAbsListView(AbsListView gridView);
}
@@ -0,0 +1,5 @@
package com.limelight.ui;
public interface GameGestures {
public void showKeyboard();
}
@@ -0,0 +1,55 @@
package com.limelight.utils;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.Reader;
public class CacheHelper {
private static File openPath(boolean createPath, File root, String... path) {
File f = root;
for (int i = 0; i < path.length; i++) {
String component = path[i];
if (i == path.length - 1) {
// This is the file component so now we create parent directories
if (createPath) {
f.mkdirs();
}
}
f = new File(f, component);
}
return f;
}
public static FileInputStream openCacheFileForInput(File root, String... path) throws FileNotFoundException {
return new FileInputStream(openPath(false, root, path));
}
public static FileOutputStream openCacheFileForOutput(File root, String... path) throws FileNotFoundException {
return new FileOutputStream(openPath(true, root, path));
}
public static String readInputStreamToString(InputStream in) throws IOException {
Reader r = new InputStreamReader(in);
StringBuilder sb = new StringBuilder();
char[] buf = new char[256];
int bytesRead;
while ((bytesRead = r.read(buf)) != -1) {
sb.append(buf, 0, bytesRead);
}
return sb.toString();
}
public static void writeStringToOutputStream(OutputStream out, String str) throws IOException {
out.write(str.getBytes("UTF-8"));
}
}
@@ -7,15 +7,16 @@ import android.app.AlertDialog;
import android.content.DialogInterface; import android.content.DialogInterface;
public class Dialog implements Runnable { public class Dialog implements Runnable {
private String title, message; private final String title;
private Activity activity; private final String message;
private boolean endAfterDismiss; private final Activity activity;
private final boolean endAfterDismiss;
private AlertDialog alert; private AlertDialog alert;
private static final ArrayList<Dialog> rundownDialogs = new ArrayList<Dialog>(); private static final ArrayList<Dialog> rundownDialogs = new ArrayList<Dialog>();
public Dialog(Activity activity, String title, String message, boolean endAfterDismiss) private Dialog(Activity activity, String title, String message, boolean endAfterDismiss)
{ {
this.activity = activity; this.activity = activity;
this.title = title; this.title = title;
@@ -9,14 +9,15 @@ import android.content.DialogInterface;
import android.content.DialogInterface.OnCancelListener; import android.content.DialogInterface.OnCancelListener;
public class SpinnerDialog implements Runnable,OnCancelListener { public class SpinnerDialog implements Runnable,OnCancelListener {
private String title, message; private final String title;
private Activity activity; private final String message;
private final Activity activity;
private ProgressDialog progress; private ProgressDialog progress;
private boolean finish; private final boolean finish;
private static final ArrayList<SpinnerDialog> rundownDialogs = new ArrayList<SpinnerDialog>(); private static final ArrayList<SpinnerDialog> rundownDialogs = new ArrayList<SpinnerDialog>();
public SpinnerDialog(Activity activity, String title, String message, boolean finish) private SpinnerDialog(Activity activity, String title, String message, boolean finish)
{ {
this.activity = activity; this.activity = activity;
this.title = title; this.title = title;
+12 -12
View File
@@ -69,9 +69,6 @@ int nv_avc_init(int width, int height, int perf_lvl, int thread_count) {
return -1; return -1;
} }
// Show frames even before a reference frame
decoder_ctx->flags2 |= CODEC_FLAG2_SHOW_ALL;
if (perf_lvl & DISABLE_LOOP_FILTER) { if (perf_lvl & DISABLE_LOOP_FILTER) {
// Skip the loop filter for performance reasons // Skip the loop filter for performance reasons
decoder_ctx->skip_loop_filter = AVDISCARD_ALL; decoder_ctx->skip_loop_filter = AVDISCARD_ALL;
@@ -370,17 +367,20 @@ int nv_avc_decode(unsigned char* indata, int inlen) {
// Only copy the picture at the end of decoding the packet // Only copy the picture at the end of decoding the packet
if (got_pic) { if (got_pic) {
// Clone the current decode frame outside of the mutex
AVFrame* new_frame = av_frame_clone(dec_frame);
AVFrame* old_frame;
// Swap it in under lock
pthread_mutex_lock(&mutex); pthread_mutex_lock(&mutex);
old_frame = yuv_frame;
// Only clone this frame if the last frame was taken. yuv_frame = new_frame;
// This saves on extra copies for frames that don't get
// rendered.
if (yuv_frame == NULL) {
// Clone a new frame
yuv_frame = av_frame_clone(dec_frame);
}
pthread_mutex_unlock(&mutex); pthread_mutex_unlock(&mutex);
// Free the old frame outside of the mutex
if (old_frame != NULL) {
av_frame_free(&old_frame);
}
} }
return err < 0 ? err : 0; return err < 0 ? err : 0;
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle" >
<stroke android:width="1dip" android:color="#ffffff"/>
</shape>
@@ -33,12 +33,10 @@
android:text="@string/searching_pc"/> android:text="@string/searching_pc"/>
</RelativeLayout> </RelativeLayout>
<GridView <FrameLayout
android:id="@+id/pcGridView" android:id="@+id/pcFragmentContainer"
android:layout_width="fill_parent" android:layout_width="fill_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:numColumns="auto_fit"
android:columnWidth="160dp"
android:gravity="center" android:gravity="center"
android:layout_alignParentBottom="true" android:layout_alignParentBottom="true"
android:layout_alignParentTop="true" android:layout_alignParentTop="true"
@@ -33,12 +33,10 @@
android:text="@string/searching_pc"/> android:text="@string/searching_pc"/>
</RelativeLayout> </RelativeLayout>
<GridView <FrameLayout
android:id="@+id/pcGridView" android:id="@+id/pcFragmentContainer"
android:layout_width="fill_parent" android:layout_width="fill_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:numColumns="auto_fit"
android:columnWidth="160dp"
android:gravity="center" android:gravity="center"
android:layout_toLeftOf="@+id/manuallyAddPc" android:layout_toLeftOf="@+id/manuallyAddPc"
android:layout_toStartOf="@+id/manuallyAddPc" android:layout_toStartOf="@+id/manuallyAddPc"
@@ -8,21 +8,17 @@
android:paddingTop="@dimen/activity_vertical_margin" android:paddingTop="@dimen/activity_vertical_margin"
tools:context=".AppView" > tools:context=".AppView" >
<GridView <FrameLayout
android:id="@+id/appGridView" android:id="@+id/appFragmentContainer"
android:layout_width="fill_parent" android:layout_width="fill_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:numColumns="auto_fit"
android:columnWidth="160dp"
android:stretchMode="spacingWidth"
android:gravity="center" android:gravity="center"
android:layout_alignParentLeft="true" android:layout_alignParentLeft="true"
android:layout_alignParentStart="true" android:layout_alignParentStart="true"
android:layout_alignParentRight="true" android:layout_alignParentRight="true"
android:layout_alignParentEnd="true" android:layout_alignParentEnd="true"
android:layout_alignParentBottom="true" android:layout_alignParentBottom="true"
android:layout_below="@+id/appListText"> android:layout_below="@+id/appListText"/>
</GridView>
<TextView <TextView
android:id="@+id/appListText" android:id="@+id/appListText"
@@ -30,6 +26,7 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_centerHorizontal="true" android:layout_centerHorizontal="true"
android:layout_alignParentTop="true" android:layout_alignParentTop="true"
android:gravity="center"
android:paddingTop="0dp" android:paddingTop="0dp"
android:paddingBottom="10dp" android:paddingBottom="10dp"
android:textSize="28sp"/> android:textSize="28sp"/>
@@ -0,0 +1,41 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="10dp">
<RelativeLayout
android:id="@+id/grid_image_layout"
android:layout_centerHorizontal="true"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<ImageView
android:id="@+id/grid_image"
android:cropToPadding="false"
android:scaleType="fitXY"
android:layout_centerHorizontal="true"
android:layout_width="100dp"
android:layout_height="117dp">
</ImageView>
<ImageView
android:id="@+id/grid_overlay"
android:layout_centerHorizontal="true"
android:layout_centerVertical="true"
android:layout_width="33dp"
android:layout_height="33dp">
</ImageView>
</RelativeLayout>
<TextView
android:id="@+id/grid_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/grid_image_layout"
android:layout_marginTop="10dp"
android:layout_centerHorizontal="true"
android:gravity="center"
android:singleLine="true"
android:ellipsize="marquee"
android:marqueeRepeatLimit="marquee_forever"
android:scrollHorizontally="true"
android:textSize="14sp" >
</TextView>
</RelativeLayout>
+15
View File
@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical" android:layout_width="match_parent"
android:layout_height="match_parent">
<GridView
android:id="@+id/fragmentView"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:numColumns="auto_fit"
android:columnWidth="160dp"
android:stretchMode="spacingWidth"
android:focusable="true"
android:focusableInTouchMode="true"
android:gravity="center"/>
</LinearLayout>
@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical" android:layout_width="match_parent"
android:layout_height="match_parent">
<GridView
android:id="@+id/fragmentView"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:numColumns="auto_fit"
android:columnWidth="105dp"
android:stretchMode="spacingWidth"
android:focusable="true"
android:focusableInTouchMode="true"
android:gravity="center"/>
</LinearLayout>
+19
View File
@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical" android:layout_width="match_parent"
android:layout_height="match_parent">
<ListView
android:id="@+id/fragmentView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@drawable/list_view_unselected"
android:fastScrollEnabled="true"
android:longClickable="false"
android:focusable="true"
android:focusableInTouchMode="true"
android:stackFromBottom="false" >
</ListView>
</LinearLayout>
@@ -0,0 +1,37 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="15dp">
<RelativeLayout
android:id="@+id/grid_image_layout"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<ImageView
android:id="@+id/grid_image"
android:layout_centerHorizontal="true"
android:layout_width="100dp"
android:layout_height="67dp">
</ImageView>
<ImageView
android:id="@+id/grid_overlay"
android:layout_marginTop="10dp"
android:layout_marginLeft="42dp"
android:layout_marginStart="42dp"
android:layout_marginRight="13dp"
android:layout_marginEnd="13dp"
android:layout_width="33dp"
android:layout_height="33dp">
</ImageView>
</RelativeLayout>
<TextView
android:id="@+id/grid_text"
android:layout_width="100dp"
android:layout_height="wrap_content"
android:layout_below="@id/grid_image_layout"
android:layout_marginTop="10dp"
android:layout_centerHorizontal="true"
android:gravity="center"
android:textSize="14sp" >
</TextView>
</RelativeLayout>
+14
View File
@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical" android:layout_width="match_parent"
android:layout_height="match_parent">
<GridView
android:id="@+id/fragmentView"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:numColumns="auto_fit"
android:columnWidth="160dp"
android:focusable="true"
android:focusableInTouchMode="true"
android:gravity="center"/>
</LinearLayout>
@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical" android:layout_width="match_parent"
android:layout_height="match_parent">
<GridView
android:id="@+id/fragmentView"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:numColumns="auto_fit"
android:columnWidth="105dp"
android:focusable="true"
android:focusableInTouchMode="true"
android:gravity="center"/>
</LinearLayout>
+15
View File
@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:padding="10dp"
android:layout_height="wrap_content">
<TextView
android:id="@+id/grid_text"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:textIsSelectable="false"
android:textSize="16sp" >
</TextView>
</LinearLayout>
+17 -3
View File
@@ -56,6 +56,9 @@
<!-- General strings --> <!-- General strings -->
<string name="ip_hint">Indirizzo IP del PC</string> <string name="ip_hint">Indirizzo IP del PC</string>
<string name="searching_pc">Ricerca PC in corso…</string> <string name="searching_pc">Ricerca PC in corso…</string>
<string name="yes">Sì</string>
<string name="no">No</string>
<string name="lost_connection">Connessione con il PC persa</string>
<!-- AppList activity --> <!-- AppList activity -->
<string name="title_applist">Applicazioni su</string> <string name="title_applist">Applicazioni su</string>
@@ -70,6 +73,7 @@
<string name="applist_quit_app">Chiusura in corso…</string> <string name="applist_quit_app">Chiusura in corso…</string>
<string name="applist_quit_success">Sessione chiusa con successo</string> <string name="applist_quit_success">Sessione chiusa con successo</string>
<string name="applist_quit_fail">Chiusura sessione fallita</string> <string name="applist_quit_fail">Chiusura sessione fallita</string>
<string name="applist_quit_confirmation">Sei sicuro di voler chiudere l\'applicazione avviata? Tutti i dati non salvati saranno persi.</string>
<!-- Add computer manually activity --> <!-- Add computer manually activity -->
<string name="title_add_pc">Aggiungi PC Manualmente</string> <string name="title_add_pc">Aggiungi PC Manualmente</string>
@@ -87,20 +91,30 @@
<string name="summary_seekbar_bitrate">Abbassa il bitrate per ridurre lo stuttering; alza il bitrate per aumenteare la qualità dell\'immagine</string> <string name="summary_seekbar_bitrate">Abbassa il bitrate per ridurre lo stuttering; alza il bitrate per aumenteare la qualità dell\'immagine</string>
<string name="suffix_seekbar_bitrate">Mbps</string> <string name="suffix_seekbar_bitrate">Mbps</string>
<string name="title_checkbox_stretch_video">Forza video in full-screen</string> <string name="title_checkbox_stretch_video">Forza video in full-screen</string>
<string name="title_checkbox_disable_warnings">Disabilita i messaggi di warning</string> <string name="title_checkbox_disable_warnings">Disabilita messaggi di warning</string>
<string name="summary_checkbox_disable_warnings">Disabilita i messaggi di warning sullo schermo durante lo streaming</string> <string name="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="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="title_seekbar_deadzone">Aggiusta deadzone degli stick analogici</string>
<string name="suffix_seekbar_deadzone">%</string> <string name="suffix_seekbar_deadzone">%</string>
<string name="category_ui_settings">Impostazioni Interfaccia</string>
<string name="title_language_list">Lingua</string>
<string name="summary_language_list">Lingua da usare in Limelight</string>
<string name="title_checkbox_list_mode">Usa lista invece della griglia</string>
<string name="summary_checkbox_list_mode">Visualizza applicazioni e computers in una lista invece di una griglia</string>
<string name="title_checkbox_small_icon_mode">Usa icone piccole</string>
<string name="summary_checkbox_small_icon_mode">Usa icone piccole nella vista a griglia per avere più oggetti sullo schermo</string>
<string name="category_host_settings">Impostazioni Host</string> <string name="category_host_settings">Impostazioni Host</string>
<string name="title_checkbox_enable_sops">Ottimizza le impostazioni dei giochi</string> <string name="title_checkbox_enable_sops">Ottimizza le impostazioni dei giochi</string>
<string name="summary_checkbox_enable_sops">Permetti a GFE di modificare le impostazioni dei giochi per uno streaming ottimale</string> <string name="summary_checkbox_enable_sops">Permetti a GFE di modificare le impostazioni dei giochi per uno streaming ottimale</string>
<string name="title_checkbox_host_audio">Riproduci audio sul PC</string> <string name="title_checkbox_host_audio">Riproduci audio sul PC</string>
<string name="summary_checkbox_host_audio">Riproduci l\'audio sul computer e su questo dispositivo. Richiede GFE 2.1.2+</string> <string name="summary_checkbox_host_audio">Riproduci l\'audio sul computer e su questo dispositivo</string>
<string name="category_advanced_settings">Impostazioni Avanzate</string> <string name="category_advanced_settings">Impostazioni Avanzate</string>
<string name="title_decoder_list">Cambia decoder</string> <string name="title_decoder_list">Cambia decoder</string>
<string name="summary_decoder_list">Il decoder software può aumentare la latenza video quando si usano impostazioni streaming basse</string> <string name="summary_decoder_list">Il decoder software può ridurre la latenza video quando si usano impostazioni streaming basse</string>
</resources> </resources>
+11
View File
@@ -13,6 +13,17 @@
<item>1080p60</item> <item>1080p60</item>
</string-array> </string-array>
<string-array name="language_names" translatable="false">
<item>Default</item>
<item>English</item>
<item>Italiano</item>
</string-array>
<string-array name="language_values" translatable="false">
<item>default</item>
<item>en</item>
<item>it</item>
</string-array>
<string-array name="decoder_names"> <string-array name="decoder_names">
<item>Auto-select Decoder</item> <item>Auto-select Decoder</item>
<item>Force Software Decoding</item> <item>Force Software Decoding</item>
+15 -1
View File
@@ -56,6 +56,9 @@
<!-- General strings --> <!-- General strings -->
<string name="ip_hint">IP address of GeForce PC</string> <string name="ip_hint">IP address of GeForce PC</string>
<string name="searching_pc">Searching for PCs…</string> <string name="searching_pc">Searching for PCs…</string>
<string name="yes">Yes</string>
<string name="no">No</string>
<string name="lost_connection">Lost connection to PC</string>
<!-- AppList activity --> <!-- AppList activity -->
<string name="title_applist">Apps on</string> <string name="title_applist">Apps on</string>
@@ -70,6 +73,7 @@
<string name="applist_quit_app">Quitting</string> <string name="applist_quit_app">Quitting</string>
<string name="applist_quit_success">Successfully quit</string> <string name="applist_quit_success">Successfully quit</string>
<string name="applist_quit_fail">Failed to quit</string> <string name="applist_quit_fail">Failed to quit</string>
<string name="applist_quit_confirmation">Are you sure you want to quit the running app? All unsaved data will be lost.</string>
<!-- Add computer manually activity --> <!-- Add computer manually activity -->
<string name="title_add_pc">Add PC Manually</string> <string name="title_add_pc">Add PC Manually</string>
@@ -91,14 +95,24 @@
<string name="summary_checkbox_disable_warnings">Disable on-screen connection warning messages while streaming</string> <string name="summary_checkbox_disable_warnings">Disable on-screen connection warning messages while streaming</string>
<string name="category_gamepad_settings">Gamepad Settings</string> <string name="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="title_seekbar_deadzone">Adjust analog stick deadzone</string>
<string name="suffix_seekbar_deadzone">%</string> <string name="suffix_seekbar_deadzone">%</string>
<string name="category_ui_settings">UI Settings</string>
<string name="title_language_list">Language</string>
<string name="summary_language_list">Language to use for Limelight</string>
<string name="title_checkbox_list_mode">Use lists instead of grids</string>
<string name="summary_checkbox_list_mode">Display apps and PCs in lists instead of grids</string>
<string name="title_checkbox_small_icon_mode">Use small icons</string>
<string name="summary_checkbox_small_icon_mode">Use small icons in grid items to allow more items on screen</string>
<string name="category_host_settings">Host Settings</string> <string name="category_host_settings">Host Settings</string>
<string name="title_checkbox_enable_sops">Optimize game settings</string> <string name="title_checkbox_enable_sops">Optimize game settings</string>
<string name="summary_checkbox_enable_sops">Allow GFE to modify game settings for optimal streaming</string> <string name="summary_checkbox_enable_sops">Allow GFE to modify game settings for optimal streaming</string>
<string name="title_checkbox_host_audio">Play audio on PC</string> <string name="title_checkbox_host_audio">Play audio on PC</string>
<string name="summary_checkbox_host_audio">Play audio from the computer and this device. Requires GFE 2.1.2+</string> <string name="summary_checkbox_host_audio">Play audio from the computer and this device</string>
<string name="category_advanced_settings">Advanced Settings</string> <string name="category_advanced_settings">Advanced Settings</string>
<string name="title_decoder_list">Change decoder</string> <string name="title_decoder_list">Change decoder</string>
+27 -4
View File
@@ -26,14 +26,19 @@
android:summary="@string/summary_checkbox_disable_warnings" android:summary="@string/summary_checkbox_disable_warnings"
android:defaultValue="false" /> android:defaultValue="false" />
</PreferenceCategory> </PreferenceCategory>
<!--PreferenceCategory android:title="@string/category_gamepad_settings"> <PreferenceCategory android:title="@string/category_gamepad_settings">
<com.limelight.preferences.SeekBarPreference <!--com.limelight.preferences.SeekBarPreference
android:key="seekbar_deadzone" android:key="seekbar_deadzone"
android:defaultValue="15" android:defaultValue="15"
android:max="50" android:max="50"
android:text="@string/suffix_seekbar_deadzone" android:text="@string/suffix_seekbar_deadzone"
android:title="@string/title_seekbar_deadzone"/> android:title="@string/title_seekbar_deadzone"/-->
</PreferenceCategory--> <CheckBoxPreference
android:key="checkbox_multi_controller"
android:title="@string/title_checkbox_multi_controller"
android:summary="@string/summary_checkbox_multi_controller"
android:defaultValue="true" />
</PreferenceCategory>
<PreferenceCategory android:title="@string/category_host_settings"> <PreferenceCategory android:title="@string/category_host_settings">
<CheckBoxPreference <CheckBoxPreference
android:key="checkbox_enable_sops" android:key="checkbox_enable_sops"
@@ -46,6 +51,24 @@
android:summary="@string/summary_checkbox_host_audio" android:summary="@string/summary_checkbox_host_audio"
android:defaultValue="false" /> android:defaultValue="false" />
</PreferenceCategory> </PreferenceCategory>
<PreferenceCategory android:title="@string/category_ui_settings">
<ListPreference
android:key="list_languages"
android:title="@string/title_language_list"
android:entries="@array/language_names"
android:entryValues="@array/language_values"
android:summary="@string/summary_language_list"
android:defaultValue="default" />
<com.limelight.preferences.SmallIconCheckboxPreference
android:key="checkbox_small_icon_mode"
android:title="@string/title_checkbox_small_icon_mode"
android:summary="@string/summary_checkbox_small_icon_mode" />
<CheckBoxPreference
android:key="checkbox_list_mode"
android:title="@string/title_checkbox_list_mode"
android:summary="@string/summary_checkbox_list_mode"
android:defaultValue="false" />
</PreferenceCategory>
<PreferenceCategory android:title="@string/category_advanced_settings"> <PreferenceCategory android:title="@string/category_advanced_settings">
<ListPreference <ListPreference
android:key="list_decoders" android:key="list_decoders"
+1 -2
View File
@@ -1,6 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android">
package="com.limelight" >
<!-- Non-root application name --> <!-- Non-root application name -->
<application android:label="Limelight" /> <application android:label="Limelight" />
+1 -2
View File
@@ -1,6 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android">
package="com.limelight.root" >
<!-- Root permissions --> <!-- Root permissions -->
<uses-permission android:name="android.permission.ACCESS_SUPERUSER" /> <uses-permission android:name="android.permission.ACCESS_SUPERUSER" />
+1 -1
View File
@@ -4,7 +4,7 @@ buildscript {
jcenter() jcenter()
} }
dependencies { dependencies {
classpath 'com.android.tools.build:gradle:0.13.2' classpath 'com.android.tools.build:gradle:1.0.1'
} }
} }
+2 -2
View File
@@ -4,12 +4,12 @@ This file serves to document some of the decoder errata when using MediaCodec ha
- Affected decoders: TI OMAP4, Allwinner A20 - Affected decoders: TI OMAP4, Allwinner A20
2. Some decoders have a huge per-frame latency with the unmodified SPS sent from NVENC. Setting max_dec_frame_buffering fixes this latency issue. 2. Some decoders have a huge per-frame latency with the unmodified SPS sent from NVENC. Setting max_dec_frame_buffering fixes this latency issue.
- Affected decoders: NVIDIA Tegra 3 and 4 - Affected decoders: NVIDIA Tegra 3 and 4, Broadcom VideoCore IV
3. Some decoders strictly require that you pass BUFFER_FLAG_CODEC_CONFIG and crash upon the IDR frame if you don't 3. Some decoders strictly require that you pass BUFFER_FLAG_CODEC_CONFIG and crash upon the IDR frame if you don't
- Affected decoders: TI OMAP4 - Affected decoders: TI OMAP4
4. Some decoders require num_ref_frames=1 and max_dec_frame_buffering=1 to avoid crashing on SPS or first I-frame 4. Some decoders require num_ref_frames=1 and max_dec_frame_buffering=1 to avoid crashing on SPS on first I-frame
- Affected decoders: Qualcomm in GS3 on 4.3+, Exynos 4 at 1080p only - Affected decoders: Qualcomm in GS3 on 4.3+, Exynos 4 at 1080p only
5. Some decoders will hang if max_dec_frame_buffering is not present 5. Some decoders will hang if max_dec_frame_buffering is not present
+2 -2
View File
@@ -1,6 +1,6 @@
#Wed Apr 10 15:27:10 PDT 2013 #Sun Dec 07 22:52:07 PST 2014
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-2.1-all.zip distributionUrl=https\://services.gradle.org/distributions/gradle-2.2.1-all.zip