Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bd6ff35603 | ||
|
|
1cb7727dc7 | ||
|
|
0c73e3d0ae | ||
|
|
6371d364e1 | ||
|
|
ded9c9140d | ||
|
|
7c8a108e28 | ||
|
|
2a18ffcdba | ||
|
|
381d0d5e81 | ||
|
|
be126acfd1 | ||
|
|
fc2f5cfe4d | ||
|
|
9878902a89 | ||
|
|
f1230d46f3 | ||
|
|
d8822392f1 | ||
|
|
1d9cf71517 | ||
|
|
2160e87fef | ||
|
|
88249ba8aa | ||
|
|
2856617fb3 | ||
|
|
d822980d5a | ||
|
|
b5ba59b413 | ||
|
|
1148e0163c | ||
|
|
cf36c7adb1 | ||
|
|
eac6998e17 | ||
|
|
17afbffdb5 | ||
|
|
072a439c2d | ||
|
|
c533600983 | ||
|
|
5847fbb6b6 | ||
|
|
1876b30c1b | ||
|
|
5c71f55993 | ||
|
|
9c0960d03d | ||
|
|
29a395f3f4 | ||
|
|
a676b8d8e6 | ||
|
|
7ab0be3b62 | ||
|
|
115853fed2 | ||
|
|
60beb81ae4 | ||
|
|
5310375d42 | ||
|
|
7ce29e3a09 | ||
|
|
42c65f4f16 | ||
|
|
bf2cc2a4d5 | ||
|
|
6d6d7121f6 | ||
|
|
2ab67380d6 | ||
|
|
899387caa1 | ||
|
|
56c8a9e6fe | ||
|
|
896288a40b | ||
|
|
fc8ce5e4b9 | ||
|
|
4affc3c4ce | ||
|
|
067be54715 | ||
|
|
0dad2dc64b | ||
|
|
867b703644 | ||
|
|
3d398ef6dd | ||
|
|
85d95b2d8e | ||
|
|
d091d9db6b | ||
|
|
e081ab5239 | ||
|
|
7b12fd1ad2 | ||
|
|
80d8c5953e | ||
|
|
194037ff41 | ||
|
|
094d642739 | ||
|
|
010e03252e | ||
|
|
98638186b5 | ||
|
|
c5293ef21f | ||
|
|
366a1c91b8 | ||
|
|
157450e674 | ||
|
|
1b8d2bc81c | ||
|
|
f1787c43e5 | ||
|
|
95ea88e932 | ||
|
|
f2b8461bb9 | ||
|
|
7838a787df | ||
|
|
cc3f2ecb07 | ||
|
|
833b7c3916 | ||
|
|
90209f2ca2 | ||
|
|
2681036c32 | ||
|
|
ee58071ff1 | ||
|
|
e222f2f6c3 | ||
|
|
0b7becb161 | ||
|
|
bf795ab7a5 | ||
|
|
59df38ae8a | ||
|
|
e04ff048b8 | ||
|
|
7d25d07c6d | ||
|
|
7b0ddfae42 | ||
|
|
43fa1a7245 | ||
|
|
057530eed0 | ||
|
|
aee34f6365 | ||
|
|
5519d92243 | ||
|
|
3d95ac1f93 | ||
|
|
5c938535be | ||
|
|
2fdecc551a | ||
|
|
10204afdb4 | ||
|
|
55c800c2a5 | ||
|
|
265b3f9963 | ||
|
|
a8bf2cd1cf | ||
|
|
4fcd8b3dfe | ||
|
|
e1a1a6344d | ||
|
|
a095c10a25 | ||
|
|
b1ea487e22 | ||
|
|
47265d0d10 | ||
|
|
6a41b41a38 | ||
|
|
2247e43a48 | ||
|
|
d3986080a3 | ||
|
|
07277e1a5b | ||
|
|
39d7fc748f | ||
|
|
4d3a69cf6a | ||
|
|
b806522751 | ||
|
|
256fa897a7 | ||
|
|
5c812eed6c | ||
|
|
f0b22f9119 | ||
|
|
7e1884acb5 | ||
|
|
9512521783 | ||
|
|
da7904a767 | ||
|
|
3a0c1db168 | ||
|
|
bd21692323 | ||
|
|
5ae245bdca | ||
|
|
d3052cd97d | ||
|
|
336f85a31c | ||
|
|
b01f7c796e | ||
|
|
56f438fe47 | ||
|
|
baa5199b83 | ||
|
|
23ca62b304 | ||
|
|
2c3511195c | ||
|
|
d31ef481f3 | ||
|
|
a490da5e5c | ||
|
|
72d3576257 | ||
|
|
ebd93a55a0 | ||
|
|
4d01e1afe6 | ||
|
|
9ff1386751 | ||
|
|
5fca35f0b1 | ||
|
|
d23c763441 | ||
|
|
fa058c4783 | ||
|
|
e0ddd5f045 | ||
|
|
b7443451a4 | ||
|
|
e90e4a22c4 | ||
|
|
3a53172145 | ||
|
|
1dfcb7bc29 | ||
|
|
897bb76858 | ||
|
|
bcc67269ab | ||
|
|
4d24c654b9 | ||
|
|
cba44b091b | ||
|
|
f2d8f8a41b | ||
|
|
4b1c7e7e3c | ||
|
|
1cba278876 | ||
|
|
766898fdf9 | ||
|
|
13e91d594b | ||
|
|
ca0a0da19f | ||
|
|
82cabce86e | ||
|
|
51a630995a | ||
|
|
3a74f0726c | ||
|
|
efa6c7bba0 | ||
|
|
b8141542f8 | ||
|
|
8fc9a90207 | ||
|
|
13d707d98d | ||
|
|
aae0ff6e7a | ||
|
|
69c7b5a0d5 | ||
|
|
d1ad3115fa | ||
|
|
770af402a4 | ||
|
|
3236c0b93a | ||
|
|
51aacc3f38 | ||
|
|
397c6f46f9 | ||
|
|
d00f78f859 | ||
|
|
29fec2e0de | ||
|
|
88d28665ef | ||
|
|
de1f4da258 | ||
|
|
7985be57ab | ||
|
|
a835e7aaa2 | ||
|
|
22958cfbb1 | ||
|
|
c4dc5eb9e1 | ||
|
|
db758f386e | ||
|
|
3fb3eefa94 | ||
|
|
9340dff45d | ||
|
|
2d6c756e70 | ||
|
|
03e965d449 | ||
|
|
4cea483a87 | ||
|
|
99aa616188 |
No files matched your search
@@ -1,12 +1,14 @@
|
|||||||
#Limelight
|
#Moonlight
|
||||||
|
|
||||||
Limelight is an open source implementation of NVIDIA's GameStream, as used by the NVIDIA Shield.
|
[Moonlight](http://moonlight-stream.com) is an open source implementation of NVIDIA's GameStream, as used by the NVIDIA Shield.
|
||||||
We reverse engineered the Shield streaming software and created a version that can be run on any Android device.
|
We reverse engineered the Shield streaming software and created a version that can be run on any Android device.
|
||||||
|
|
||||||
Limelight will allow you to stream your full collection of games from your Windows PC to your Android device,
|
Moonlight will allow you to stream your full collection of games from your Windows PC to your Android device,
|
||||||
whether in your own home or over the internet.
|
whether in your own home or over the internet.
|
||||||
|
|
||||||
[Limelight-pc](https://github.com/limelight-stream/limelight-pc) is also currently in development for Windows, OS X and Linux. Versions for [iOS](https://github.com/limelight-stream/limelight-ios) and [Windows and Windows Phone](https://github.com/limelight-stream/limelight-windows) are also in development.
|
[Moonlight-pc](https://github.com/moonlight-stream/moonlight-pc) is also currently in development for Windows, OS X and Linux. Versions for [iOS](https://github.com/moonlight-stream/moonlight-ios) and [Windows and Windows Phone](https://github.com/moonlight-stream/moonlight-windows) are also in development.
|
||||||
|
|
||||||
|
Check our [wiki](https://github.com/moonlight-stream/moonlight-android/wiki) for more detailed information or a troubleshooting guide.
|
||||||
|
|
||||||
##Features
|
##Features
|
||||||
|
|
||||||
@@ -16,7 +18,7 @@ whether in your own home or over the internet.
|
|||||||
|
|
||||||
##Installation
|
##Installation
|
||||||
|
|
||||||
* Download and install Limelight for Android from
|
* Download and install Moonlight for Android from
|
||||||
[Google Play](https://play.google.com/store/apps/details?id=com.limelight)
|
[Google Play](https://play.google.com/store/apps/details?id=com.limelight)
|
||||||
* Download [GeForce Experience](http://www.geforce.com/geforce-experience) and install on your Windows PC
|
* Download [GeForce Experience](http://www.geforce.com/geforce-experience) and install on your Windows PC
|
||||||
|
|
||||||
@@ -31,7 +33,7 @@ whether in your own home or over the internet.
|
|||||||
* Turn on GameStream in the GFE settings
|
* Turn on GameStream in the GFE settings
|
||||||
* If you are connecting from outside the same network, turn on internet
|
* If you are connecting from outside the same network, turn on internet
|
||||||
streaming
|
streaming
|
||||||
* When on the same network as your PC, open Limelight and tap on your PC in the list
|
* When on the same network as your PC, open Moonlight and tap on your PC in the list
|
||||||
* Accept the pairing confirmation on your PC
|
* Accept the pairing confirmation on your PC
|
||||||
* Tap your PC again to view the list of apps to stream
|
* Tap your PC again to view the list of apps to stream
|
||||||
* Play games!
|
* Play games!
|
||||||
@@ -44,8 +46,6 @@ This project is being actively developed at [XDA Developers](http://forum.xda-de
|
|||||||
2. Write code
|
2. Write code
|
||||||
3. Send Pull Requests
|
3. Send Pull Requests
|
||||||
|
|
||||||
Check out our [website](http://limelight-stream.com) for project links and information.
|
|
||||||
|
|
||||||
##Authors
|
##Authors
|
||||||
|
|
||||||
* [Cameron Gutman](https://github.com/cgutman)
|
* [Cameron Gutman](https://github.com/cgutman)
|
||||||
@@ -53,5 +53,5 @@ Check out our [website](http://limelight-stream.com) for project links and infor
|
|||||||
* [Aaron Neyer](https://github.com/Aaronneyer)
|
* [Aaron Neyer](https://github.com/Aaronneyer)
|
||||||
* [Andrew Hennessy](https://github.com/yetanothername)
|
* [Andrew Hennessy](https://github.com/yetanothername)
|
||||||
|
|
||||||
Limelight is the work of students at [Case Western](http://case.edu) and was
|
Moonlight is the work of students at [Case Western](http://case.edu) and was
|
||||||
started as a project at [MHacks](http://mhacks.org).
|
started as a project at [MHacks](http://mhacks.org).
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<module external.linked.project.path="$MODULE_DIR$" external.root.project.path="$MODULE_DIR$/.." external.system.id="GRADLE" external.system.module.group="limelight-android" external.system.module.version="unspecified" type="JAVA_MODULE" version="4">
|
<module external.linked.project.id=":app" external.linked.project.path="$MODULE_DIR$" external.root.project.path="$MODULE_DIR$/.." external.system.id="GRADLE" external.system.module.group="moonlight-android" external.system.module.version="unspecified" type="JAVA_MODULE" version="4">
|
||||||
<component name="FacetManager">
|
<component name="FacetManager">
|
||||||
<facet type="android-gradle" name="Android-Gradle">
|
<facet type="android-gradle" name="Android-Gradle">
|
||||||
<configuration>
|
<configuration>
|
||||||
@@ -9,11 +9,13 @@
|
|||||||
<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="SOURCE_GEN_TASK_NAME" value="generateNonRootDebugSources" />
|
<option name="SOURCE_GEN_TASK_NAME" value="generateNonRootDebugSources" />
|
||||||
<option name="TEST_SOURCE_GEN_TASK_NAME" value="generateNonRootDebugTestSources" />
|
<option name="ASSEMBLE_TEST_TASK_NAME" value="assembleNonRootDebugAndroidTest" />
|
||||||
|
<option name="COMPILE_JAVA_TEST_TASK_NAME" value="compileNonRootDebugAndroidTestSources" />
|
||||||
|
<option name="TEST_SOURCE_GEN_TASK_NAME" value="generateNonRootDebugAndroidTestSources" />
|
||||||
<option name="ALLOW_USER_CONFIGURATION" value="false" />
|
<option name="ALLOW_USER_CONFIGURATION" value="false" />
|
||||||
<option name="MANIFEST_FILE_RELATIVE_PATH" value="/src/main/AndroidManifest.xml" />
|
<option name="MANIFEST_FILE_RELATIVE_PATH" value="/src/main/AndroidManifest.xml" />
|
||||||
<option name="RES_FOLDER_RELATIVE_PATH" value="/src/main/res" />
|
<option name="RES_FOLDER_RELATIVE_PATH" value="/src/main/res" />
|
||||||
@@ -24,6 +26,7 @@
|
|||||||
</component>
|
</component>
|
||||||
<component name="NewModuleRootManager" inherit-compiler-output="false">
|
<component name="NewModuleRootManager" inherit-compiler-output="false">
|
||||||
<output url="file://$MODULE_DIR$/build/intermediates/classes/nonRoot/debug" />
|
<output url="file://$MODULE_DIR$/build/intermediates/classes/nonRoot/debug" />
|
||||||
|
<output-test url="file://$MODULE_DIR$/build/intermediates/classes/androidTest/nonRoot/debug" />
|
||||||
<exclude-output />
|
<exclude-output />
|
||||||
<content url="file://$MODULE_DIR$">
|
<content url="file://$MODULE_DIR$">
|
||||||
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/r/nonRoot/debug" isTestSource="false" generated="true" />
|
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/r/nonRoot/debug" isTestSource="false" generated="true" />
|
||||||
@@ -31,6 +34,7 @@
|
|||||||
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/buildConfig/nonRoot/debug" isTestSource="false" generated="true" />
|
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/buildConfig/nonRoot/debug" isTestSource="false" generated="true" />
|
||||||
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/rs/nonRoot/debug" isTestSource="false" generated="true" />
|
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/rs/nonRoot/debug" isTestSource="false" generated="true" />
|
||||||
<sourceFolder url="file://$MODULE_DIR$/build/generated/res/rs/nonRoot/debug" type="java-resource" />
|
<sourceFolder url="file://$MODULE_DIR$/build/generated/res/rs/nonRoot/debug" type="java-resource" />
|
||||||
|
<sourceFolder url="file://$MODULE_DIR$/build/generated/res/generated/nonRoot/debug" type="java-resource" />
|
||||||
<sourceFolder url="file://$MODULE_DIR$/src/nonRootDebug/res" type="java-resource" />
|
<sourceFolder url="file://$MODULE_DIR$/src/nonRootDebug/res" type="java-resource" />
|
||||||
<sourceFolder url="file://$MODULE_DIR$/src/nonRootDebug/resources" type="java-resource" />
|
<sourceFolder url="file://$MODULE_DIR$/src/nonRootDebug/resources" type="java-resource" />
|
||||||
<sourceFolder url="file://$MODULE_DIR$/src/nonRootDebug/assets" type="java-resource" />
|
<sourceFolder url="file://$MODULE_DIR$/src/nonRootDebug/assets" type="java-resource" />
|
||||||
@@ -38,11 +42,12 @@
|
|||||||
<sourceFolder url="file://$MODULE_DIR$/src/nonRootDebug/java" isTestSource="false" />
|
<sourceFolder url="file://$MODULE_DIR$/src/nonRootDebug/java" isTestSource="false" />
|
||||||
<sourceFolder url="file://$MODULE_DIR$/src/nonRootDebug/jni" isTestSource="false" />
|
<sourceFolder url="file://$MODULE_DIR$/src/nonRootDebug/jni" isTestSource="false" />
|
||||||
<sourceFolder url="file://$MODULE_DIR$/src/nonRootDebug/rs" isTestSource="false" />
|
<sourceFolder url="file://$MODULE_DIR$/src/nonRootDebug/rs" isTestSource="false" />
|
||||||
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/r/test/nonRoot/debug" isTestSource="true" generated="true" />
|
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/r/androidTest/nonRoot/debug" isTestSource="true" generated="true" />
|
||||||
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/aidl/test/nonRoot/debug" isTestSource="true" generated="true" />
|
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/aidl/androidTest/nonRoot/debug" isTestSource="true" generated="true" />
|
||||||
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/buildConfig/test/nonRoot/debug" isTestSource="true" generated="true" />
|
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/buildConfig/androidTest/nonRoot/debug" isTestSource="true" generated="true" />
|
||||||
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/rs/test/nonRoot/debug" isTestSource="true" generated="true" />
|
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/rs/androidTest/nonRoot/debug" isTestSource="true" generated="true" />
|
||||||
<sourceFolder url="file://$MODULE_DIR$/build/generated/res/rs/test/nonRoot/debug" type="java-test-resource" />
|
<sourceFolder url="file://$MODULE_DIR$/build/generated/res/rs/androidTest/nonRoot/debug" type="java-test-resource" />
|
||||||
|
<sourceFolder url="file://$MODULE_DIR$/build/generated/res/generated/androidTest/nonRoot/debug" type="java-test-resource" />
|
||||||
<sourceFolder url="file://$MODULE_DIR$/src/nonRoot/res" type="java-resource" />
|
<sourceFolder url="file://$MODULE_DIR$/src/nonRoot/res" type="java-resource" />
|
||||||
<sourceFolder url="file://$MODULE_DIR$/src/nonRoot/resources" type="java-resource" />
|
<sourceFolder url="file://$MODULE_DIR$/src/nonRoot/resources" type="java-resource" />
|
||||||
<sourceFolder url="file://$MODULE_DIR$/src/nonRoot/assets" type="java-resource" />
|
<sourceFolder url="file://$MODULE_DIR$/src/nonRoot/assets" type="java-resource" />
|
||||||
@@ -99,20 +104,15 @@
|
|||||||
<excludeFolder url="file://$MODULE_DIR$/build/outputs" />
|
<excludeFolder url="file://$MODULE_DIR$/build/outputs" />
|
||||||
<excludeFolder url="file://$MODULE_DIR$/build/tmp" />
|
<excludeFolder url="file://$MODULE_DIR$/build/tmp" />
|
||||||
</content>
|
</content>
|
||||||
<orderEntry type="jdk" jdkName="Android API 21 Platform" jdkType="Android SDK" />
|
<orderEntry type="jdk" jdkName="Android API 22 Platform" jdkType="Android SDK" />
|
||||||
<orderEntry type="sourceFolder" forTests="false" />
|
<orderEntry type="sourceFolder" forTests="false" />
|
||||||
<orderEntry type="library" exported="" name="bcprov-jdk15on-1.51" level="project" />
|
<orderEntry type="library" exported="" name="bcprov-jdk15on-1.51" level="project" />
|
||||||
<orderEntry type="library" exported="" name="gson-2.3.1" level="project" />
|
|
||||||
<orderEntry type="library" exported="" name="support-v4-r7" level="project" />
|
|
||||||
<orderEntry type="library" exported="" name="jmdns-fixed" level="project" />
|
<orderEntry type="library" exported="" name="jmdns-fixed" 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="okhttp-2.1.0" 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="okio-1.0.1" level="project" />
|
<orderEntry type="library" exported="" name="okhttp-2.2.0" level="project" />
|
||||||
<orderEntry type="library" exported="" name="androidasync-e1dfb4" level="project" />
|
|
||||||
<orderEntry type="library" exported="" name="jcodec-0.1.9" level="project" />
|
<orderEntry type="library" exported="" name="jcodec-0.1.9" level="project" />
|
||||||
<orderEntry type="library" exported="" name="ion-2f46fa" level="project" />
|
<orderEntry type="library" exported="" name="okio-1.2.0" level="project" />
|
||||||
</component>
|
</component>
|
||||||
</module>
|
</module>
|
||||||
|
|
||||||
@@ -4,15 +4,15 @@ import org.apache.tools.ant.taskdefs.condition.Os
|
|||||||
apply plugin: 'com.android.application'
|
apply plugin: 'com.android.application'
|
||||||
|
|
||||||
android {
|
android {
|
||||||
compileSdkVersion 21
|
compileSdkVersion 22
|
||||||
buildToolsVersion "21.1.1"
|
buildToolsVersion "21.1.2"
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
minSdkVersion 16
|
minSdkVersion 16
|
||||||
targetSdkVersion 21
|
targetSdkVersion 22
|
||||||
|
|
||||||
versionName "3.0.1"
|
versionName "3.1.7"
|
||||||
versionCode = 47
|
versionCode = 62
|
||||||
}
|
}
|
||||||
|
|
||||||
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,19 +62,14 @@ android {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
compile group: 'org.jcodec', name: 'jcodec', version: '+'
|
compile group: 'org.jcodec', name: 'jcodec', version: '0.1.9'
|
||||||
compile group: 'org.bouncycastle', name: 'bcprov-jdk15on', version: '+'
|
|
||||||
compile group: 'org.bouncycastle', name: 'bcpkix-jdk15on', version: '+'
|
|
||||||
compile group: 'com.google.android', name: 'support-v4', version:'+'
|
|
||||||
|
|
||||||
// FIXME: Pending resolution of issue #346 using custom build
|
compile group: 'org.bouncycastle', name: 'bcprov-jdk15on', version: '1.51'
|
||||||
//compile group: 'com.koushikdutta.ion', name: 'ion', version:'+'
|
compile group: 'org.bouncycastle', name: 'bcpkix-jdk15on', version: '1.51'
|
||||||
compile group: 'com.google.code.gson', name: 'gson', version:'+'
|
|
||||||
compile files('libs/androidasync-e1dfb4.jar')
|
compile group: 'com.squareup.okhttp', name: 'okhttp', version:'2.2.0'
|
||||||
compile files('libs/ion-2f46fa.jar')
|
compile group: 'com.squareup.okio', name:'okio', version:'1.2.0'
|
||||||
|
|
||||||
compile group: 'com.squareup.okhttp', name: 'okhttp', version:'+'
|
|
||||||
compile group: 'com.squareup.okio', name:'okio', version:'+'
|
|
||||||
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')
|
||||||
|
|||||||
@@ -17,6 +17,10 @@
|
|||||||
android:icon="@drawable/ic_launcher"
|
android:icon="@drawable/ic_launcher"
|
||||||
android:theme="@style/AppTheme" >
|
android:theme="@style/AppTheme" >
|
||||||
|
|
||||||
|
<!-- Samsung multi-window support -->
|
||||||
|
<uses-library android:name="com.sec.android.app.multiwindow" android:required="false" />
|
||||||
|
<meta-data android:name="com.sec.android.support.multiwindow" android:value="true" />
|
||||||
|
|
||||||
<!-- Launcher for traditional devices -->
|
<!-- Launcher for traditional devices -->
|
||||||
<activity
|
<activity
|
||||||
android:name=".PcView"
|
android:name=".PcView"
|
||||||
@@ -24,6 +28,7 @@
|
|||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN" />
|
<action android:name="android.intent.action.MAIN" />
|
||||||
<category android:name="android.intent.category.LAUNCHER" />
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
|
<category android:name="android.intent.category.MULTIWINDOW_LAUNCHER" />
|
||||||
<category android:name="tv.ouya.intent.category.APP" />
|
<category android:name="tv.ouya.intent.category.APP" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
@@ -68,7 +73,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"
|
||||||
|
|||||||
@@ -1,109 +1,239 @@
|
|||||||
package com.limelight;
|
package com.limelight;
|
||||||
|
|
||||||
import java.io.FileNotFoundException;
|
import java.io.StringReader;
|
||||||
import java.io.IOException;
|
|
||||||
import java.net.InetAddress;
|
|
||||||
import java.net.UnknownHostException;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
import org.xmlpull.v1.XmlPullParserException;
|
import com.limelight.computers.ComputerManagerListener;
|
||||||
|
import com.limelight.computers.ComputerManagerService;
|
||||||
import com.limelight.binding.PlatformBinding;
|
|
||||||
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.ServerHelper;
|
||||||
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.Service;
|
||||||
|
import android.content.ComponentName;
|
||||||
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 lastRunningAppId;
|
||||||
|
private boolean suspendGridUpdates;
|
||||||
|
|
||||||
|
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 void startComputerUpdates() {
|
||||||
|
if (managerBinder == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
managerBinder.startPolling(new ComputerManagerListener() {
|
||||||
|
@Override
|
||||||
|
public void notifyComputerUpdated(ComputerDetails details) {
|
||||||
|
// Do nothing if updates are suspended
|
||||||
|
if (suspendGridUpdates) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Don't care about other computers
|
||||||
|
if (!details.uuid.toString().equalsIgnoreCase(uuidString)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (details.state == ComputerDetails.State.OFFLINE) {
|
||||||
|
// The PC is unreachable now
|
||||||
|
AppView.this.runOnUiThread(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
// Display a toast to the user and quit the activity
|
||||||
|
Toast.makeText(AppView.this, getResources().getText(R.string.lost_connection), Toast.LENGTH_SHORT).show();
|
||||||
|
finish();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// App list is the same or empty
|
||||||
|
if (details.rawAppList == null || details.rawAppList.equals(lastRawApplist)) {
|
||||||
|
|
||||||
|
// Let's check if the running app ID changed
|
||||||
|
if (details.runningGameId != lastRunningAppId) {
|
||||||
|
// Update the currently running game using the app ID
|
||||||
|
lastRunningAppId = details.runningGameId;
|
||||||
|
updateUiWithServerinfo(details);
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
lastRunningAppId = details.runningGameId;
|
||||||
|
lastRawApplist = details.rawAppList;
|
||||||
|
|
||||||
|
try {
|
||||||
|
updateUiWithAppList(NvHTTP.getAppListByReader(new StringReader(details.rawAppList)));
|
||||||
|
|
||||||
|
if (blockingLoadSpinner != null) {
|
||||||
|
blockingLoadSpinner.dismiss();
|
||||||
|
blockingLoadSpinner = null;
|
||||||
|
}
|
||||||
|
} catch (Exception ignored) {}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (poller == null) {
|
||||||
|
poller = managerBinder.createAppListPoller(computer);
|
||||||
|
}
|
||||||
|
poller.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void stopComputerUpdates() {
|
||||||
|
if (poller != null) {
|
||||||
|
poller.stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (managerBinder != null) {
|
||||||
|
managerBinder.stopPolling();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (appGridAdapter != null) {
|
||||||
|
appGridAdapter.cancelQueuedOperations();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@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,27 +242,30 @@ 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() {
|
||||||
int runningAppId = -1;
|
int runningAppId = -1;
|
||||||
for (int i = 0; i < appGridAdapter.getCount(); i++) {
|
for (int i = 0; i < appGridAdapter.getCount(); i++) {
|
||||||
AppObject app = (AppObject) appGridAdapter.getItem(i);
|
AppObject app = (AppObject) appGridAdapter.getItem(i);
|
||||||
if (app.app == null) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (app.app.getIsRunning()) {
|
if (app.app.getIsRunning()) {
|
||||||
runningAppId = app.app.getAppId();
|
runningAppId = app.app.getAppId();
|
||||||
break;
|
break;
|
||||||
@@ -147,18 +280,14 @@ public class AppView extends Activity {
|
|||||||
|
|
||||||
AdapterContextMenuInfo info = (AdapterContextMenuInfo) menuInfo;
|
AdapterContextMenuInfo info = (AdapterContextMenuInfo) menuInfo;
|
||||||
AppObject selectedApp = (AppObject) appGridAdapter.getItem(info.position);
|
AppObject selectedApp = (AppObject) appGridAdapter.getItem(info.position);
|
||||||
if (selectedApp == null || selectedApp.app == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
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));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -171,15 +300,43 @@ public class AppView extends Activity {
|
|||||||
@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
|
||||||
|
UiHelper.displayQuitConfirmationDialog(this, new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
ServerHelper.doStart(AppView.this, app.app, computer, managerBinder);
|
||||||
|
}
|
||||||
|
}, null);
|
||||||
|
return true;
|
||||||
|
|
||||||
|
case START_OR_RESUME_ID:
|
||||||
// Resume is the same as start for us
|
// Resume is the same as start for us
|
||||||
doStart(app.app);
|
ServerHelper.doStart(AppView.this, app.app, computer, managerBinder);
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
case QUIT_ID:
|
case QUIT_ID:
|
||||||
doQuit(app.app);
|
// Display a confirmation dialog first
|
||||||
|
UiHelper.displayQuitConfirmationDialog(this, new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
suspendGridUpdates = true;
|
||||||
|
ServerHelper.doQuit(AppView.this,
|
||||||
|
ServerHelper.getCurrentAddressFromComputer(computer),
|
||||||
|
app.app, managerBinder, new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
// Trigger a poll immediately
|
||||||
|
suspendGridUpdates = false;
|
||||||
|
if (poller != null) {
|
||||||
|
poller.pollNow();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, null);
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
case CANCEL_ID:
|
case CANCEL_ID:
|
||||||
@@ -190,103 +347,150 @@ public class AppView extends Activity {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updateAppList(final boolean displayError) {
|
private void updateUiWithServerinfo(final ComputerDetails details) {
|
||||||
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) {
|
|
||||||
appGridAdapter.addApp(new AppObject(app));
|
// Look through our current app list to tag the running app
|
||||||
|
for (int i = 0; i < appGridAdapter.getCount(); i++) {
|
||||||
|
AppObject existingApp = (AppObject) appGridAdapter.getItem(i);
|
||||||
|
|
||||||
|
// There can only be one or zero apps running.
|
||||||
|
if (existingApp.app.getIsRunning() &&
|
||||||
|
existingApp.app.getAppId() == details.runningGameId) {
|
||||||
|
// This app was running and still is, so we're done now
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
else if (existingApp.app.getAppId() == details.runningGameId) {
|
||||||
|
// This app wasn't running but now is
|
||||||
|
existingApp.app.setIsRunning(true);
|
||||||
|
updated = true;
|
||||||
|
}
|
||||||
|
else if (existingApp.app.getIsRunning()) {
|
||||||
|
// This app was running but now isn't
|
||||||
|
existingApp.app.setIsRunning(false);
|
||||||
|
updated = true;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// This app wasn't running and still isn't
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (updated) {
|
||||||
appGridAdapter.notifyDataSetChanged();
|
appGridAdapter.notifyDataSetChanged();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Success case
|
|
||||||
return;
|
|
||||||
} catch (GfeHttpResponseException ignored) {
|
|
||||||
} catch (IOException ignored) {
|
|
||||||
} catch (XmlPullParserException ignored) {
|
|
||||||
} finally {
|
|
||||||
spinner.dismiss();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (displayError) {
|
private void updateUiWithAppList(final List<NvApp> appList) {
|
||||||
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() {
|
AppView.this.runOnUiThread(new Runnable() {
|
||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
finish();
|
boolean updated = false;
|
||||||
|
|
||||||
|
// First handle app updates and additions
|
||||||
|
for (NvApp app : appList) {
|
||||||
|
boolean foundExistingApp = false;
|
||||||
|
|
||||||
|
// Try to update an existing app in the list first
|
||||||
|
for (int i = 0; i < appGridAdapter.getCount(); i++) {
|
||||||
|
AppObject existingApp = (AppObject) appGridAdapter.getItem(i);
|
||||||
|
if (existingApp.app.getAppId() == app.getAppId()) {
|
||||||
|
// Found the app; update its properties
|
||||||
|
if (existingApp.app.getIsRunning() != app.getIsRunning()) {
|
||||||
|
existingApp.app.setIsRunning(app.getIsRunning());
|
||||||
|
updated = true;
|
||||||
|
}
|
||||||
|
if (!existingApp.app.getAppName().equals(app.getAppName())) {
|
||||||
|
existingApp.app.setAppName(app.getAppName());
|
||||||
|
updated = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
foundExistingApp = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!foundExistingApp) {
|
||||||
|
// This app must be new
|
||||||
|
appGridAdapter.addApp(new AppObject(app));
|
||||||
|
updated = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Next handle app removals
|
||||||
|
int i = 0;
|
||||||
|
while (i < appGridAdapter.getCount()) {
|
||||||
|
boolean foundExistingApp = false;
|
||||||
|
AppObject existingApp = (AppObject) appGridAdapter.getItem(i);
|
||||||
|
|
||||||
|
// Check if this app is in the latest list
|
||||||
|
for (NvApp app : appList) {
|
||||||
|
if (existingApp.app.getAppId() == app.getAppId()) {
|
||||||
|
foundExistingApp = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// This app was removed in the latest app list
|
||||||
|
if (!foundExistingApp) {
|
||||||
|
appGridAdapter.removeApp(existingApp);
|
||||||
|
updated = true;
|
||||||
|
|
||||||
|
// Check this same index again because the item at i+1 is now at i after
|
||||||
|
// the removal
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Move on to the next item
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (updated) {
|
||||||
|
appGridAdapter.notifyDataSetChanged();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}.start();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void doStart(NvApp app) {
|
|
||||||
Intent intent = new Intent(this, Game.class);
|
|
||||||
intent.putExtra(Game.EXTRA_HOST, ipAddress.getHostAddress());
|
|
||||||
intent.putExtra(Game.EXTRA_APP, app.getAppName());
|
|
||||||
intent.putExtra(Game.EXTRA_UNIQUEID, uniqueId);
|
|
||||||
intent.putExtra(Game.EXTRA_STREAMING_REMOTE, remote);
|
|
||||||
startActivity(intent);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void doQuit(final NvApp app) {
|
|
||||||
Toast.makeText(AppView.this, getResources().getString(R.string.applist_quit_app)+" "+app.getAppName()+"...", Toast.LENGTH_SHORT).show();
|
|
||||||
new Thread(new Runnable() {
|
|
||||||
@Override
|
@Override
|
||||||
public void run() {
|
public int getAdapterFragmentLayoutId() {
|
||||||
NvHTTP httpConn;
|
return PreferenceConfiguration.readPreferences(this).listMode ?
|
||||||
String message;
|
R.layout.list_view : (PreferenceConfiguration.readPreferences(AppView.this).smallIconMode ?
|
||||||
try {
|
R.layout.app_grid_view_small : R.layout.app_grid_view);
|
||||||
httpConn = new NvHTTP(ipAddress, uniqueId, null, PlatformBinding.getCryptoProvider(AppView.this));
|
|
||||||
if (httpConn.quitApp()) {
|
|
||||||
message = getResources().getString(R.string.applist_quit_success)+" "+app.getAppName();
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
message = getResources().getString(R.string.applist_quit_fail)+" "+app.getAppName();
|
|
||||||
}
|
|
||||||
updateAppList(true);
|
|
||||||
} catch (UnknownHostException e) {
|
|
||||||
message = getResources().getString(R.string.error_unknown_host);
|
|
||||||
} catch (FileNotFoundException e) {
|
|
||||||
message = getResources().getString(R.string.error_404);
|
|
||||||
} catch (Exception e) {
|
|
||||||
message = e.getMessage();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
final String toastMessage = message;
|
|
||||||
runOnUiThread(new Runnable() {
|
|
||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void receiveAbsListView(AbsListView listView) {
|
||||||
Toast.makeText(AppView.this, toastMessage, Toast.LENGTH_LONG).show();
|
listView.setAdapter(appGridAdapter);
|
||||||
|
listView.setOnItemClickListener(new OnItemClickListener() {
|
||||||
|
@Override
|
||||||
|
public void onItemClick(AdapterView<?> arg0, View arg1, int pos,
|
||||||
|
long id) {
|
||||||
|
AppObject app = (AppObject) appGridAdapter.getItem(pos);
|
||||||
|
|
||||||
|
// Only open the context menu if something is running, otherwise start it
|
||||||
|
if (getRunningAppId() != -1) {
|
||||||
|
openContextMenu(arg1);
|
||||||
|
} else {
|
||||||
|
ServerHelper.doStart(AppView.this, app.app, computer, managerBinder);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
registerForContextMenu(listView);
|
||||||
}).start();
|
listView.requestFocus();
|
||||||
}
|
}
|
||||||
|
|
||||||
public class AppObject {
|
public class AppObject {
|
||||||
public NvApp app;
|
public final NvApp app;
|
||||||
|
|
||||||
public AppObject(NvApp app) {
|
public AppObject(NvApp app) {
|
||||||
|
if (app == null) {
|
||||||
|
throw new IllegalArgumentException("app must not be null");
|
||||||
|
}
|
||||||
this.app = app;
|
this.app = app;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -13,21 +13,26 @@ import com.limelight.nvstream.NvConnection;
|
|||||||
import com.limelight.nvstream.NvConnectionListener;
|
import com.limelight.nvstream.NvConnectionListener;
|
||||||
import com.limelight.nvstream.StreamConfiguration;
|
import com.limelight.nvstream.StreamConfiguration;
|
||||||
import com.limelight.nvstream.av.video.VideoDecoderRenderer;
|
import com.limelight.nvstream.av.video.VideoDecoderRenderer;
|
||||||
|
import com.limelight.nvstream.http.NvApp;
|
||||||
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 +46,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;
|
||||||
@@ -79,7 +90,8 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
private int drFlags = 0;
|
private int drFlags = 0;
|
||||||
|
|
||||||
public static final String EXTRA_HOST = "Host";
|
public static final String EXTRA_HOST = "Host";
|
||||||
public static final String EXTRA_APP = "App";
|
public static final String EXTRA_APP_NAME = "AppName";
|
||||||
|
public static final String EXTRA_APP_ID = "AppId";
|
||||||
public static final String EXTRA_UNIQUEID = "UniqueId";
|
public static final String EXTRA_UNIQUEID = "UniqueId";
|
||||||
public static final String EXTRA_STREAMING_REMOTE = "Remote";
|
public static final String EXTRA_STREAMING_REMOTE = "Remote";
|
||||||
|
|
||||||
@@ -87,6 +99,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);
|
||||||
|
|
||||||
@@ -154,29 +173,39 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
|||||||
wifiLock.acquire();
|
wifiLock.acquire();
|
||||||
|
|
||||||
String host = Game.this.getIntent().getStringExtra(EXTRA_HOST);
|
String host = Game.this.getIntent().getStringExtra(EXTRA_HOST);
|
||||||
String app = Game.this.getIntent().getStringExtra(EXTRA_APP);
|
String appName = Game.this.getIntent().getStringExtra(EXTRA_APP_NAME);
|
||||||
|
int appId = Game.this.getIntent().getIntExtra(EXTRA_APP_ID, StreamConfiguration.INVALID_APP_ID);
|
||||||
String uniqueId = Game.this.getIntent().getStringExtra(EXTRA_UNIQUEID);
|
String uniqueId = Game.this.getIntent().getStringExtra(EXTRA_UNIQUEID);
|
||||||
boolean remote = Game.this.getIntent().getBooleanExtra(EXTRA_STREAMING_REMOTE, false);
|
boolean remote = Game.this.getIntent().getBooleanExtra(EXTRA_STREAMING_REMOTE, false);
|
||||||
|
|
||||||
|
if (appId == StreamConfiguration.INVALID_APP_ID) {
|
||||||
|
finish();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
decoderRenderer = new ConfigurableDecoderRenderer();
|
decoderRenderer = new ConfigurableDecoderRenderer();
|
||||||
decoderRenderer.initializeWithFlags(drFlags);
|
decoderRenderer.initializeWithFlags(drFlags);
|
||||||
|
|
||||||
StreamConfiguration config = new StreamConfiguration.Builder()
|
StreamConfiguration config = new StreamConfiguration.Builder()
|
||||||
.setResolution(prefConfig.width, prefConfig.height)
|
.setResolution(prefConfig.width, prefConfig.height)
|
||||||
.setRefreshRate(prefConfig.fps)
|
.setRefreshRate(prefConfig.fps)
|
||||||
.setApp(app)
|
.setApp(new NvApp(appName, appId))
|
||||||
.setBitrate(prefConfig.bitrate * 1000)
|
.setBitrate(prefConfig.bitrate * 1000)
|
||||||
.setEnableSops(prefConfig.enableSops)
|
.setEnableSops(prefConfig.enableSops)
|
||||||
.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 : 1292)
|
.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 +254,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 +290,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 +323,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 +499,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 +529,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 +557,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));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -11,13 +12,18 @@ import com.limelight.computers.ComputerManagerListener;
|
|||||||
import com.limelight.computers.ComputerManagerService;
|
import com.limelight.computers.ComputerManagerService;
|
||||||
import com.limelight.grid.PcGridAdapter;
|
import com.limelight.grid.PcGridAdapter;
|
||||||
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.http.PairingManager;
|
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.ServerHelper;
|
||||||
import com.limelight.utils.UiHelper;
|
import com.limelight.utils.UiHelper;
|
||||||
|
|
||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
@@ -35,20 +41,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);
|
||||||
@@ -89,6 +95,8 @@ public class PcView extends Activity {
|
|||||||
private final static int UNPAIR_ID = 3;
|
private final static int UNPAIR_ID = 3;
|
||||||
private final static int WOL_ID = 4;
|
private final static int WOL_ID = 4;
|
||||||
private final static int DELETE_ID = 5;
|
private final static int DELETE_ID = 5;
|
||||||
|
private final static int RESUME_ID = 6;
|
||||||
|
private final static int QUIT_ID = 7;
|
||||||
|
|
||||||
private void initializeViews() {
|
private void initializeViews() {
|
||||||
setContentView(R.layout.activity_pc_view);
|
setContentView(R.layout.activity_pc_view);
|
||||||
@@ -102,27 +110,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 +124,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 +142,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();
|
||||||
}
|
}
|
||||||
@@ -241,8 +241,7 @@ public class PcView extends Activity {
|
|||||||
|
|
||||||
AdapterContextMenuInfo info = (AdapterContextMenuInfo) menuInfo;
|
AdapterContextMenuInfo info = (AdapterContextMenuInfo) menuInfo;
|
||||||
ComputerObject computer = (ComputerObject) pcGridAdapter.getItem(info.position);
|
ComputerObject computer = (ComputerObject) pcGridAdapter.getItem(info.position);
|
||||||
if (computer == null || computer.details == null ||
|
if (computer.details.reachability == ComputerDetails.Reachability.UNKNOWN) {
|
||||||
computer.details.reachability == ComputerDetails.Reachability.UNKNOWN) {
|
|
||||||
startComputerUpdates();
|
startComputerUpdates();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -257,8 +256,16 @@ public class PcView extends Activity {
|
|||||||
menu.add(Menu.NONE, DELETE_ID, 2, getResources().getString(R.string.pcview_menu_delete_pc));
|
menu.add(Menu.NONE, DELETE_ID, 2, getResources().getString(R.string.pcview_menu_delete_pc));
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
menu.add(Menu.NONE, APP_LIST_ID, 1, getResources().getString(R.string.pcview_menu_app_list));
|
if (computer.details.runningGameId != 0) {
|
||||||
menu.add(Menu.NONE, UNPAIR_ID, 2, getResources().getString(R.string.pcview_menu_unpair_pc));
|
menu.add(Menu.NONE, RESUME_ID, 1, getResources().getString(R.string.applist_menu_resume));
|
||||||
|
menu.add(Menu.NONE, QUIT_ID, 2, getResources().getString(R.string.applist_menu_quit));
|
||||||
|
}
|
||||||
|
|
||||||
|
menu.add(Menu.NONE, APP_LIST_ID, 3, getResources().getString(R.string.pcview_menu_app_list));
|
||||||
|
|
||||||
|
// FIXME: We used to be able to unpair here but it's been broken since GFE 2.1.x, so I've replaced
|
||||||
|
// it with delete which actually work
|
||||||
|
menu.add(Menu.NONE, DELETE_ID, 4, getResources().getString(R.string.pcview_menu_delete_pc));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -299,6 +306,10 @@ public class PcView extends Activity {
|
|||||||
else if (computer.reachability == ComputerDetails.Reachability.REMOTE) {
|
else if (computer.reachability == ComputerDetails.Reachability.REMOTE) {
|
||||||
addr = computer.remoteIp;
|
addr = computer.remoteIp;
|
||||||
}
|
}
|
||||||
|
else {
|
||||||
|
LimeLog.warning("Unknown reachability - using local IP");
|
||||||
|
addr = computer.localIp;
|
||||||
|
}
|
||||||
|
|
||||||
httpConn = new NvHTTP(addr,
|
httpConn = new NvHTTP(addr,
|
||||||
managerBinder.getUniqueId(),
|
managerBinder.getUniqueId(),
|
||||||
@@ -424,6 +435,10 @@ public class PcView extends Activity {
|
|||||||
else if (computer.reachability == ComputerDetails.Reachability.REMOTE) {
|
else if (computer.reachability == ComputerDetails.Reachability.REMOTE) {
|
||||||
addr = computer.remoteIp;
|
addr = computer.remoteIp;
|
||||||
}
|
}
|
||||||
|
else {
|
||||||
|
LimeLog.warning("Unknown reachability - using local IP");
|
||||||
|
addr = computer.localIp;
|
||||||
|
}
|
||||||
|
|
||||||
httpConn = new NvHTTP(addr,
|
httpConn = new NvHTTP(addr,
|
||||||
managerBinder.getUniqueId(),
|
managerBinder.getUniqueId(),
|
||||||
@@ -472,25 +487,15 @@ 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);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean onContextItemSelected(MenuItem item) {
|
public boolean onContextItemSelected(MenuItem item) {
|
||||||
AdapterContextMenuInfo info = (AdapterContextMenuInfo) item.getMenuInfo();
|
AdapterContextMenuInfo info = (AdapterContextMenuInfo) item.getMenuInfo();
|
||||||
ComputerObject computer = (ComputerObject) pcGridAdapter.getItem(info.position);
|
final ComputerObject computer = (ComputerObject) pcGridAdapter.getItem(info.position);
|
||||||
switch (item.getItemId())
|
switch (item.getItemId()) {
|
||||||
{
|
|
||||||
case PAIR_ID:
|
case PAIR_ID:
|
||||||
doPair(computer.details);
|
doPair(computer.details);
|
||||||
return true;
|
return true;
|
||||||
@@ -516,6 +521,32 @@ public class PcView extends Activity {
|
|||||||
doAppList(computer.details);
|
doAppList(computer.details);
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
|
case RESUME_ID:
|
||||||
|
if (managerBinder == null) {
|
||||||
|
Toast.makeText(PcView.this, getResources().getString(R.string.error_manager_not_running), Toast.LENGTH_LONG).show();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
ServerHelper.doStart(this, new NvApp("app", computer.details.runningGameId), computer.details, managerBinder);
|
||||||
|
return true;
|
||||||
|
|
||||||
|
case QUIT_ID:
|
||||||
|
if (managerBinder == null) {
|
||||||
|
Toast.makeText(PcView.this, getResources().getString(R.string.error_manager_not_running), Toast.LENGTH_LONG).show();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Display a confirmation dialog first
|
||||||
|
UiHelper.displayQuitConfirmationDialog(this, new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
ServerHelper.doQuit(PcView.this,
|
||||||
|
ServerHelper.getCurrentAddressFromComputer(computer.details),
|
||||||
|
new NvApp("app", 0), managerBinder, null);
|
||||||
|
}
|
||||||
|
}, null);
|
||||||
|
return true;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return super.onContextItemSelected(item);
|
return super.onContextItemSelected(item);
|
||||||
}
|
}
|
||||||
@@ -527,6 +558,13 @@ public class PcView extends Activity {
|
|||||||
|
|
||||||
if (details.equals(computer.details)) {
|
if (details.equals(computer.details)) {
|
||||||
pcGridAdapter.removeComputer(computer);
|
pcGridAdapter.removeComputer(computer);
|
||||||
|
pcGridAdapter.notifyDataSetChanged();
|
||||||
|
|
||||||
|
if (pcGridAdapter.getCount() == 0) {
|
||||||
|
// Show the "Discovery in progress" view
|
||||||
|
noPcFoundLayout.setVisibility(View.VISIBLE);
|
||||||
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -539,7 +577,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,10 +599,44 @@ 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;
|
||||||
|
|
||||||
public ComputerObject(ComputerDetails details) {
|
public ComputerObject(ComputerDetails details) {
|
||||||
|
if (details == null) {
|
||||||
|
throw new IllegalArgumentException("details must not be null");
|
||||||
|
}
|
||||||
this.details = details;
|
this.details = 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,8 +1,8 @@
|
|||||||
package com.limelight.binding.input;
|
package com.limelight.binding.input;
|
||||||
|
|
||||||
import java.util.HashMap;
|
import android.hardware.input.InputManager;
|
||||||
|
|
||||||
import android.os.SystemClock;
|
import android.os.SystemClock;
|
||||||
|
import android.util.SparseArray;
|
||||||
import android.view.InputDevice;
|
import android.view.InputDevice;
|
||||||
import android.view.KeyEvent;
|
import android.view.KeyEvent;
|
||||||
import android.view.MotionEvent;
|
import android.view.MotionEvent;
|
||||||
@@ -10,27 +10,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,26 +27,31 @@ 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 SparseArray<ControllerContext> contexts = new SparseArray<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;
|
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();
|
int[] ids = InputDevice.getDeviceIds();
|
||||||
for (int i = 0; i < ids.length; i++) {
|
for (int id : ids) {
|
||||||
InputDevice dev = InputDevice.getDevice(ids[i]);
|
InputDevice dev = InputDevice.getDevice(id);
|
||||||
if ((dev.getSources() & InputDevice.SOURCE_JOYSTICK) != 0 ||
|
if ((dev.getSources() & InputDevice.SOURCE_JOYSTICK) != 0 ||
|
||||||
(dev.getSources() & InputDevice.SOURCE_GAMEPAD) != 0) {
|
(dev.getSources() & InputDevice.SOURCE_GAMEPAD) != 0) {
|
||||||
// This looks like a gamepad, but we'll check X and Y to be sure
|
// This looks like a gamepad, but we'll check X and Y to be sure
|
||||||
@@ -77,15 +70,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) {
|
||||||
@@ -101,19 +95,87 @@ public class ControllerHandler {
|
|||||||
return range;
|
return range;
|
||||||
}
|
}
|
||||||
|
|
||||||
private ControllerMapping createMappingForDevice(InputDevice dev) {
|
@Override
|
||||||
ControllerMapping mapping = new ControllerMapping();
|
public void onInputDeviceAdded(int deviceId) {
|
||||||
|
// Nothing happening here yet
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onInputDeviceRemoved(int deviceId) {
|
||||||
|
ControllerContext context = contexts.get(deviceId);
|
||||||
|
if (context != null) {
|
||||||
|
LimeLog.info("Removed controller: "+context.name+" ("+deviceId+")");
|
||||||
|
releaseControllerNumber(context);
|
||||||
|
contexts.remove(deviceId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onInputDeviceChanged(int deviceId) {
|
||||||
|
// Remove and re-add
|
||||||
|
onInputDeviceRemoved(deviceId);
|
||||||
|
onInputDeviceAdded(deviceId);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void releaseControllerNumber(ControllerContext context) {
|
||||||
|
if (context.reservedControllerNumber) {
|
||||||
|
LimeLog.info("Controller number "+context.controllerNumber+" is now available");
|
||||||
|
currentControllers &= ~(1 << context.controllerNumber);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Called before sending input but after we've determined that this
|
||||||
|
// is definitely a controller (not a keyboard, mouse, or something else)
|
||||||
|
private void assignControllerNumberIfNeeded(ControllerContext context) {
|
||||||
|
if (context.assignedControllerNumber) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
LimeLog.info(context.name+" ("+context.id+") needs a controller number assigned");
|
||||||
|
if (context.name != null && context.name.contains("gpio-keys")) {
|
||||||
|
// This is the back button on Shield portable consoles
|
||||||
|
LimeLog.info("Built-in buttons hardcoded as controller 0");
|
||||||
|
context.controllerNumber = 0;
|
||||||
|
}
|
||||||
|
else if (multiControllerEnabled && context.hasJoystickAxes) {
|
||||||
|
context.controllerNumber = 0;
|
||||||
|
|
||||||
|
LimeLog.info("Reserving the next available controller number");
|
||||||
|
for (short i = 0; i < 4; i++) {
|
||||||
|
if ((currentControllers & (1 << i)) == 0) {
|
||||||
|
// Found an unused controller value
|
||||||
|
currentControllers |= (1 << i);
|
||||||
|
context.controllerNumber = i;
|
||||||
|
context.reservedControllerNumber = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
LimeLog.info("Not reserving a controller number");
|
||||||
|
context.controllerNumber = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
LimeLog.info("Assigned as controller "+context.controllerNumber);
|
||||||
|
context.assignedControllerNumber = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private ControllerContext createContextForDevice(InputDevice dev) {
|
||||||
|
ControllerContext context = new ControllerContext();
|
||||||
String devName = dev.getName();
|
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();
|
||||||
if (getMotionRangeForJoystickAxis(dev, mapping.leftStickXAxis) != null &&
|
|
||||||
getMotionRangeForJoystickAxis(dev, mapping.leftStickYAxis) != null) {
|
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
|
// This is a gamepad
|
||||||
hasGameController = true;
|
hasGameController = true;
|
||||||
mapping.hasJoystickAxes = true;
|
context.hasJoystickAxes = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
InputDevice.MotionRange leftTriggerRange = getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_LTRIGGER);
|
InputDevice.MotionRange leftTriggerRange = getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_LTRIGGER);
|
||||||
@@ -123,14 +185,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
|
||||||
{
|
{
|
||||||
@@ -139,34 +201,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);
|
||||||
@@ -174,8 +236,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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -184,30 +246,30 @@ 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 13% (around XInput's default)
|
// For triggers without (valid) deadzones, we'll use 13% (around XInput's default)
|
||||||
if (mapping.triggerDeadzone < 0.13f ||
|
if (context.triggerDeadzone < 0.13f ||
|
||||||
mapping.triggerDeadzone > 0.30f)
|
context.triggerDeadzone > 0.30f)
|
||||||
{
|
{
|
||||||
mapping.triggerDeadzone = 0.13f;
|
context.triggerDeadzone = 0.13f;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -221,66 +283,78 @@ public class ControllerHandler {
|
|||||||
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// The ASUS Gamepad has triggers that sit far forward and are prone to false presses
|
// 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
|
// so we increase the deadzone on them to minimize this
|
||||||
mapping.triggerDeadzone = 0.30f;
|
context.triggerDeadzone = 0.30f;
|
||||||
}
|
}
|
||||||
// Classify this device as a remote by name
|
// Classify this device as a remote by name
|
||||||
else if (devName.contains("Fire TV Remote") || devName.contains("Nexus Remote")) {
|
else if (devName.contains("Fire TV Remote") || devName.contains("Nexus Remote")) {
|
||||||
// It's only a remote if it doesn't any sticks
|
// It's only a remote if it doesn't any sticks
|
||||||
if (!mapping.hasJoystickAxes) {
|
if (!context.hasJoystickAxes) {
|
||||||
mapping.isRemote = true;
|
context.isRemote = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// SHIELD controllers will use small stick deadzones
|
||||||
|
else if (devName.contains("SHIELD")) {
|
||||||
|
context.leftStickDeadzoneRadius = 0.07f;
|
||||||
|
context.rightStickDeadzoneRadius = 0.07f;
|
||||||
|
}
|
||||||
|
// Samsung's face buttons appear as a non-virtual button so we'll classify them as remotes
|
||||||
|
// so the back button gets passed through to exit streaming
|
||||||
|
else if (devName.equals("sec_touchscreen")) {
|
||||||
|
context.isRemote = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
LimeLog.info("Analog stick deadzone: "+mapping.leftStickDeadzoneRadius+" "+mapping.rightStickDeadzoneRadius);
|
LimeLog.info("Analog stick deadzone: "+context.leftStickDeadzoneRadius+" "+context.rightStickDeadzoneRadius);
|
||||||
LimeLog.info("Trigger deadzone: "+mapping.triggerDeadzone);
|
LimeLog.info("Trigger deadzone: "+context.triggerDeadzone);
|
||||||
|
|
||||||
return mapping;
|
return context;
|
||||||
}
|
}
|
||||||
|
|
||||||
private ControllerMapping getMappingForDevice(InputDevice dev) {
|
private ControllerContext getContextForDevice(InputDevice dev) {
|
||||||
// Unknown devices use the default mapping
|
// Unknown devices use the default context
|
||||||
if (dev == null) {
|
if (dev == null) {
|
||||||
return defaultMapping;
|
return defaultContext;
|
||||||
}
|
}
|
||||||
|
|
||||||
String descriptor = dev.getDescriptor();
|
// Return the existing context if it exists
|
||||||
|
ControllerContext context = contexts.get(dev.getId());
|
||||||
// Return the existing mapping if it exists
|
if (context != null) {
|
||||||
ControllerMapping mapping = mappings.get(descriptor);
|
return context;
|
||||||
if (mapping != null) {
|
|
||||||
return mapping;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Otherwise create a new mapping
|
// Otherwise create a new context
|
||||||
mapping = createMappingForDevice(dev);
|
context = createContextForDevice(dev);
|
||||||
mappings.put(descriptor, mapping);
|
contexts.put(dev.getId(), context);
|
||||||
|
|
||||||
return mapping;
|
return context;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void sendControllerInputPacket() {
|
private void sendControllerInputPacket(ControllerContext context) {
|
||||||
conn.sendControllerInput(inputMap, leftTrigger, rightTrigger,
|
assignControllerNumberIfNeeded(context);
|
||||||
leftStickX, leftStickY, rightStickX, rightStickY);
|
conn.sendControllerInput(context.controllerNumber, context.inputMap,
|
||||||
|
context.leftTrigger, context.rightTrigger,
|
||||||
|
context.leftStickX, context.leftStickY,
|
||||||
|
context.rightStickX, context.rightStickY);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return a valid keycode, 0 to consume, or -1 to not consume the event
|
// Return a valid keycode, 0 to consume, or -1 to not consume the event
|
||||||
// Device MAY BE NULL
|
// Device MAY BE NULL
|
||||||
private int handleRemapping(ControllerMapping mapping, KeyEvent event) {
|
private int handleRemapping(ControllerContext context, KeyEvent event) {
|
||||||
// For remotes, don't capture the back button
|
// For remotes, don't capture the back button
|
||||||
if (mapping.isRemote) {
|
if (context.isRemote) {
|
||||||
if (event.getKeyCode() == KeyEvent.KEYCODE_BACK) {
|
if (event.getKeyCode() == KeyEvent.KEYCODE_BACK) {
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mapping.isDualShock4) {
|
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;
|
||||||
@@ -319,7 +393,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:
|
||||||
@@ -330,9 +404,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
|
||||||
@@ -365,171 +439,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;
|
||||||
}
|
}
|
||||||
@@ -548,75 +587,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);
|
||||||
@@ -625,16 +669,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);
|
||||||
@@ -642,109 +686,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;
|
||||||
@@ -764,7 +814,31 @@ 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 isRemote;
|
||||||
public boolean hasJoystickAxes;
|
public boolean hasJoystickAxes;
|
||||||
|
|
||||||
|
public boolean assignedControllerNumber;
|
||||||
|
public boolean reservedControllerNumber;
|
||||||
|
public short controllerNumber;
|
||||||
|
|
||||||
|
public short inputMap = 0x0000;
|
||||||
|
public byte leftTrigger = 0x00;
|
||||||
|
public byte rightTrigger = 0x00;
|
||||||
|
public short rightStickX = 0x0000;
|
||||||
|
public short rightStickY = 0x0000;
|
||||||
|
public short leftStickX = 0x0000;
|
||||||
|
public short leftStickY = 0x0000;
|
||||||
|
public int emulatingButtonFlags = 0;
|
||||||
|
|
||||||
|
// Used for OUYA bumper state tracking since they force all buttons
|
||||||
|
// up when the OUYA button goes down. We watch the last time we get
|
||||||
|
// a bumper up and compare that to our maximum delay when we receive
|
||||||
|
// a Start button press to see if we should activate one of our
|
||||||
|
// emulated button combos.
|
||||||
|
public long lastLbUpTime = 0;
|
||||||
|
public long lastRbUpTime = 0;
|
||||||
|
|
||||||
|
public long startDownTime = 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -15,7 +15,7 @@ public class KeyboardTranslator extends KeycodeTranslator {
|
|||||||
/**
|
/**
|
||||||
* GFE's prefix for every key code
|
* 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() {
|
||||||
@@ -178,7 +178,6 @@ public class AndroidCpuDecoderRenderer extends EnhancedDecoderRenderer {
|
|||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
long nextFrameTime = System.currentTimeMillis();
|
long nextFrameTime = System.currentTimeMillis();
|
||||||
DecodeUnit du;
|
|
||||||
while (!isInterrupted())
|
while (!isInterrupted())
|
||||||
{
|
{
|
||||||
long diff = nextFrameTime - System.currentTimeMillis();
|
long diff = nextFrameTime - System.currentTimeMillis();
|
||||||
@@ -232,7 +231,8 @@ public class AndroidCpuDecoderRenderer extends EnhancedDecoderRenderer {
|
|||||||
if (decodeUnit.getDataLength() <= DECODER_BUFFER_SIZE) {
|
if (decodeUnit.getDataLength() <= DECODER_BUFFER_SIZE) {
|
||||||
decoderBuffer.clear();
|
decoderBuffer.clear();
|
||||||
|
|
||||||
for (ByteBufferDescriptor bbd : decodeUnit.getBufferList()) {
|
for (ByteBufferDescriptor bbd = decodeUnit.getBufferHead();
|
||||||
|
bbd != null; bbd = bbd.nextDescriptor) {
|
||||||
decoderBuffer.put(bbd.data, bbd.offset, bbd.length);
|
decoderBuffer.put(bbd.data, bbd.offset, bbd.length);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -242,7 +242,8 @@ public class AndroidCpuDecoderRenderer extends EnhancedDecoderRenderer {
|
|||||||
data = new byte[decodeUnit.getDataLength()+AvcDecoder.getInputPaddingSize()];
|
data = new byte[decodeUnit.getDataLength()+AvcDecoder.getInputPaddingSize()];
|
||||||
|
|
||||||
int offset = 0;
|
int offset = 0;
|
||||||
for (ByteBufferDescriptor bbd : decodeUnit.getBufferList()) {
|
for (ByteBufferDescriptor bbd = decodeUnit.getBufferHead();
|
||||||
|
bbd != null; bbd = bbd.nextDescriptor) {
|
||||||
System.arraycopy(bbd.data, bbd.offset, data, offset, bbd.length);
|
System.arraycopy(bbd.data, bbd.offset, data, offset, bbd.length);
|
||||||
offset += bbd.length;
|
offset += bbd.length;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package com.limelight.binding.video;
|
package com.limelight.binding.video;
|
||||||
|
|
||||||
|
import com.limelight.nvstream.av.DecodeUnit;
|
||||||
import com.limelight.nvstream.av.video.VideoDecoderRenderer;
|
import com.limelight.nvstream.av.video.VideoDecoderRenderer;
|
||||||
import com.limelight.nvstream.av.video.VideoDepacketizer;
|
import com.limelight.nvstream.av.video.VideoDepacketizer;
|
||||||
|
|
||||||
@@ -55,6 +56,11 @@ public class ConfigurableDecoderRenderer extends EnhancedDecoderRenderer {
|
|||||||
return decoderRenderer.getCapabilities();
|
return decoderRenderer.getCapabilities();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void directSubmitDecodeUnit(DecodeUnit du) {
|
||||||
|
decoderRenderer.directSubmitDecodeUnit(du);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int getAverageDecoderLatency() {
|
public int getAverageDecoderLatency() {
|
||||||
if (decoderRenderer != null) {
|
if (decoderRenderer != null) {
|
||||||
|
|||||||
@@ -2,6 +2,6 @@ package com.limelight.binding.video;
|
|||||||
|
|
||||||
import com.limelight.nvstream.av.video.VideoDecoderRenderer;
|
import com.limelight.nvstream.av.video.VideoDecoderRenderer;
|
||||||
|
|
||||||
public abstract class EnhancedDecoderRenderer implements VideoDecoderRenderer {
|
public abstract class EnhancedDecoderRenderer extends VideoDecoderRenderer {
|
||||||
public abstract String getDecoderName();
|
public abstract String getDecoderName();
|
||||||
}
|
}
|
||||||
@@ -28,9 +28,9 @@ public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer {
|
|||||||
private ByteBuffer[] videoDecoderInputBuffers;
|
private ByteBuffer[] videoDecoderInputBuffers;
|
||||||
private MediaCodec videoDecoder;
|
private MediaCodec videoDecoder;
|
||||||
private Thread rendererThread;
|
private Thread rendererThread;
|
||||||
private boolean needsSpsBitstreamFixup, isExynos4;
|
private final boolean needsSpsBitstreamFixup, isExynos4;
|
||||||
private VideoDepacketizer depacketizer;
|
private VideoDepacketizer depacketizer;
|
||||||
private boolean adaptivePlayback;
|
private final boolean adaptivePlayback, directSubmit;
|
||||||
private int initialWidth, initialHeight;
|
private int initialWidth, initialHeight;
|
||||||
|
|
||||||
private boolean needsBaselineSpsHack;
|
private boolean needsBaselineSpsHack;
|
||||||
@@ -46,8 +46,6 @@ public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer {
|
|||||||
private int numPpsIn;
|
private int numPpsIn;
|
||||||
private int numIframeIn;
|
private int numIframeIn;
|
||||||
|
|
||||||
private static final boolean ENABLE_ASYNC_RENDERER = false;
|
|
||||||
|
|
||||||
@TargetApi(Build.VERSION_CODES.KITKAT)
|
@TargetApi(Build.VERSION_CODES.KITKAT)
|
||||||
public MediaCodecDecoderRenderer() {
|
public MediaCodecDecoderRenderer() {
|
||||||
//dumpDecoders();
|
//dumpDecoders();
|
||||||
@@ -58,12 +56,15 @@ public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer {
|
|||||||
}
|
}
|
||||||
if (decoder == null) {
|
if (decoder == null) {
|
||||||
// This case is handled later in setup()
|
// This case is handled later in setup()
|
||||||
|
needsSpsBitstreamFixup = isExynos4 =
|
||||||
|
adaptivePlayback = directSubmit = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
decoderName = decoder.getName();
|
decoderName = decoder.getName();
|
||||||
|
|
||||||
// Set decoder-specific attributes
|
// Set decoder-specific attributes
|
||||||
|
directSubmit = MediaCodecHelper.decoderCanDirectSubmit(decoderName, decoder);
|
||||||
adaptivePlayback = MediaCodecHelper.decoderSupportsAdaptivePlayback(decoderName, decoder);
|
adaptivePlayback = MediaCodecHelper.decoderSupportsAdaptivePlayback(decoderName, decoder);
|
||||||
needsSpsBitstreamFixup = MediaCodecHelper.decoderNeedsSpsBitstreamRestrictions(decoderName, decoder);
|
needsSpsBitstreamFixup = MediaCodecHelper.decoderNeedsSpsBitstreamRestrictions(decoderName, decoder);
|
||||||
needsBaselineSpsHack = MediaCodecHelper.decoderNeedsBaselineSpsHack(decoderName, decoder);
|
needsBaselineSpsHack = MediaCodecHelper.decoderNeedsBaselineSpsHack(decoderName, decoder);
|
||||||
@@ -77,9 +78,11 @@ public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer {
|
|||||||
if (isExynos4) {
|
if (isExynos4) {
|
||||||
LimeLog.info("Decoder "+decoderName+" is on Exynos 4");
|
LimeLog.info("Decoder "+decoderName+" is on Exynos 4");
|
||||||
}
|
}
|
||||||
|
if (directSubmit) {
|
||||||
|
LimeLog.info("Decoder "+decoderName+" will use direct submit");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
|
|
||||||
@Override
|
@Override
|
||||||
public boolean setup(int width, int height, int redrawRate, Object renderTarget, int drFlags) {
|
public boolean setup(int width, int height, int redrawRate, Object renderTarget, int drFlags) {
|
||||||
this.initialWidth = width;
|
this.initialWidth = width;
|
||||||
@@ -107,52 +110,6 @@ public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer {
|
|||||||
videoFormat.setInteger(MediaFormat.KEY_MAX_HEIGHT, height);
|
videoFormat.setInteger(MediaFormat.KEY_MAX_HEIGHT, height);
|
||||||
}
|
}
|
||||||
|
|
||||||
// On Lollipop, we use asynchronous mode to avoid having a busy looping renderer thread
|
|
||||||
if (ENABLE_ASYNC_RENDERER && Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
|
||||||
videoDecoder.setCallback(new MediaCodec.Callback() {
|
|
||||||
@Override
|
|
||||||
public void onOutputFormatChanged(MediaCodec codec, MediaFormat format) {
|
|
||||||
LimeLog.info("Output format changed");
|
|
||||||
LimeLog.info("New output Format: " + format);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onOutputBufferAvailable(MediaCodec codec, int index,
|
|
||||||
BufferInfo info) {
|
|
||||||
try {
|
|
||||||
// FIXME: It looks like we can't frameskip here
|
|
||||||
codec.releaseOutputBuffer(index, true);
|
|
||||||
} catch (Exception e) {
|
|
||||||
handleDecoderException(MediaCodecDecoderRenderer.this, e, null, 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onInputBufferAvailable(MediaCodec codec, int index) {
|
|
||||||
try {
|
|
||||||
submitDecodeUnit(depacketizer.takeNextDecodeUnit(), codec.getInputBuffer(index), index);
|
|
||||||
} catch (InterruptedException e) {
|
|
||||||
// What do we do here?
|
|
||||||
e.printStackTrace();
|
|
||||||
} catch (Exception e) {
|
|
||||||
handleDecoderException(MediaCodecDecoderRenderer.this, e, null, 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onError(MediaCodec codec, CodecException e) {
|
|
||||||
if (e.isTransient()) {
|
|
||||||
LimeLog.warning(e.getDiagnosticInfo());
|
|
||||||
e.printStackTrace();
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
LimeLog.severe(e.getDiagnosticInfo());
|
|
||||||
e.printStackTrace();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
videoDecoder.configure(videoFormat, ((SurfaceHolder)renderTarget).getSurface(), null, 0);
|
videoDecoder.configure(videoFormat, ((SurfaceHolder)renderTarget).getSurface(), null, 0);
|
||||||
videoDecoder.setVideoScalingMode(MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT);
|
videoDecoder.setVideoScalingMode(MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT);
|
||||||
|
|
||||||
@@ -162,7 +119,7 @@ public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
|
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
|
||||||
private void handleDecoderException(MediaCodecDecoderRenderer dr, Exception e, ByteBuffer buf, int codecFlags) {
|
private void handleDecoderException(Exception e, ByteBuffer buf, int codecFlags) {
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||||
if (e instanceof CodecException) {
|
if (e instanceof CodecException) {
|
||||||
CodecException codecExc = (CodecException) e;
|
CodecException codecExc = (CodecException) e;
|
||||||
@@ -177,15 +134,96 @@ public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Only throw if this happens at the beginning of a stream
|
||||||
|
if (totalFrames < 60) {
|
||||||
if (buf != null || codecFlags != 0) {
|
if (buf != null || codecFlags != 0) {
|
||||||
throw new RendererException(dr, e, buf, codecFlags);
|
throw new RendererException(this, e, buf, codecFlags);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
throw new RendererException(dr, e);
|
throw new RendererException(this, e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void startRendererThread()
|
private void startDirectSubmitRendererThread()
|
||||||
|
{
|
||||||
|
rendererThread = new Thread() {
|
||||||
|
@SuppressWarnings("deprecation")
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
BufferInfo info = new BufferInfo();
|
||||||
|
while (!isInterrupted()) {
|
||||||
|
try {
|
||||||
|
// Try to output a frame
|
||||||
|
int outIndex = videoDecoder.dequeueOutputBuffer(info, 50000);
|
||||||
|
if (outIndex >= 0) {
|
||||||
|
long presentationTimeUs = info.presentationTimeUs;
|
||||||
|
int lastIndex = outIndex;
|
||||||
|
|
||||||
|
// Get the last output buffer in the queue
|
||||||
|
while ((outIndex = videoDecoder.dequeueOutputBuffer(info, 0)) >= 0) {
|
||||||
|
videoDecoder.releaseOutputBuffer(lastIndex, false);
|
||||||
|
|
||||||
|
lastIndex = outIndex;
|
||||||
|
presentationTimeUs = info.presentationTimeUs;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render the last buffer
|
||||||
|
videoDecoder.releaseOutputBuffer(lastIndex, true);
|
||||||
|
|
||||||
|
// Add delta time to the totals (excluding probable outliers)
|
||||||
|
long delta = System.currentTimeMillis() - (presentationTimeUs / 1000);
|
||||||
|
if (delta >= 0 && delta < 1000) {
|
||||||
|
decoderTimeMs += delta;
|
||||||
|
totalTimeMs += delta;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
switch (outIndex) {
|
||||||
|
case MediaCodec.INFO_TRY_AGAIN_LATER:
|
||||||
|
break;
|
||||||
|
case MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED:
|
||||||
|
LimeLog.info("Output buffers changed");
|
||||||
|
break;
|
||||||
|
case MediaCodec.INFO_OUTPUT_FORMAT_CHANGED:
|
||||||
|
LimeLog.info("Output format changed");
|
||||||
|
LimeLog.info("New output Format: " + videoDecoder.getOutputFormat());
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
handleDecoderException(e, null, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
rendererThread.setName("Video - Renderer (MediaCodec)");
|
||||||
|
rendererThread.setPriority(Thread.NORM_PRIORITY + 2);
|
||||||
|
rendererThread.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
private int dequeueInputBuffer(boolean wait, boolean infiniteWait) {
|
||||||
|
int index;
|
||||||
|
long startTime, queueTime;
|
||||||
|
|
||||||
|
startTime = System.currentTimeMillis();
|
||||||
|
|
||||||
|
index = videoDecoder.dequeueInputBuffer(wait ? (infiniteWait ? -1 : 3000) : 0);
|
||||||
|
if (index < 0) {
|
||||||
|
return index;
|
||||||
|
}
|
||||||
|
|
||||||
|
queueTime = System.currentTimeMillis();
|
||||||
|
|
||||||
|
if (queueTime - startTime >= 20) {
|
||||||
|
LimeLog.warning("Queue input buffer ran long: "+(queueTime - startTime)+" ms");
|
||||||
|
}
|
||||||
|
|
||||||
|
return index;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void startLegacyRendererThread()
|
||||||
{
|
{
|
||||||
rendererThread = new Thread() {
|
rendererThread = new Thread() {
|
||||||
@SuppressWarnings("deprecation")
|
@SuppressWarnings("deprecation")
|
||||||
@@ -194,6 +232,7 @@ public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer {
|
|||||||
BufferInfo info = new BufferInfo();
|
BufferInfo info = new BufferInfo();
|
||||||
DecodeUnit du = null;
|
DecodeUnit du = null;
|
||||||
int inputIndex = -1;
|
int inputIndex = -1;
|
||||||
|
long lastDuDequeueTime = 0;
|
||||||
while (!isInterrupted())
|
while (!isInterrupted())
|
||||||
{
|
{
|
||||||
// In order to get as much data to the decoder as early as possible,
|
// In order to get as much data to the decoder as early as possible,
|
||||||
@@ -201,8 +240,12 @@ public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer {
|
|||||||
if (inputIndex == -1 && du == null) {
|
if (inputIndex == -1 && du == null) {
|
||||||
try {
|
try {
|
||||||
for (int i = 0; i < 5; i++) {
|
for (int i = 0; i < 5; i++) {
|
||||||
inputIndex = videoDecoder.dequeueInputBuffer(0);
|
inputIndex = dequeueInputBuffer(false, false);
|
||||||
du = depacketizer.pollNextDecodeUnit();
|
du = depacketizer.pollNextDecodeUnit();
|
||||||
|
if (du != null) {
|
||||||
|
lastDuDequeueTime = System.currentTimeMillis();
|
||||||
|
notifyDuReceived(du);
|
||||||
|
}
|
||||||
|
|
||||||
// Stop if we can't get a DU or input buffer
|
// Stop if we can't get a DU or input buffer
|
||||||
if (du == null || inputIndex == -1) {
|
if (du == null || inputIndex == -1) {
|
||||||
@@ -216,7 +259,7 @@ public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer {
|
|||||||
}
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
inputIndex = -1;
|
inputIndex = -1;
|
||||||
handleDecoderException(MediaCodecDecoderRenderer.this, e, null, 0);
|
handleDecoderException(e, null, 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -231,21 +274,30 @@ public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer {
|
|||||||
// If we've got a DU waiting to be given to the decoder,
|
// If we've got a DU waiting to be given to the decoder,
|
||||||
// wait a full 3 ms for an input buffer. Otherwise
|
// wait a full 3 ms for an input buffer. Otherwise
|
||||||
// just see if we can get one immediately.
|
// just see if we can get one immediately.
|
||||||
inputIndex = videoDecoder.dequeueInputBuffer(du != null ? 3000 : 0);
|
inputIndex = dequeueInputBuffer(du != null, false);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
inputIndex = -1;
|
inputIndex = -1;
|
||||||
handleDecoderException(MediaCodecDecoderRenderer.this, e, null, 0);
|
handleDecoderException(e, null, 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Grab a decode unit if we don't have one already
|
// Grab a decode unit if we don't have one already
|
||||||
if (du == null) {
|
if (du == null) {
|
||||||
du = depacketizer.pollNextDecodeUnit();
|
du = depacketizer.pollNextDecodeUnit();
|
||||||
|
if (du != null) {
|
||||||
|
lastDuDequeueTime = System.currentTimeMillis();
|
||||||
|
notifyDuReceived(du);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// If we've got both a decode unit and an input buffer, we'll
|
// If we've got both a decode unit and an input buffer, we'll
|
||||||
// submit now. Otherwise, we wait until we have one.
|
// submit now. Otherwise, we wait until we have one.
|
||||||
if (du != null && inputIndex >= 0) {
|
if (du != null && inputIndex >= 0) {
|
||||||
|
long submissionTime = System.currentTimeMillis();
|
||||||
|
if (submissionTime - lastDuDequeueTime >= 20) {
|
||||||
|
LimeLog.warning("Receiving an input buffer took too long: "+(submissionTime - lastDuDequeueTime)+" ms");
|
||||||
|
}
|
||||||
|
|
||||||
submitDecodeUnit(du, videoDecoderInputBuffers[inputIndex], inputIndex);
|
submitDecodeUnit(du, videoDecoderInputBuffers[inputIndex], inputIndex);
|
||||||
|
|
||||||
// DU and input buffer have both been consumed
|
// DU and input buffer have both been consumed
|
||||||
@@ -298,7 +350,7 @@ public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
handleDecoderException(MediaCodecDecoderRenderer.this, e, null, 0);
|
handleDecoderException(e, null, 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -316,11 +368,15 @@ public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer {
|
|||||||
// Start the decoder
|
// Start the decoder
|
||||||
videoDecoder.start();
|
videoDecoder.start();
|
||||||
|
|
||||||
// On devices pre-Lollipop, we'll use a rendering thread
|
|
||||||
if (!ENABLE_ASYNC_RENDERER || Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
|
|
||||||
videoDecoderInputBuffers = videoDecoder.getInputBuffers();
|
videoDecoderInputBuffers = videoDecoder.getInputBuffers();
|
||||||
startRendererThread();
|
|
||||||
|
if (directSubmit) {
|
||||||
|
startDirectSubmitRendererThread();
|
||||||
}
|
}
|
||||||
|
else {
|
||||||
|
startLegacyRendererThread();
|
||||||
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -357,7 +413,7 @@ public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer {
|
|||||||
timestampUs, codecFlags);
|
timestampUs, codecFlags);
|
||||||
break;
|
break;
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
handleDecoderException(this, e, null, codecFlags);
|
handleDecoderException(e, null, codecFlags);
|
||||||
lastException = e;
|
lastException = e;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -369,14 +425,7 @@ public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer {
|
|||||||
|
|
||||||
@SuppressWarnings("deprecation")
|
@SuppressWarnings("deprecation")
|
||||||
private void submitDecodeUnit(DecodeUnit decodeUnit, ByteBuffer buf, int inputBufferIndex) {
|
private void submitDecodeUnit(DecodeUnit decodeUnit, ByteBuffer buf, int inputBufferIndex) {
|
||||||
long currentTime = System.currentTimeMillis();
|
long timestampUs = System.currentTimeMillis() * 1000;
|
||||||
long delta = currentTime-decodeUnit.getReceiveTimestamp();
|
|
||||||
if (delta >= 0 && delta < 1000) {
|
|
||||||
totalTimeMs += currentTime-decodeUnit.getReceiveTimestamp();
|
|
||||||
totalFrames++;
|
|
||||||
}
|
|
||||||
|
|
||||||
long timestampUs = currentTime * 1000;
|
|
||||||
if (timestampUs <= lastTimestampUs) {
|
if (timestampUs <= lastTimestampUs) {
|
||||||
// We can't submit multiple buffers with the same timestamp
|
// We can't submit multiple buffers with the same timestamp
|
||||||
// so bump it up by one before queuing
|
// so bump it up by one before queuing
|
||||||
@@ -400,7 +449,7 @@ public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer {
|
|||||||
boolean needsSpsReplay = false;
|
boolean needsSpsReplay = false;
|
||||||
|
|
||||||
if ((decodeUnitFlags & DecodeUnit.DU_FLAG_CODEC_CONFIG) != 0) {
|
if ((decodeUnitFlags & DecodeUnit.DU_FLAG_CODEC_CONFIG) != 0) {
|
||||||
ByteBufferDescriptor header = decodeUnit.getBufferList().get(0);
|
ByteBufferDescriptor header = decodeUnit.getBufferHead();
|
||||||
if (header.data[header.offset+4] == 0x67) {
|
if (header.data[header.offset+4] == 0x67) {
|
||||||
numSpsIn++;
|
numSpsIn++;
|
||||||
|
|
||||||
@@ -411,6 +460,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.
|
||||||
//
|
//
|
||||||
@@ -468,8 +534,8 @@ public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Copy data from our buffer list into the input buffer
|
// Copy data from our buffer list into the input buffer
|
||||||
for (ByteBufferDescriptor desc : decodeUnit.getBufferList())
|
for (ByteBufferDescriptor desc = decodeUnit.getBufferHead();
|
||||||
{
|
desc != null; desc = desc.nextDescriptor) {
|
||||||
buf.put(desc.data, desc.offset, desc.length);
|
buf.put(desc.data, desc.offset, desc.length);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -485,7 +551,7 @@ public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void replaySps() {
|
private void replaySps() {
|
||||||
int inputIndex = videoDecoder.dequeueInputBuffer(-1);
|
int inputIndex = dequeueInputBuffer(true, true);
|
||||||
ByteBuffer inputBuffer = videoDecoderInputBuffers[inputIndex];
|
ByteBuffer inputBuffer = videoDecoderInputBuffers[inputIndex];
|
||||||
|
|
||||||
inputBuffer.clear();
|
inputBuffer.clear();
|
||||||
@@ -513,8 +579,15 @@ public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int getCapabilities() {
|
public int getCapabilities() {
|
||||||
return adaptivePlayback ?
|
int caps = 0;
|
||||||
|
|
||||||
|
caps |= adaptivePlayback ?
|
||||||
VideoDecoderRenderer.CAPABILITY_ADAPTIVE_RESOLUTION : 0;
|
VideoDecoderRenderer.CAPABILITY_ADAPTIVE_RESOLUTION : 0;
|
||||||
|
|
||||||
|
caps |= directSubmit ?
|
||||||
|
VideoDecoderRenderer.CAPABILITY_DIRECT_SUBMIT : 0;
|
||||||
|
|
||||||
|
return caps;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -538,11 +611,40 @@ public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer {
|
|||||||
return decoderName;
|
return decoderName;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void notifyDuReceived(DecodeUnit du) {
|
||||||
|
long currentTime = System.currentTimeMillis();
|
||||||
|
long delta = currentTime-du.getReceiveTimestamp();
|
||||||
|
if (delta >= 0 && delta < 1000) {
|
||||||
|
totalTimeMs += currentTime-du.getReceiveTimestamp();
|
||||||
|
totalFrames++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void directSubmitDecodeUnit(DecodeUnit du) {
|
||||||
|
int inputIndex;
|
||||||
|
|
||||||
|
notifyDuReceived(du);
|
||||||
|
|
||||||
|
for (;;) {
|
||||||
|
try {
|
||||||
|
inputIndex = dequeueInputBuffer(true, true);
|
||||||
|
break;
|
||||||
|
} catch (Exception e) {
|
||||||
|
handleDecoderException(e, null, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (inputIndex >= 0) {
|
||||||
|
submitDecodeUnit(du, videoDecoderInputBuffers[inputIndex], inputIndex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public class RendererException extends RuntimeException {
|
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,27 @@ 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;
|
||||||
|
private static final List<String> directSubmitPrefixes;
|
||||||
|
|
||||||
|
static {
|
||||||
|
directSubmitPrefixes = new LinkedList<String>();
|
||||||
|
|
||||||
|
// These decoders have low enough input buffer latency that they
|
||||||
|
// can be directly invoked from the receive thread
|
||||||
|
directSubmitPrefixes.add("omx.qcom");
|
||||||
|
directSubmitPrefixes.add("omx.sec");
|
||||||
|
directSubmitPrefixes.add("omx.exynos");
|
||||||
|
directSubmitPrefixes.add("omx.intel");
|
||||||
|
directSubmitPrefixes.add("omx.brcm");
|
||||||
|
directSubmitPrefixes.add("omx.TI");
|
||||||
|
directSubmitPrefixes.add("omx.arc");
|
||||||
|
}
|
||||||
|
|
||||||
static {
|
static {
|
||||||
preferredDecoders = new LinkedList<String>();
|
preferredDecoders = new LinkedList<String>();
|
||||||
@@ -98,6 +113,10 @@ public class MediaCodecHelper {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static boolean decoderCanDirectSubmit(String decoderName, MediaCodecInfo decoderInfo) {
|
||||||
|
return isDecoderInList(directSubmitPrefixes, decoderName) && !isExynos4Device();
|
||||||
|
}
|
||||||
|
|
||||||
public static boolean decoderNeedsSpsBitstreamRestrictions(String decoderName, MediaCodecInfo decoderInfo) {
|
public static boolean decoderNeedsSpsBitstreamRestrictions(String decoderName, MediaCodecInfo decoderInfo) {
|
||||||
return isDecoderInList(spsFixupBitstreamFixupDecoderPrefixes, decoderName);
|
return isDecoderInList(spsFixupBitstreamFixupDecoderPrefixes, decoderName);
|
||||||
}
|
}
|
||||||
@@ -146,7 +165,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
|
||||||
@@ -217,7 +236,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,25 @@
|
|||||||
package com.limelight.computers;
|
package com.limelight.computers;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
import java.io.StringReader;
|
||||||
import java.net.InetAddress;
|
import java.net.InetAddress;
|
||||||
|
import java.net.InetSocketAddress;
|
||||||
|
import java.net.Socket;
|
||||||
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 +28,24 @@ import android.content.ServiceConnection;
|
|||||||
import android.os.Binder;
|
import android.os.Binder;
|
||||||
import android.os.IBinder;
|
import android.os.IBinder;
|
||||||
|
|
||||||
public class ComputerManagerService extends Service {
|
import org.xmlpull.v1.XmlPullParserException;
|
||||||
private static final int POLLING_PERIOD_MS = 3000;
|
|
||||||
private static final int MDNS_QUERY_PERIOD_MS = 1000;
|
|
||||||
|
|
||||||
private ComputerManagerBinder binder = new ComputerManagerBinder();
|
public class ComputerManagerService extends Service {
|
||||||
|
private static final int SERVERINFO_POLLING_PERIOD_MS = 3000;
|
||||||
|
private static final int APPLIST_POLLING_PERIOD_MS = 30000;
|
||||||
|
private static final int MDNS_QUERY_PERIOD_MS = 1000;
|
||||||
|
private static final int FAST_POLL_TIMEOUT = 500;
|
||||||
|
private static final int OFFLINE_POLL_TRIES = 3;
|
||||||
|
|
||||||
|
private final ComputerManagerBinder binder = new ComputerManagerBinder();
|
||||||
|
|
||||||
private ComputerDatabaseManager dbManager;
|
private 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 +69,7 @@ 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, int offlineCount) throws InterruptedException {
|
||||||
{
|
|
||||||
boolean newPc = details.name.isEmpty();
|
|
||||||
|
|
||||||
if (!getLocalDatabaseReference()) {
|
if (!getLocalDatabaseReference()) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -66,12 +77,23 @@ public class ComputerManagerService extends Service {
|
|||||||
activePolls.incrementAndGet();
|
activePolls.incrementAndGet();
|
||||||
|
|
||||||
// Poll the machine
|
// Poll the machine
|
||||||
if (!doPollMachine(details)) {
|
try {
|
||||||
|
if (!pollComputer(details)) {
|
||||||
|
if (!newPc && offlineCount < OFFLINE_POLL_TRIES) {
|
||||||
|
// Return without calling the listener
|
||||||
|
releaseLocalDatabaseReference();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
details.state = ComputerDetails.State.OFFLINE;
|
details.state = ComputerDetails.State.OFFLINE;
|
||||||
details.reachability = ComputerDetails.Reachability.OFFLINE;
|
details.reachability = ComputerDetails.Reachability.OFFLINE;
|
||||||
}
|
}
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
releaseLocalDatabaseReference();
|
||||||
|
throw e;
|
||||||
|
} finally {
|
||||||
activePolls.decrementAndGet();
|
activePolls.decrementAndGet();
|
||||||
|
}
|
||||||
|
|
||||||
// If it's online, update our persistent state
|
// If it's online, update our persistent state
|
||||||
if (details.state == ComputerDetails.State.ONLINE) {
|
if (details.state == ComputerDetails.State.ONLINE) {
|
||||||
@@ -81,7 +103,7 @@ public class ComputerManagerService extends Service {
|
|||||||
if (dbManager.getComputerByName(details.name) == null) {
|
if (dbManager.getComputerByName(details.name) == null) {
|
||||||
// It's gone
|
// It's gone
|
||||||
releaseLocalDatabaseReference();
|
releaseLocalDatabaseReference();
|
||||||
return true;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -101,13 +123,21 @@ public class ComputerManagerService extends Service {
|
|||||||
Thread t = new Thread() {
|
Thread t = new Thread() {
|
||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
|
|
||||||
|
int offlineCount = 0;
|
||||||
while (!isInterrupted() && pollingActive) {
|
while (!isInterrupted() && pollingActive) {
|
||||||
|
try {
|
||||||
// Check if this poll has modified the details
|
// Check if this poll has modified the details
|
||||||
runPoll(details);
|
if (!runPoll(details, false, offlineCount)) {
|
||||||
|
LimeLog.warning(details.name + " is offline (try " + offlineCount + ")");
|
||||||
|
offlineCount++;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
offlineCount = 0;
|
||||||
|
}
|
||||||
|
|
||||||
// Wait until the next polling interval
|
// Wait until the next polling interval
|
||||||
try {
|
Thread.sleep(SERVERINFO_POLLING_PERIOD_MS / ((offlineCount > 0) ? 2 : 1));
|
||||||
Thread.sleep(POLLING_PERIOD_MS);
|
|
||||||
} catch (InterruptedException e) {
|
} catch (InterruptedException e) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -167,10 +197,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 +206,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 +255,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,27 +271,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)) {
|
||||||
// If there's no name on one of these computers, compare with the local IP
|
// Update details anyway in case this machine has been re-added by IP
|
||||||
((details.name.isEmpty() || tuple.computer.name.isEmpty()) &&
|
// after not being reachable by our existing information
|
||||||
tuple.computer.localIp.equals(details.localIp)) ||
|
tuple.computer.localIp = details.localIp;
|
||||||
// If there is a name on both computers, compare with name
|
tuple.computer.remoteIp = details.remoteIp;
|
||||||
((!details.name.isEmpty() && !tuple.computer.name.isEmpty()) &&
|
|
||||||
tuple.computer.name.equals(details.name))) {
|
|
||||||
|
|
||||||
// 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) {
|
||||||
@@ -276,13 +306,18 @@ 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);
|
try {
|
||||||
|
runPoll(fakeDetails, true, 0);
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
// 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;
|
||||||
@@ -353,8 +388,91 @@ public class ComputerManagerService extends Service {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean pollComputer(ComputerDetails details, boolean localFirst) {
|
// Just try to establish a TCP connection to speculatively detect a running
|
||||||
|
// GFE server
|
||||||
|
private boolean fastPollIp(InetAddress addr) {
|
||||||
|
Socket s = new Socket();
|
||||||
|
try {
|
||||||
|
s.connect(new InetSocketAddress(addr, NvHTTP.HTTPS_PORT), FAST_POLL_TIMEOUT);
|
||||||
|
s.close();
|
||||||
|
return true;
|
||||||
|
} catch (IOException e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void startFastPollThread(final InetAddress addr, final boolean[] info) {
|
||||||
|
Thread t = new Thread() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
boolean pollRes = fastPollIp(addr);
|
||||||
|
|
||||||
|
synchronized (info) {
|
||||||
|
info[0] = true; // Done
|
||||||
|
info[1] = pollRes; // Polling result
|
||||||
|
|
||||||
|
info.notify();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
t.setName("Fast Poll - "+addr.getHostAddress());
|
||||||
|
t.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
private ComputerDetails.Reachability fastPollPc(final InetAddress local, final InetAddress remote) throws InterruptedException {
|
||||||
|
final boolean[] remoteInfo = new boolean[2];
|
||||||
|
final boolean[] localInfo = new boolean[2];
|
||||||
|
|
||||||
|
startFastPollThread(local, localInfo);
|
||||||
|
startFastPollThread(remote, remoteInfo);
|
||||||
|
|
||||||
|
// Check local first
|
||||||
|
synchronized (localInfo) {
|
||||||
|
while (!localInfo[0]) {
|
||||||
|
localInfo.wait(500);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (localInfo[1]) {
|
||||||
|
return ComputerDetails.Reachability.LOCAL;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now remote
|
||||||
|
synchronized (remoteInfo) {
|
||||||
|
while (!remoteInfo[0]) {
|
||||||
|
remoteInfo.wait(500);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (remoteInfo[1]) {
|
||||||
|
return ComputerDetails.Reachability.REMOTE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ComputerDetails.Reachability.OFFLINE;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean pollComputer(ComputerDetails details) throws InterruptedException {
|
||||||
ComputerDetails polledDetails;
|
ComputerDetails polledDetails;
|
||||||
|
ComputerDetails.Reachability reachability;
|
||||||
|
|
||||||
|
// If the local address is routable across the Internet,
|
||||||
|
// always consider this PC remote to be conservative
|
||||||
|
if (details.localIp.equals(details.remoteIp)) {
|
||||||
|
reachability = ComputerDetails.Reachability.REMOTE;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// Do a TCP-level connection to the HTTP server to see if it's listening
|
||||||
|
LimeLog.info("Starting fast poll for "+details.name+" ("+details.localIp+", "+details.remoteIp+")");
|
||||||
|
reachability = fastPollPc(details.localIp, details.remoteIp);
|
||||||
|
LimeLog.info("Fast poll for "+details.name+" returned "+reachability.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no connection could be established to either IP address, there's nothing we can do
|
||||||
|
if (reachability == ComputerDetails.Reachability.OFFLINE) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean localFirst = (reachability == ComputerDetails.Reachability.LOCAL);
|
||||||
|
|
||||||
if (localFirst) {
|
if (localFirst) {
|
||||||
polledDetails = tryPollIp(details, details.localIp);
|
polledDetails = tryPollIp(details, details.localIp);
|
||||||
@@ -388,24 +506,19 @@ public class ComputerManagerService extends Service {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Save the old MAC address
|
||||||
|
String savedMacAddress = details.macAddress;
|
||||||
|
|
||||||
// If we got here, it's reachable
|
// If we got here, it's reachable
|
||||||
details.update(polledDetails);
|
details.update(polledDetails);
|
||||||
return true;
|
|
||||||
|
// If the new MAC address is empty, restore the old one (workaround for GFE bug)
|
||||||
|
if (details.macAddress.equals("00:00:00:00:00:00") && savedMacAddress != null) {
|
||||||
|
LimeLog.info("MAC address was empty; using existing value: "+savedMacAddress);
|
||||||
|
details.macAddress = savedMacAddress;
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean doPollMachine(ComputerDetails details) {
|
return true;
|
||||||
if (details.reachability == ComputerDetails.Reachability.UNKNOWN ||
|
|
||||||
details.reachability == ComputerDetails.Reachability.OFFLINE) {
|
|
||||||
// Always try local first to avoid potential UDP issues when
|
|
||||||
// attempting to stream via the router's external IP address
|
|
||||||
// behind its NAT
|
|
||||||
return pollComputer(details, true);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
// If we're already reached a machine via a particular IP address,
|
|
||||||
// always try that one first
|
|
||||||
return pollComputer(details, details.reachability == ComputerDetails.Reachability.LOCAL);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -451,11 +564,122 @@ 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(APPLIST_POLLING_PERIOD_MS);
|
||||||
|
}
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return thread != null && !thread.isInterrupted();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void start() {
|
||||||
|
thread = new Thread() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
do {
|
||||||
|
InetAddress selectedAddr;
|
||||||
|
|
||||||
|
// Can't poll if it's not online
|
||||||
|
if (computer.state != ComputerDetails.State.ONLINE) {
|
||||||
|
if (listener != null) {
|
||||||
|
listener.notifyComputerUpdated(computer);
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Can't poll if there's no UUID yet
|
||||||
|
if (computer.uuid == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (computer.reachability == ComputerDetails.Reachability.LOCAL) {
|
||||||
|
selectedAddr = computer.localIp;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
selectedAddr = computer.remoteIp;
|
||||||
|
}
|
||||||
|
|
||||||
|
NvHTTP http = new NvHTTP(selectedAddr, idManager.getUniqueId(),
|
||||||
|
null, PlatformBinding.getCryptoProvider(ComputerManagerService.this));
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Query the app list from the server
|
||||||
|
String appList = http.getAppListRaw();
|
||||||
|
List<NvApp> list = NvHTTP.getAppListByReader(new StringReader(appList));
|
||||||
|
if (appList != null && !appList.isEmpty() && !list.isEmpty()) {
|
||||||
|
// Open the cache file
|
||||||
|
OutputStream cacheOut = null;
|
||||||
|
try {
|
||||||
|
cacheOut = CacheHelper.openCacheFileForOutput(getCacheDir(), "applist", computer.uuid.toString());
|
||||||
|
CacheHelper.writeStringToOutputStream(cacheOut, appList);
|
||||||
|
} catch (IOException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
} finally {
|
||||||
|
try {
|
||||||
|
if (cacheOut != null) {
|
||||||
|
cacheOut.close();
|
||||||
|
}
|
||||||
|
} catch (IOException ignored) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the computer
|
||||||
|
computer.rawAppList = appList;
|
||||||
|
|
||||||
|
// Notify that the app list has been updated
|
||||||
|
// and ensure that the thread is still active
|
||||||
|
if (listener != null && thread != null) {
|
||||||
|
listener.notifyComputerUpdated(computer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
LimeLog.warning("Empty app list received from "+computer.uuid);
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
} catch (XmlPullParserException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
} while (waitPollingDelay());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
thread.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void stop() {
|
||||||
|
if (thread != null) {
|
||||||
|
thread.interrupt();
|
||||||
|
|
||||||
|
// Don't join here because we might be blocked on network I/O
|
||||||
|
|
||||||
|
thread = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class PollingTuple {
|
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,148 +1,94 @@
|
|||||||
package com.limelight.grid;
|
package com.limelight.grid;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.app.Activity;
|
||||||
|
import android.graphics.BitmapFactory;
|
||||||
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.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.grid.assets.CachedAppAssetLoader;
|
||||||
import com.limelight.nvstream.http.LimelightCryptoProvider;
|
import com.limelight.grid.assets.DiskAssetLoader;
|
||||||
|
import com.limelight.grid.assets.MemoryAssetLoader;
|
||||||
|
import com.limelight.grid.assets.NetworkAssetLoader;
|
||||||
|
import com.limelight.nvstream.http.ComputerDetails;
|
||||||
|
|
||||||
import java.net.InetAddress;
|
|
||||||
import java.net.Socket;
|
|
||||||
import java.security.KeyManagementException;
|
import java.security.KeyManagementException;
|
||||||
import java.security.NoSuchAlgorithmException;
|
import java.security.NoSuchAlgorithmException;
|
||||||
import java.security.Principal;
|
import java.util.Collections;
|
||||||
import java.security.PrivateKey;
|
import java.util.Comparator;
|
||||||
import java.security.SecureRandom;
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.concurrent.Future;
|
|
||||||
|
|
||||||
import javax.net.ssl.HostnameVerifier;
|
|
||||||
import javax.net.ssl.KeyManager;
|
|
||||||
import javax.net.ssl.SSLContext;
|
|
||||||
import javax.net.ssl.SSLSession;
|
|
||||||
import javax.net.ssl.TrustManager;
|
|
||||||
import javax.net.ssl.X509KeyManager;
|
|
||||||
import javax.net.ssl.X509TrustManager;
|
|
||||||
import java.security.cert.X509Certificate;
|
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
public class AppGridAdapter extends GenericGridAdapter<AppView.AppObject> {
|
public class AppGridAdapter extends GenericGridAdapter<AppView.AppObject> {
|
||||||
|
private static final int ART_WIDTH_PX = 300;
|
||||||
|
private static final int SMALL_WIDTH_DP = 100;
|
||||||
|
private static final int LARGE_WIDTH_DP = 150;
|
||||||
|
|
||||||
private InetAddress address;
|
private final CachedAppAssetLoader loader;
|
||||||
private String uniqueId;
|
|
||||||
private LimelightCryptoProvider cryptoProvider;
|
|
||||||
private SSLContext sslContext;
|
|
||||||
private final HashMap<ImageView, Future> pendingRequests = new HashMap<ImageView, Future>();
|
|
||||||
|
|
||||||
public AppGridAdapter(Context context, InetAddress address, String uniqueId) throws NoSuchAlgorithmException, KeyManagementException {
|
public AppGridAdapter(Activity activity, boolean listMode, boolean small, ComputerDetails computer, String uniqueId) {
|
||||||
super(context, R.layout.app_grid_item, R.drawable.image_loading);
|
super(activity, listMode ? R.layout.simple_row : (small ? R.layout.app_grid_item_small : R.layout.app_grid_item), R.drawable.image_loading);
|
||||||
|
|
||||||
this.address = address;
|
int dpi = activity.getResources().getDisplayMetrics().densityDpi;
|
||||||
this.uniqueId = uniqueId;
|
int dp;
|
||||||
|
|
||||||
cryptoProvider = PlatformBinding.getCryptoProvider(context);
|
if (small) {
|
||||||
|
dp = SMALL_WIDTH_DP;
|
||||||
sslContext = SSLContext.getInstance("SSL");
|
}
|
||||||
sslContext.init(ourKeyman, trustAllCerts, new SecureRandom());
|
else {
|
||||||
|
dp = LARGE_WIDTH_DP;
|
||||||
}
|
}
|
||||||
|
|
||||||
TrustManager[] trustAllCerts = new TrustManager[] {
|
double scalingDivisor = ART_WIDTH_PX / (dp * (dpi / 160.0));
|
||||||
new X509TrustManager() {
|
if (scalingDivisor < 1.0) {
|
||||||
public X509Certificate[] getAcceptedIssuers() {
|
// We don't want to make them bigger before draw-time
|
||||||
return new X509Certificate[0];
|
scalingDivisor = 1.0;
|
||||||
}
|
}
|
||||||
public void checkClientTrusted(X509Certificate[] certs, String authType) {}
|
LimeLog.info("Art scaling divisor: " + scalingDivisor);
|
||||||
public void checkServerTrusted(X509Certificate[] certs, String authType) {}
|
|
||||||
}};
|
|
||||||
|
|
||||||
KeyManager[] ourKeyman = new KeyManager[] {
|
BitmapFactory.Options options = new BitmapFactory.Options();
|
||||||
new X509KeyManager() {
|
options.inSampleSize = (int) scalingDivisor;
|
||||||
public String chooseClientAlias(String[] keyTypes,
|
|
||||||
Principal[] issuers, Socket socket) {
|
this.loader = new CachedAppAssetLoader(computer, scalingDivisor,
|
||||||
return "Limelight-RSA";
|
new NetworkAssetLoader(context, uniqueId),
|
||||||
|
new MemoryAssetLoader(),
|
||||||
|
new DiskAssetLoader(context.getCacheDir()),
|
||||||
|
BitmapFactory.decodeResource(activity.getResources(),
|
||||||
|
R.drawable.image_loading, options));
|
||||||
}
|
}
|
||||||
|
|
||||||
public String chooseServerAlias(String keyType, Principal[] issuers,
|
public void cancelQueuedOperations() {
|
||||||
Socket socket) {
|
loader.cancelForegroundLoads();
|
||||||
return null;
|
loader.cancelBackgroundLoads();
|
||||||
}
|
loader.freeCacheMemory();
|
||||||
|
|
||||||
public X509Certificate[] getCertificateChain(String alias) {
|
|
||||||
return new X509Certificate[] {cryptoProvider.getClientCertificate()};
|
|
||||||
}
|
|
||||||
|
|
||||||
public String[] getClientAliases(String keyType, Principal[] issuers) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public PrivateKey getPrivateKey(String alias) {
|
|
||||||
return cryptoProvider.getClientPrivateKey();
|
|
||||||
}
|
|
||||||
|
|
||||||
public String[] getServerAliases(String keyType, Principal[] issuers) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Ignore differences between given hostname and certificate hostname
|
|
||||||
HostnameVerifier hv = new HostnameVerifier() {
|
|
||||||
public boolean verify(String hostname, SSLSession session) { return true; }
|
|
||||||
};
|
|
||||||
|
|
||||||
public void addApp(AppView.AppObject app) {
|
|
||||||
itemList.add(app);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void abortPendingRequests() {
|
|
||||||
HashMap<ImageView, Future> tempMap;
|
|
||||||
|
|
||||||
synchronized (pendingRequests) {
|
|
||||||
// Copy the pending requests under a lock
|
|
||||||
tempMap = new HashMap<ImageView, Future>(pendingRequests);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (Future f : tempMap.values()) {
|
|
||||||
f.cancel(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
synchronized (pendingRequests) {
|
|
||||||
// Remove cancelled requests
|
|
||||||
for (ImageView v : tempMap.keySet()) {
|
|
||||||
pendingRequests.remove(v);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void sortList() {
|
||||||
|
Collections.sort(itemList, new Comparator<AppView.AppObject>() {
|
||||||
@Override
|
@Override
|
||||||
public boolean populateImageView(final ImageView imgView, AppView.AppObject obj) {
|
public int compare(AppView.AppObject lhs, AppView.AppObject rhs) {
|
||||||
|
return lhs.app.getAppName().compareTo(rhs.app.getAppName());
|
||||||
// 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
|
|
||||||
public void onCompleted(Exception e, ImageView result) {
|
|
||||||
synchronized (pendingRequests) {
|
|
||||||
pendingRequests.remove(imgView);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
pendingRequests.put(imgView, f);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void addApp(AppView.AppObject app) {
|
||||||
|
// Queue a request to fetch this bitmap into cache
|
||||||
|
loader.queueCacheLoad(app.app);
|
||||||
|
|
||||||
|
// Add the app to our sorted list
|
||||||
|
itemList.add(app);
|
||||||
|
sortList();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void removeApp(AppView.AppObject app) {
|
||||||
|
itemList.remove(app);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean populateImageView(ImageView imgView, AppView.AppObject obj) {
|
||||||
|
// Let the cached asset loader handle it
|
||||||
|
loader.populateImageView(obj.app, imgView);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -0,0 +1,333 @@
|
|||||||
|
package com.limelight.grid.assets;
|
||||||
|
|
||||||
|
import android.content.res.Resources;
|
||||||
|
import android.graphics.Bitmap;
|
||||||
|
import android.graphics.drawable.BitmapDrawable;
|
||||||
|
import android.graphics.drawable.Drawable;
|
||||||
|
import android.os.AsyncTask;
|
||||||
|
import android.widget.ImageView;
|
||||||
|
|
||||||
|
import com.limelight.nvstream.http.ComputerDetails;
|
||||||
|
import com.limelight.nvstream.http.NvApp;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.lang.ref.WeakReference;
|
||||||
|
import java.util.concurrent.LinkedBlockingQueue;
|
||||||
|
import java.util.concurrent.ThreadPoolExecutor;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
public class CachedAppAssetLoader {
|
||||||
|
private static final int MAX_CONCURRENT_DISK_LOADS = 3;
|
||||||
|
private static final int MAX_CONCURRENT_NETWORK_LOADS = 3;
|
||||||
|
private static final int MAX_CONCURRENT_CACHE_LOADS = 1;
|
||||||
|
|
||||||
|
private static final int MAX_PENDING_CACHE_LOADS = 100;
|
||||||
|
private static final int MAX_PENDING_NETWORK_LOADS = 40;
|
||||||
|
private static final int MAX_PENDING_DISK_LOADS = 40;
|
||||||
|
|
||||||
|
private final ThreadPoolExecutor cacheExecutor = new ThreadPoolExecutor(
|
||||||
|
MAX_CONCURRENT_CACHE_LOADS, MAX_CONCURRENT_CACHE_LOADS,
|
||||||
|
Long.MAX_VALUE, TimeUnit.DAYS,
|
||||||
|
new LinkedBlockingQueue<Runnable>(MAX_PENDING_CACHE_LOADS),
|
||||||
|
new ThreadPoolExecutor.DiscardOldestPolicy());
|
||||||
|
|
||||||
|
private final ThreadPoolExecutor foregroundExecutor = new ThreadPoolExecutor(
|
||||||
|
MAX_CONCURRENT_DISK_LOADS, MAX_CONCURRENT_DISK_LOADS,
|
||||||
|
Long.MAX_VALUE, TimeUnit.DAYS,
|
||||||
|
new LinkedBlockingQueue<Runnable>(MAX_PENDING_DISK_LOADS),
|
||||||
|
new ThreadPoolExecutor.DiscardOldestPolicy());
|
||||||
|
|
||||||
|
private final ThreadPoolExecutor networkExecutor = new ThreadPoolExecutor(
|
||||||
|
MAX_CONCURRENT_NETWORK_LOADS, MAX_CONCURRENT_NETWORK_LOADS,
|
||||||
|
Long.MAX_VALUE, TimeUnit.DAYS,
|
||||||
|
new LinkedBlockingQueue<Runnable>(MAX_PENDING_NETWORK_LOADS),
|
||||||
|
new ThreadPoolExecutor.DiscardOldestPolicy());
|
||||||
|
|
||||||
|
private final ComputerDetails computer;
|
||||||
|
private final double scalingDivider;
|
||||||
|
private final NetworkAssetLoader networkLoader;
|
||||||
|
private final MemoryAssetLoader memoryLoader;
|
||||||
|
private final DiskAssetLoader diskLoader;
|
||||||
|
private final Bitmap placeholderBitmap;
|
||||||
|
|
||||||
|
public CachedAppAssetLoader(ComputerDetails computer, double scalingDivider,
|
||||||
|
NetworkAssetLoader networkLoader, MemoryAssetLoader memoryLoader,
|
||||||
|
DiskAssetLoader diskLoader, Bitmap placeholderBitmap) {
|
||||||
|
this.computer = computer;
|
||||||
|
this.scalingDivider = scalingDivider;
|
||||||
|
this.networkLoader = networkLoader;
|
||||||
|
this.memoryLoader = memoryLoader;
|
||||||
|
this.diskLoader = diskLoader;
|
||||||
|
this.placeholderBitmap = placeholderBitmap;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void cancelBackgroundLoads() {
|
||||||
|
Runnable r;
|
||||||
|
while ((r = cacheExecutor.getQueue().poll()) != null) {
|
||||||
|
cacheExecutor.remove(r);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void cancelForegroundLoads() {
|
||||||
|
Runnable r;
|
||||||
|
|
||||||
|
while ((r = foregroundExecutor.getQueue().poll()) != null) {
|
||||||
|
foregroundExecutor.remove(r);
|
||||||
|
}
|
||||||
|
|
||||||
|
while ((r = networkExecutor.getQueue().poll()) != null) {
|
||||||
|
networkExecutor.remove(r);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void freeCacheMemory() {
|
||||||
|
memoryLoader.clearCache();
|
||||||
|
}
|
||||||
|
|
||||||
|
private Bitmap doNetworkAssetLoad(LoaderTuple tuple, LoaderTask task) {
|
||||||
|
// Try 3 times
|
||||||
|
for (int i = 0; i < 3; i++) {
|
||||||
|
// Check again whether we've been cancelled or the image view is gone
|
||||||
|
if (task != null && (task.isCancelled() || task.imageViewRef.get() == null)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
InputStream in = networkLoader.getBitmapStream(tuple);
|
||||||
|
if (in != null) {
|
||||||
|
// Write the stream straight to disk
|
||||||
|
diskLoader.populateCacheWithStream(tuple, in);
|
||||||
|
|
||||||
|
// Close the network input stream
|
||||||
|
try {
|
||||||
|
in.close();
|
||||||
|
} catch (IOException ignored) {}
|
||||||
|
|
||||||
|
// If there's a task associated with this load, we should return the bitmap
|
||||||
|
if (task != null) {
|
||||||
|
return diskLoader.loadBitmapFromCache(tuple, (int) scalingDivider);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// Otherwise it's a background load and we return nothing
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait 1 second with a bit of fuzz
|
||||||
|
try {
|
||||||
|
Thread.sleep((int) (1000 + (Math.random() * 500)));
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private class LoaderTask extends AsyncTask<LoaderTuple, Void, Bitmap> {
|
||||||
|
private final WeakReference<ImageView> imageViewRef;
|
||||||
|
private final boolean diskOnly;
|
||||||
|
|
||||||
|
private LoaderTuple tuple;
|
||||||
|
|
||||||
|
public LoaderTask(ImageView imageView, boolean diskOnly) {
|
||||||
|
this.imageViewRef = new WeakReference<ImageView>(imageView);
|
||||||
|
this.diskOnly = diskOnly;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Bitmap doInBackground(LoaderTuple... params) {
|
||||||
|
tuple = params[0];
|
||||||
|
|
||||||
|
// Check whether it has been cancelled or the image view is gone
|
||||||
|
if (isCancelled() || imageViewRef.get() == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
Bitmap bmp = diskLoader.loadBitmapFromCache(tuple, (int) scalingDivider);
|
||||||
|
if (bmp == null) {
|
||||||
|
if (!diskOnly) {
|
||||||
|
// Try to load the asset from the network
|
||||||
|
bmp = doNetworkAssetLoad(tuple, this);
|
||||||
|
} else {
|
||||||
|
// Report progress to display the placeholder and spin
|
||||||
|
// off the network-capable task
|
||||||
|
publishProgress();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cache the bitmap
|
||||||
|
if (bmp != null) {
|
||||||
|
memoryLoader.populateCache(tuple, bmp);
|
||||||
|
}
|
||||||
|
|
||||||
|
return bmp;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onProgressUpdate(Void... nothing) {
|
||||||
|
// Do nothing if cancelled
|
||||||
|
if (isCancelled()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the current loader task for this view isn't us, do nothing
|
||||||
|
final ImageView imageView = imageViewRef.get();
|
||||||
|
if (getLoaderTask(imageView) == this) {
|
||||||
|
// Set off another loader task on the network executor
|
||||||
|
LoaderTask task = new LoaderTask(imageView, false);
|
||||||
|
AsyncDrawable asyncDrawable = new AsyncDrawable(imageView.getResources(), placeholderBitmap, task);
|
||||||
|
imageView.setAlpha(1.0f);
|
||||||
|
imageView.setImageDrawable(asyncDrawable);
|
||||||
|
task.executeOnExecutor(networkExecutor, tuple);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onPostExecute(Bitmap bitmap) {
|
||||||
|
// Do nothing if cancelled
|
||||||
|
if (isCancelled()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final ImageView imageView = imageViewRef.get();
|
||||||
|
if (getLoaderTask(imageView) == this) {
|
||||||
|
// Set the bitmap
|
||||||
|
if (bitmap != null) {
|
||||||
|
imageView.setImageBitmap(bitmap);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show the view
|
||||||
|
imageView.setAlpha(1.0f);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static class AsyncDrawable extends BitmapDrawable {
|
||||||
|
private final WeakReference<LoaderTask> loaderTaskReference;
|
||||||
|
|
||||||
|
public AsyncDrawable(Resources res, Bitmap bitmap,
|
||||||
|
LoaderTask loaderTask) {
|
||||||
|
super(res, bitmap);
|
||||||
|
loaderTaskReference = new WeakReference<LoaderTask>(loaderTask);
|
||||||
|
}
|
||||||
|
|
||||||
|
public LoaderTask getLoaderTask() {
|
||||||
|
return loaderTaskReference.get();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static LoaderTask getLoaderTask(ImageView imageView) {
|
||||||
|
if (imageView == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
final Drawable drawable = imageView.getDrawable();
|
||||||
|
|
||||||
|
// If our drawable is in play, get the loader task
|
||||||
|
if (drawable instanceof AsyncDrawable) {
|
||||||
|
final AsyncDrawable asyncDrawable = (AsyncDrawable) drawable;
|
||||||
|
return asyncDrawable.getLoaderTask();
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean cancelPendingLoad(LoaderTuple tuple, ImageView imageView) {
|
||||||
|
final LoaderTask loaderTask = getLoaderTask(imageView);
|
||||||
|
|
||||||
|
// Check if any task was pending for this image view
|
||||||
|
if (loaderTask != null && !loaderTask.isCancelled()) {
|
||||||
|
final LoaderTuple taskTuple = loaderTask.tuple;
|
||||||
|
|
||||||
|
// Cancel the task if it's not already loading the same data
|
||||||
|
if (taskTuple == null || !taskTuple.equals(tuple)) {
|
||||||
|
loaderTask.cancel(true);
|
||||||
|
} else {
|
||||||
|
// It's already loading what we want
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Allow the load to proceed
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void queueCacheLoad(NvApp app) {
|
||||||
|
final LoaderTuple tuple = new LoaderTuple(computer, app);
|
||||||
|
|
||||||
|
if (memoryLoader.loadBitmapFromCache(tuple) != null) {
|
||||||
|
// It's in memory which means it must also be on disk
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Queue a fetch in the cache executor
|
||||||
|
cacheExecutor.execute(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
// Check if the image is cached on disk
|
||||||
|
if (diskLoader.checkCacheExists(tuple)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to load the asset from the network and cache result on disk
|
||||||
|
doNetworkAssetLoad(tuple, null);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public void populateImageView(NvApp app, ImageView view) {
|
||||||
|
LoaderTuple tuple = new LoaderTuple(computer, app);
|
||||||
|
|
||||||
|
// If there's already a task in progress for this view,
|
||||||
|
// cancel it. If the task is already loading the same image,
|
||||||
|
// we return and let that load finish.
|
||||||
|
if (!cancelPendingLoad(tuple, view)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// First, try the memory cache in the current context
|
||||||
|
Bitmap bmp = memoryLoader.loadBitmapFromCache(tuple);
|
||||||
|
if (bmp != null) {
|
||||||
|
// Show the bitmap immediately
|
||||||
|
view.setAlpha(1.0f);
|
||||||
|
view.setImageBitmap(bmp);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If it's not in memory, create an async task to load it. This task will be attached
|
||||||
|
// via AsyncDrawable to this view.
|
||||||
|
final LoaderTask task = new LoaderTask(view, true);
|
||||||
|
final AsyncDrawable asyncDrawable = new AsyncDrawable(view.getResources(), placeholderBitmap, task);
|
||||||
|
view.setAlpha(0.0f);
|
||||||
|
view.setImageDrawable(asyncDrawable);
|
||||||
|
|
||||||
|
// Run the task on our foreground executor
|
||||||
|
task.executeOnExecutor(foregroundExecutor, tuple);
|
||||||
|
}
|
||||||
|
|
||||||
|
public class LoaderTuple {
|
||||||
|
public final ComputerDetails computer;
|
||||||
|
public final NvApp app;
|
||||||
|
|
||||||
|
public LoaderTuple(ComputerDetails computer, NvApp app) {
|
||||||
|
this.computer = computer;
|
||||||
|
this.app = app;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object o) {
|
||||||
|
if (!(o instanceof LoaderTuple)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
LoaderTuple other = (LoaderTuple) o;
|
||||||
|
return computer.uuid.equals(other.computer.uuid) && app.getAppId() == other.app.getAppId();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return "("+computer.uuid+", "+app.getAppId()+")";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
package com.limelight.grid.assets;
|
||||||
|
|
||||||
|
import android.graphics.Bitmap;
|
||||||
|
import android.graphics.BitmapFactory;
|
||||||
|
|
||||||
|
import com.limelight.LimeLog;
|
||||||
|
import com.limelight.utils.CacheHelper;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileNotFoundException;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
|
||||||
|
public class DiskAssetLoader {
|
||||||
|
private final File cacheDir;
|
||||||
|
|
||||||
|
public DiskAssetLoader(File cacheDir) {
|
||||||
|
this.cacheDir = cacheDir;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean checkCacheExists(CachedAppAssetLoader.LoaderTuple tuple) {
|
||||||
|
return CacheHelper.cacheFileExists(cacheDir, "boxart", tuple.computer.uuid.toString(), tuple.app.getAppId() + ".png");
|
||||||
|
}
|
||||||
|
|
||||||
|
public Bitmap loadBitmapFromCache(CachedAppAssetLoader.LoaderTuple tuple, int sampleSize) {
|
||||||
|
InputStream in = null;
|
||||||
|
Bitmap bmp = null;
|
||||||
|
try {
|
||||||
|
in = CacheHelper.openCacheFileForInput(cacheDir, "boxart", tuple.computer.uuid.toString(), tuple.app.getAppId() + ".png");
|
||||||
|
BitmapFactory.Options options = new BitmapFactory.Options();
|
||||||
|
options.inSampleSize = sampleSize;
|
||||||
|
bmp = BitmapFactory.decodeStream(in, null, options);
|
||||||
|
} catch (FileNotFoundException ignored) {
|
||||||
|
} catch (IOException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
} finally {
|
||||||
|
if (in != null) {
|
||||||
|
try {
|
||||||
|
in.close();
|
||||||
|
} catch (IOException ignored) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bmp != null) {
|
||||||
|
LimeLog.info("Disk cache hit for tuple: "+tuple);
|
||||||
|
}
|
||||||
|
|
||||||
|
return bmp;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void populateCacheWithStream(CachedAppAssetLoader.LoaderTuple tuple, InputStream input) {
|
||||||
|
OutputStream out = null;
|
||||||
|
try {
|
||||||
|
out = CacheHelper.openCacheFileForOutput(cacheDir, "boxart", tuple.computer.uuid.toString(), tuple.app.getAppId() + ".png");
|
||||||
|
CacheHelper.writeInputStreamToOutputStream(input, out);
|
||||||
|
} catch (IOException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
} finally {
|
||||||
|
if (out != null) {
|
||||||
|
try {
|
||||||
|
out.close();
|
||||||
|
} catch (IOException ignored) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
package com.limelight.grid.assets;
|
||||||
|
|
||||||
|
import android.graphics.Bitmap;
|
||||||
|
import android.util.LruCache;
|
||||||
|
|
||||||
|
import com.limelight.LimeLog;
|
||||||
|
|
||||||
|
public class MemoryAssetLoader {
|
||||||
|
private static final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
|
||||||
|
private static final LruCache<String, Bitmap> memoryCache = new LruCache<String, Bitmap>(maxMemory / 16) {
|
||||||
|
@Override
|
||||||
|
protected int sizeOf(String key, Bitmap bitmap) {
|
||||||
|
// Sizeof returns kilobytes
|
||||||
|
return bitmap.getByteCount() / 1024;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private static String constructKey(CachedAppAssetLoader.LoaderTuple tuple) {
|
||||||
|
return tuple.computer.uuid.toString()+"-"+tuple.app.getAppId();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Bitmap loadBitmapFromCache(CachedAppAssetLoader.LoaderTuple tuple) {
|
||||||
|
Bitmap bmp = memoryCache.get(constructKey(tuple));
|
||||||
|
if (bmp != null) {
|
||||||
|
LimeLog.info("Memory cache hit for tuple: "+tuple);
|
||||||
|
}
|
||||||
|
return bmp;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void populateCache(CachedAppAssetLoader.LoaderTuple tuple, Bitmap bitmap) {
|
||||||
|
memoryCache.put(constructKey(tuple), bitmap);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void clearCache() {
|
||||||
|
memoryCache.evictAll();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
package com.limelight.grid.assets;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
|
||||||
|
import com.limelight.LimeLog;
|
||||||
|
import com.limelight.binding.PlatformBinding;
|
||||||
|
import com.limelight.nvstream.http.ComputerDetails;
|
||||||
|
import com.limelight.nvstream.http.NvHTTP;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.net.InetAddress;
|
||||||
|
|
||||||
|
public class NetworkAssetLoader {
|
||||||
|
private final Context context;
|
||||||
|
private final String uniqueId;
|
||||||
|
|
||||||
|
public NetworkAssetLoader(Context context, String uniqueId) {
|
||||||
|
this.context = context;
|
||||||
|
this.uniqueId = uniqueId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public InputStream getBitmapStream(CachedAppAssetLoader.LoaderTuple tuple) {
|
||||||
|
NvHTTP http = new NvHTTP(getCurrentAddress(tuple.computer), uniqueId, null, PlatformBinding.getCryptoProvider(context));
|
||||||
|
|
||||||
|
InputStream in = null;
|
||||||
|
try {
|
||||||
|
in = http.getBoxArt(tuple.app);
|
||||||
|
} catch (IOException ignored) {}
|
||||||
|
|
||||||
|
if (in != null) {
|
||||||
|
LimeLog.info("Network asset load complete: " + tuple);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
LimeLog.info("Network asset load failed: " + tuple);
|
||||||
|
}
|
||||||
|
|
||||||
|
return in;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static InetAddress getCurrentAddress(ComputerDetails computer) {
|
||||||
|
if (computer.reachability == ComputerDetails.Reachability.LOCAL) {
|
||||||
|
return computer.localIp;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return computer.remoteIp;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
@@ -150,7 +161,13 @@ public class AddComputerManually extends Activity {
|
|||||||
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,8 @@ 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.os.Build;
|
||||||
import android.preference.PreferenceManager;
|
import android.preference.PreferenceManager;
|
||||||
|
|
||||||
public class PreferenceConfiguration {
|
public class PreferenceConfiguration {
|
||||||
@@ -13,11 +15,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 +33,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 +46,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 +68,26 @@ public class PreferenceConfiguration {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static boolean getDefaultSmallMode(Context context) {
|
||||||
|
PackageManager manager = context.getPackageManager();
|
||||||
|
if (manager != null) {
|
||||||
|
// TVs shouldn't use small mode by default
|
||||||
|
if (manager.hasSystemFeature(PackageManager.FEATURE_TELEVISION)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// API 21 uses LEANBACK instead of TELEVISION
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP_MR1) {
|
||||||
|
if (manager.hasSystemFeature(PackageManager.FEATURE_LEANBACK)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use small mode on anything smaller than a 7" tablet
|
||||||
|
return context.getResources().getConfiguration().smallestScreenWidthDp < 600;
|
||||||
|
}
|
||||||
|
|
||||||
public static int getDefaultBitrate(Context context) {
|
public static int getDefaultBitrate(Context context) {
|
||||||
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
|
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
|
||||||
|
|
||||||
@@ -135,11 +166,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,74 @@
|
|||||||
|
package com.limelight.utils;
|
||||||
|
|
||||||
|
import java.io.BufferedInputStream;
|
||||||
|
import java.io.BufferedOutputStream;
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileInputStream;
|
||||||
|
import java.io.FileNotFoundException;
|
||||||
|
import java.io.FileOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.InputStreamReader;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
import java.io.Reader;
|
||||||
|
|
||||||
|
public class CacheHelper {
|
||||||
|
private static File openPath(boolean createPath, File root, String... path) {
|
||||||
|
File f = root;
|
||||||
|
for (int i = 0; i < path.length; i++) {
|
||||||
|
String component = path[i];
|
||||||
|
|
||||||
|
if (i == path.length - 1) {
|
||||||
|
// This is the file component so now we create parent directories
|
||||||
|
if (createPath) {
|
||||||
|
f.mkdirs();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
f = new File(f, component);
|
||||||
|
}
|
||||||
|
return f;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean cacheFileExists(File root, String... path) {
|
||||||
|
return openPath(false, root, path).exists();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static InputStream openCacheFileForInput(File root, String... path) throws FileNotFoundException {
|
||||||
|
return new BufferedInputStream(new FileInputStream(openPath(false, root, path)));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static OutputStream openCacheFileForOutput(File root, String... path) throws FileNotFoundException {
|
||||||
|
return new BufferedOutputStream(new FileOutputStream(openPath(true, root, path)));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void writeInputStreamToOutputStream(InputStream in, OutputStream out) throws IOException {
|
||||||
|
byte[] buf = new byte[4096];
|
||||||
|
int bytesRead;
|
||||||
|
|
||||||
|
while ((bytesRead = in.read(buf)) != -1) {
|
||||||
|
out.write(buf, 0, bytesRead);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String readInputStreamToString(InputStream in) throws IOException {
|
||||||
|
Reader r = new InputStreamReader(in);
|
||||||
|
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
char[] buf = new char[256];
|
||||||
|
int bytesRead;
|
||||||
|
while ((bytesRead = r.read(buf)) != -1) {
|
||||||
|
sb.append(buf, 0, bytesRead);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
in.close();
|
||||||
|
} catch (IOException ignored) {}
|
||||||
|
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void writeStringToOutputStream(OutputStream out, String str) throws IOException {
|
||||||
|
out.write(str.getBytes("UTF-8"));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
|||||||
@@ -0,0 +1,90 @@
|
|||||||
|
package com.limelight.utils;
|
||||||
|
|
||||||
|
import android.app.Activity;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.widget.Toast;
|
||||||
|
|
||||||
|
import com.limelight.Game;
|
||||||
|
import com.limelight.R;
|
||||||
|
import com.limelight.binding.PlatformBinding;
|
||||||
|
import com.limelight.computers.ComputerManagerService;
|
||||||
|
import com.limelight.nvstream.http.ComputerDetails;
|
||||||
|
import com.limelight.nvstream.http.GfeHttpResponseException;
|
||||||
|
import com.limelight.nvstream.http.NvApp;
|
||||||
|
import com.limelight.nvstream.http.NvHTTP;
|
||||||
|
|
||||||
|
import java.io.FileNotFoundException;
|
||||||
|
import java.net.InetAddress;
|
||||||
|
import java.net.UnknownHostException;
|
||||||
|
|
||||||
|
public class ServerHelper {
|
||||||
|
public static InetAddress getCurrentAddressFromComputer(ComputerDetails computer) {
|
||||||
|
return computer.reachability == ComputerDetails.Reachability.LOCAL ?
|
||||||
|
computer.localIp : computer.remoteIp;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void doStart(Activity parent, NvApp app, ComputerDetails computer,
|
||||||
|
ComputerManagerService.ComputerManagerBinder managerBinder) {
|
||||||
|
Intent intent = new Intent(parent, Game.class);
|
||||||
|
intent.putExtra(Game.EXTRA_HOST,
|
||||||
|
computer.reachability == ComputerDetails.Reachability.LOCAL ?
|
||||||
|
computer.localIp.getHostAddress() : computer.remoteIp.getHostAddress());
|
||||||
|
intent.putExtra(Game.EXTRA_APP_NAME, app.getAppName());
|
||||||
|
intent.putExtra(Game.EXTRA_APP_ID, app.getAppId());
|
||||||
|
intent.putExtra(Game.EXTRA_UNIQUEID, managerBinder.getUniqueId());
|
||||||
|
intent.putExtra(Game.EXTRA_STREAMING_REMOTE,
|
||||||
|
computer.reachability != ComputerDetails.Reachability.LOCAL);
|
||||||
|
parent.startActivity(intent);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void doQuit(final Activity parent,
|
||||||
|
final InetAddress address,
|
||||||
|
final NvApp app,
|
||||||
|
final ComputerManagerService.ComputerManagerBinder managerBinder,
|
||||||
|
final Runnable onComplete) {
|
||||||
|
Toast.makeText(parent, parent.getResources().getString(R.string.applist_quit_app) + " " + app.getAppName() + "...", Toast.LENGTH_SHORT).show();
|
||||||
|
new Thread(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
NvHTTP httpConn;
|
||||||
|
String message;
|
||||||
|
try {
|
||||||
|
httpConn = new NvHTTP(address,
|
||||||
|
managerBinder.getUniqueId(), null, PlatformBinding.getCryptoProvider(parent));
|
||||||
|
if (httpConn.quitApp()) {
|
||||||
|
message = parent.getResources().getString(R.string.applist_quit_success) + " " + app.getAppName();
|
||||||
|
} else {
|
||||||
|
message = parent.getResources().getString(R.string.applist_quit_fail) + " " + app.getAppName();
|
||||||
|
}
|
||||||
|
} catch (GfeHttpResponseException e) {
|
||||||
|
if (e.getErrorCode() == 599) {
|
||||||
|
message = "This session wasn't started by this device," +
|
||||||
|
" so it cannot be quit. End streaming on the original " +
|
||||||
|
"device or the PC itself. (Error code: "+e.getErrorCode()+")";
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
message = e.getMessage();
|
||||||
|
}
|
||||||
|
} catch (UnknownHostException e) {
|
||||||
|
message = parent.getResources().getString(R.string.error_unknown_host);
|
||||||
|
} catch (FileNotFoundException e) {
|
||||||
|
message = parent.getResources().getString(R.string.error_404);
|
||||||
|
} catch (Exception e) {
|
||||||
|
message = e.getMessage();
|
||||||
|
} finally {
|
||||||
|
if (onComplete != null) {
|
||||||
|
onComplete.run();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final String toastMessage = message;
|
||||||
|
parent.runOnUiThread(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
Toast.makeText(parent, toastMessage, Toast.LENGTH_LONG).show();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}).start();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,14 +9,15 @@ import android.content.DialogInterface;
|
|||||||
import android.content.DialogInterface.OnCancelListener;
|
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;
|
||||||
|
|||||||
@@ -1,11 +1,15 @@
|
|||||||
package com.limelight.utils;
|
package com.limelight.utils;
|
||||||
|
|
||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
|
import android.app.AlertDialog;
|
||||||
import android.app.UiModeManager;
|
import android.app.UiModeManager;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
|
import android.content.DialogInterface;
|
||||||
import android.content.res.Configuration;
|
import android.content.res.Configuration;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
|
|
||||||
|
import com.limelight.R;
|
||||||
|
|
||||||
public class UiHelper {
|
public class UiHelper {
|
||||||
|
|
||||||
// Values from https://developer.android.com/training/tv/start/layouts.html
|
// Values from https://developer.android.com/training/tv/start/layouts.html
|
||||||
@@ -28,4 +32,31 @@ public class UiHelper {
|
|||||||
horizontalPaddingPixels, verticalPaddingPixels);
|
horizontalPaddingPixels, verticalPaddingPixels);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static void displayQuitConfirmationDialog(Activity parent, final Runnable onYes, final Runnable onNo) {
|
||||||
|
DialogInterface.OnClickListener dialogClickListener = new DialogInterface.OnClickListener() {
|
||||||
|
@Override
|
||||||
|
public void onClick(DialogInterface dialog, int which) {
|
||||||
|
switch (which){
|
||||||
|
case DialogInterface.BUTTON_POSITIVE:
|
||||||
|
if (onYes != null) {
|
||||||
|
onYes.run();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case DialogInterface.BUTTON_NEGATIVE:
|
||||||
|
if (onNo != null) {
|
||||||
|
onNo.run();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
AlertDialog.Builder builder = new AlertDialog.Builder(parent);
|
||||||
|
builder.setMessage(parent.getResources().getString(R.string.applist_quit_confirmation))
|
||||||
|
.setPositiveButton(parent.getResources().getString(R.string.yes), dialogClickListener)
|
||||||
|
.setNegativeButton(parent.getResources().getString(R.string.no), dialogClickListener)
|
||||||
|
.show();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Before Width: | Height: | Size: 5.4 KiB After Width: | Height: | Size: 5.2 KiB |
|
Before Width: | Height: | Size: 3.1 KiB After Width: | Height: | Size: 2.9 KiB |
|
Before Width: | Height: | Size: 7.6 KiB After Width: | Height: | Size: 7.2 KiB |
|
Before Width: | Height: | Size: 66 KiB After Width: | Height: | Size: 99 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 7.6 KiB After Width: | Height: | Size: 7.2 KiB |
|
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 65 KiB |
@@ -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"/>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
<merge xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
@@ -10,4 +10,4 @@
|
|||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:layout_gravity="center" />
|
android:layout_gravity="center" />
|
||||||
|
|
||||||
</FrameLayout>
|
</merge>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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 Moonlight</string>
|
||||||
|
<string name="title_checkbox_list_mode">Usa lista invece della griglia</string>
|
||||||
|
<string name="summary_checkbox_list_mode">Visualizza applicazioni e computers in una lista invece di una griglia</string>
|
||||||
|
<string name="title_checkbox_small_icon_mode">Usa icone piccole</string>
|
||||||
|
<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>
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -44,9 +44,9 @@
|
|||||||
<string name="conn_establishing_title">Establishing Connection</string>
|
<string name="conn_establishing_title">Establishing Connection</string>
|
||||||
<string name="conn_establishing_msg">Starting connection</string>
|
<string name="conn_establishing_msg">Starting connection</string>
|
||||||
<string name="conn_metered">Warning: Your active network connection is metered!</string>
|
<string name="conn_metered">Warning: Your active network connection is metered!</string>
|
||||||
<string name="conn_client_latency">Average client-side frame latency:</string>
|
<string name="conn_client_latency">Average frame decoding latency:</string>
|
||||||
<string name="conn_client_latency_hw">hardware decoder latency:</string>
|
<string name="conn_client_latency_hw">hardware decoder latency:</string>
|
||||||
<string name="conn_hardware_latency">Average hardware decoder latency:</string>
|
<string name="conn_hardware_latency">Average hardware decoding latency:</string>
|
||||||
<string name="conn_starting">Starting</string>
|
<string name="conn_starting">Starting</string>
|
||||||
<string name="conn_error_title">Connection Error</string>
|
<string name="conn_error_title">Connection Error</string>
|
||||||
<string name="conn_error_msg">Failed to start</string>
|
<string name="conn_error_msg">Failed to start</string>
|
||||||
@@ -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 Moonlight</string>
|
||||||
|
<string name="title_checkbox_list_mode">Use lists instead of grids</string>
|
||||||
|
<string name="summary_checkbox_list_mode">Display apps and PCs in lists instead of grids</string>
|
||||||
|
<string name="title_checkbox_small_icon_mode">Use small icons</string>
|
||||||
|
<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>
|
||||||
|
|||||||
@@ -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,7 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
<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="Moonlight" />
|
||||||
</manifest>
|
</manifest>
|
||||||
@@ -1,10 +1,9 @@
|
|||||||
<?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" />
|
||||||
|
|
||||||
<!-- Root application name -->
|
<!-- Root application name -->
|
||||||
<application android:label="Limelight (Root)" />
|
<application android:label="Moonlight (Root)" />
|
||||||
</manifest>
|
</manifest>
|
||||||
@@ -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.2.3'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -1,9 +1,10 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<module external.linked.project.path="$MODULE_DIR$" external.root.project.path="$MODULE_DIR$" external.system.id="GRADLE" external.system.module.group="" external.system.module.version="unspecified" type="JAVA_MODULE" version="4">
|
<module external.linked.project.id="moonlight-android" external.linked.project.path="$MODULE_DIR$" external.root.project.path="$MODULE_DIR$" external.system.id="GRADLE" external.system.module.group="" external.system.module.version="unspecified" type="JAVA_MODULE" version="4">
|
||||||
<component name="FacetManager">
|
<component name="FacetManager">
|
||||||
<facet type="java-gradle" name="Java-Gradle">
|
<facet type="java-gradle" name="Java-Gradle">
|
||||||
<configuration>
|
<configuration>
|
||||||
<option name="BUILD_FOLDER_PATH" value="$MODULE_DIR$/build" />
|
<option name="BUILD_FOLDER_PATH" value="$MODULE_DIR$/build" />
|
||||||
|
<option name="BUILDABLE" value="false" />
|
||||||
</configuration>
|
</configuration>
|
||||||
</facet>
|
</facet>
|
||||||
</component>
|
</component>
|
||||||
@@ -16,4 +17,3 @@
|
|||||||
<orderEntry type="sourceFolder" forTests="false" />
|
<orderEntry type="sourceFolder" forTests="false" />
|
||||||
</component>
|
</component>
|
||||||
</module>
|
</module>
|
||||||
|
|
||||||