Compare commits
128 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3d177e97e4 | |||
| 6c3aaedc83 | |||
| bf84ebef6d | |||
| 8991b29329 | |||
| fa84575be5 | |||
| 0432d5725b | |||
| 8e7b144339 | |||
| fc629db653 | |||
| d5863e1bef | |||
| c2c3a6b37c | |||
| e701699dea | |||
| 17179bd027 | |||
| b2f210700d | |||
| f0e85c4c53 | |||
| 92f8425ace | |||
| 6ad001e8be | |||
| b6e4d5528b | |||
| 0f0b83badc | |||
| 453fbb5f58 | |||
| e7dc3a4c11 | |||
| d68b2382cf | |||
| 1b5330323c | |||
| 8aba4888e1 | |||
| 1c3b9a3859 | |||
| e8f04f5a3b | |||
| 56b814e877 | |||
| 628ccd39d6 | |||
| 59db3f9b62 | |||
| 416f922b56 | |||
| b52a86e6cc | |||
| e523b5069e | |||
| e8ae8d9807 | |||
| 64e56a861d | |||
| c1bcd09c9b | |||
| 574258804f | |||
| 21ea3d8a2b | |||
| 6de4288a85 | |||
| a107b5e652 | |||
| b02db2c182 | |||
| f8a04cda7a | |||
| 226e8edefc | |||
| 9b90b30a1f | |||
| 2ed245b25a | |||
| 4b769839d0 | |||
| 239dd1d5a1 | |||
| 37509cce9b | |||
| 227c71549b | |||
| a10d8334f3 | |||
| 0fc61e52dd | |||
| 5e44c33bb6 | |||
| df3655e958 | |||
| fe43e13145 | |||
| acd3aad8d9 | |||
| 811b4b4f22 | |||
| 7db3b9f401 | |||
| a5a099cf43 | |||
| ba605643bb | |||
| 96e98c1abb | |||
| 5de6f6ae2b | |||
| 0685722773 | |||
| 29df3b2859 | |||
| fc6f859ced | |||
| 6b21a5416f | |||
| 74e7c8bbf1 | |||
| 757075b16a | |||
| e8903c4d48 | |||
| 98262d16ee | |||
| 339506cf10 | |||
| 63bd5df09b | |||
| 32af2d0831 | |||
| 242b03d4b5 | |||
| 87a62666ac | |||
| 2dcf5486da | |||
| 60d3d8b3ae | |||
| e9141d65fe | |||
| aae591daec | |||
| a5ca8a7472 | |||
| 36f8cc02cb | |||
| 55b9645651 | |||
| d30ecbed5b | |||
| 0bbd27f04c | |||
| 3c53fb7403 | |||
| 7a81950819 | |||
| 74f212c702 | |||
| 36be943854 | |||
| 26a4fc75a5 | |||
| a5ec5fc265 | |||
| 541ac44be4 | |||
| 117b555fcd | |||
| a10cd04441 | |||
| 53dccbde2a | |||
| 56625dfe4b | |||
| 2eab5a3b7b | |||
| f9e811862a | |||
| 25ccc3d0e1 | |||
| 8853bf0670 | |||
| 71fa3a824b | |||
| 56fd50834c | |||
| 48ba812cf6 | |||
| 019dc6d45f | |||
| cbcb784a79 | |||
| 39fa0258ad | |||
| d0dd5bfa8c | |||
| b948c47618 | |||
| 18cae8ac53 | |||
| 0576231dfc | |||
| 6ad35a83dd | |||
| 33d4dfc745 | |||
| f3bf63a668 | |||
| 2dbb7395a4 | |||
| 7c1eb80d62 | |||
| f2bf093691 | |||
| 2f002bfa4a | |||
| 4a19038d54 | |||
| 15fb3dd92c | |||
| e0982d3961 | |||
| 7fb2f15f54 | |||
| f93dbb4116 | |||
| bc34fe3a9f | |||
| bbe49491c1 | |||
| d5ccb80f26 | |||
| 50fd15379a | |||
| ed479f1155 | |||
| 04db9ba714 | |||
| 6a973e3248 | |||
| 96d9e4977b | |||
| 5a3897f22a | |||
| ceef00b79a |
@@ -32,4 +32,5 @@ Thumbs.db
|
||||
build/
|
||||
|
||||
# Compiled JNI libraries folder
|
||||
**/jniLibs
|
||||
**/jniLibs
|
||||
app/.externalNativeBuild/
|
||||
|
||||
@@ -8,7 +8,7 @@ whether in your own home or over the internet.
|
||||
|
||||
[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.
|
||||
Check our [wiki](https://github.com/moonlight-stream/moonlight-docs/wiki) for more detailed information or a troubleshooting guide.
|
||||
|
||||
##Features
|
||||
|
||||
@@ -46,6 +46,12 @@ This project is being actively developed at [XDA Developers](http://forum.xda-de
|
||||
2. Write code
|
||||
3. Send Pull Requests
|
||||
|
||||
##Building
|
||||
* Install Android Studio and the Android NDK
|
||||
* Run ‘git submodule update --init --recursive’ from within moonlight-android/
|
||||
* In moonlight-android/, create a file called ‘local.properties’. Add an ‘ndk.dir=’ property to the local.properties file and set it equal to your NDK directory.
|
||||
* Build the APK using Android Studio
|
||||
|
||||
##Authors
|
||||
|
||||
* [Cameron Gutman](https://github.com/cgutman)
|
||||
|
||||
@@ -12,10 +12,7 @@
|
||||
<option name="SELECTED_TEST_ARTIFACT" value="_android_test_" />
|
||||
<option name="ASSEMBLE_TASK_NAME" value="assembleNonRootDebug" />
|
||||
<option name="COMPILE_JAVA_TASK_NAME" value="compileNonRootDebugSources" />
|
||||
<option name="ASSEMBLE_TEST_TASK_NAME" value="assembleNonRootDebugAndroidTest" />
|
||||
<option name="COMPILE_JAVA_TEST_TASK_NAME" value="compileNonRootDebugAndroidTestSources" />
|
||||
<afterSyncTasks>
|
||||
<task>generateNonRootDebugAndroidTestSources</task>
|
||||
<task>generateNonRootDebugSources</task>
|
||||
</afterSyncTasks>
|
||||
<option name="ALLOW_USER_CONFIGURATION" value="false" />
|
||||
@@ -25,16 +22,26 @@
|
||||
<option name="ASSETS_FOLDER_RELATIVE_PATH" value="/src/main/assets" />
|
||||
</configuration>
|
||||
</facet>
|
||||
<facet type="native-android-gradle" name="Native-Android-Gradle">
|
||||
<configuration>
|
||||
<option name="SELECTED_BUILD_VARIANT" value="nonRootDebug" />
|
||||
</configuration>
|
||||
</facet>
|
||||
</component>
|
||||
<component name="NewModuleRootManager" LANGUAGE_LEVEL="JDK_1_7" inherit-compiler-output="false">
|
||||
<output url="file://$MODULE_DIR$/build/intermediates/classes/nonRoot/debug" />
|
||||
<output-test url="file://$MODULE_DIR$/build/intermediates/classes/androidTest/nonRoot/debug" />
|
||||
<output-test url="file://$MODULE_DIR$/build/intermediates/classes/test/nonRoot/debug" />
|
||||
<exclude-output />
|
||||
<content url="file://$MODULE_DIR$">
|
||||
<sourceFolder url="file://$MODULE_DIR$/src/main/jni/jnienet/enet" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/src/main/jni/jnienet" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/src/main/jni/evdev_reader" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/src/main/jni/nv_opus_dec" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/r/nonRoot/debug" isTestSource="false" generated="true" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/aidl/nonRoot/debug" isTestSource="false" generated="true" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/buildConfig/nonRoot/debug" isTestSource="false" generated="true" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/rs/nonRoot/debug" isTestSource="false" generated="true" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/apt/nonRoot/debug" isTestSource="false" generated="true" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/build/generated/res/rs/nonRoot/debug" type="java-resource" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/build/generated/res/resValues/nonRoot/debug" type="java-resource" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/src/nonRootDebug/res" type="java-resource" />
|
||||
@@ -42,71 +49,97 @@
|
||||
<sourceFolder url="file://$MODULE_DIR$/src/nonRootDebug/assets" type="java-resource" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/src/nonRootDebug/aidl" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/src/nonRootDebug/java" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/src/nonRootDebug/jni" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/src/nonRootDebug/rs" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/src/nonRootDebug/shaders" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/r/androidTest/nonRoot/debug" isTestSource="true" generated="true" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/aidl/androidTest/nonRoot/debug" isTestSource="true" generated="true" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/buildConfig/androidTest/nonRoot/debug" isTestSource="true" generated="true" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/rs/androidTest/nonRoot/debug" isTestSource="true" generated="true" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/apt/androidTest/nonRoot/debug" isTestSource="true" generated="true" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/build/generated/res/rs/androidTest/nonRoot/debug" type="java-test-resource" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/build/generated/res/resValues/androidTest/nonRoot/debug" type="java-test-resource" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/src/testNonRootDebug/res" type="java-test-resource" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/src/testNonRootDebug/resources" type="java-test-resource" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/src/testNonRootDebug/assets" type="java-test-resource" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/src/testNonRootDebug/aidl" isTestSource="true" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/src/testNonRootDebug/java" isTestSource="true" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/src/testNonRootDebug/rs" isTestSource="true" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/src/testNonRootDebug/shaders" isTestSource="true" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/src/nonRoot/res" type="java-resource" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/src/nonRoot/resources" type="java-resource" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/src/nonRoot/assets" type="java-resource" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/src/nonRoot/aidl" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/src/nonRoot/java" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/src/nonRoot/jni" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/src/nonRoot/rs" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/src/nonRoot/shaders" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/src/testNonRoot/res" type="java-test-resource" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/src/testNonRoot/resources" type="java-test-resource" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/src/testNonRoot/assets" type="java-test-resource" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/src/testNonRoot/aidl" isTestSource="true" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/src/testNonRoot/java" isTestSource="true" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/src/testNonRoot/rs" isTestSource="true" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/src/testNonRoot/shaders" isTestSource="true" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/src/androidTestNonRoot/res" type="java-test-resource" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/src/androidTestNonRoot/resources" type="java-test-resource" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/src/androidTestNonRoot/assets" type="java-test-resource" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/src/androidTestNonRoot/aidl" isTestSource="true" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/src/androidTestNonRoot/java" isTestSource="true" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/src/androidTestNonRoot/jni" isTestSource="true" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/src/androidTestNonRoot/rs" isTestSource="true" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/src/androidTestNonRoot/shaders" isTestSource="true" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/src/debug/res" type="java-resource" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/src/debug/resources" type="java-resource" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/src/debug/assets" type="java-resource" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/src/debug/aidl" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/src/debug/java" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/src/debug/jni" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/src/debug/rs" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/src/debug/shaders" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/src/testDebug/res" type="java-test-resource" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/src/testDebug/resources" type="java-test-resource" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/src/testDebug/assets" type="java-test-resource" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/src/testDebug/aidl" isTestSource="true" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/src/testDebug/java" isTestSource="true" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/src/testDebug/rs" isTestSource="true" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/src/testDebug/shaders" isTestSource="true" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/src/main/res" type="java-resource" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/src/main/resources" type="java-resource" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/src/main/assets" type="java-resource" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/src/main/aidl" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/src/main/java" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/src/main/rs" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/src/main/shaders" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/src/androidTest/res" type="java-test-resource" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/src/androidTest/resources" type="java-test-resource" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/src/androidTest/assets" type="java-test-resource" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/src/androidTest/aidl" isTestSource="true" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/src/androidTest/java" isTestSource="true" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/src/androidTest/jni" isTestSource="true" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/src/androidTest/rs" isTestSource="true" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/src/androidTest/shaders" isTestSource="true" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/src/test/res" type="java-test-resource" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/src/test/resources" type="java-test-resource" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/src/test/assets" type="java-test-resource" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/src/test/aidl" isTestSource="true" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/src/test/java" isTestSource="true" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/src/test/rs" isTestSource="true" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/src/test/shaders" isTestSource="true" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/assets" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/bundles" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/blame" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/classes" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/coverage-instrumented-classes" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/dependency-cache" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/dex" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/dex-cache" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/incremental" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/jacoco" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/javaResources" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/libs" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/lint" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/incremental-safeguard" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/jniLibs" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/manifests" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/ndk" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/ndkBuild" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/pre-dexed" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/proguard" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/res" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/rs" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/shaders" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/symbols" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/transforms" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/build/outputs" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/build/tmp" />
|
||||
</content>
|
||||
<orderEntry type="jdk" jdkName="Android API 23 Platform" jdkType="Android SDK" />
|
||||
<orderEntry type="jdk" jdkName="Android API 25 Platform" jdkType="Android SDK" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
<orderEntry type="library" exported="" name="bcprov-jdk15on-1.52" level="project" />
|
||||
<orderEntry type="library" exported="" name="bcpkix-jdk15on-1.52" level="project" />
|
||||
|
||||
@@ -4,24 +4,30 @@ import org.apache.tools.ant.taskdefs.condition.Os
|
||||
apply plugin: 'com.android.application'
|
||||
|
||||
android {
|
||||
compileSdkVersion 23
|
||||
buildToolsVersion "23.0.2"
|
||||
compileSdkVersion 25
|
||||
buildToolsVersion "25.0.0"
|
||||
|
||||
defaultConfig {
|
||||
minSdkVersion 16
|
||||
targetSdkVersion 23
|
||||
targetSdkVersion 25
|
||||
|
||||
versionName "4.5.4"
|
||||
versionCode = 91
|
||||
versionName "4.8.1"
|
||||
versionCode = 113
|
||||
}
|
||||
|
||||
productFlavors {
|
||||
root {
|
||||
applicationId "com.limelight.root"
|
||||
ndk {
|
||||
abiFilters "armeabi-v7a", "arm64-v8a", "x86", "x86_64", "mips", "mips64"
|
||||
}
|
||||
}
|
||||
|
||||
nonRoot {
|
||||
applicationId "com.limelight"
|
||||
ndk {
|
||||
abiFilters "armeabi-v7a", "arm64-v8a", "x86", "x86_64", "mips", "mips64"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,32 +49,10 @@ android {
|
||||
exclude 'META-INF/BCKEY.DSA'
|
||||
}
|
||||
|
||||
sourceSets.main.jni.srcDirs = []
|
||||
|
||||
//noinspection GroovyAssignabilityCheck,GroovyAssignabilityCheck
|
||||
task ndkBuild(type: Exec, description: 'Compile JNI source via NDK') {
|
||||
Properties properties = new Properties()
|
||||
properties.load(project.rootProject.file('local.properties').newDataInputStream())
|
||||
def ndkDir = properties.getProperty('ndk.dir')
|
||||
|
||||
if (Os.isFamily(Os.FAMILY_WINDOWS)) {
|
||||
commandLine "$ndkDir\\ndk-build.cmd",
|
||||
'NDK_PROJECT_PATH=build/intermediates/ndk',
|
||||
'NDK_LIBS_OUT=src/main/jniLibs',
|
||||
'APP_BUILD_SCRIPT=src/main/jni/Android.mk',
|
||||
'NDK_APPLICATION_MK=src/main/jni/Application.mk'
|
||||
externalNativeBuild {
|
||||
ndkBuild {
|
||||
path "src/main/jni/Android.mk"
|
||||
}
|
||||
else {
|
||||
commandLine "$ndkDir/ndk-build",
|
||||
'NDK_PROJECT_PATH=build/intermediates/ndk',
|
||||
'NDK_LIBS_OUT=src/main/jniLibs',
|
||||
'APP_BUILD_SCRIPT=src/main/jni/Android.mk',
|
||||
'NDK_APPLICATION_MK=src/main/jni/Application.mk'
|
||||
}
|
||||
}
|
||||
|
||||
tasks.withType(JavaCompile) {
|
||||
compileTask -> compileTask.dependsOn ndkBuild
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,60 +7,85 @@
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||
<uses-permission android:name="android.permission.CHANGE_WIFI_MULTICAST_STATE" />
|
||||
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
|
||||
|
||||
<uses-feature android:name="android.hardware.touchscreen" android:required="false" />
|
||||
<uses-feature android:name="android.hardware.wifi" android:required="false" />
|
||||
<uses-feature android:name="android.hardware.gamepad" android:required="false" />
|
||||
<uses-feature android:name="android.hardware.usb.host" android:required="false" />
|
||||
<uses-feature android:name="android.software.leanback" android:required="false" />
|
||||
|
||||
<uses-feature
|
||||
android:name="android.hardware.touchscreen"
|
||||
android:required="false" />
|
||||
<uses-feature
|
||||
android:name="android.hardware.wifi"
|
||||
android:required="false" />
|
||||
<uses-feature
|
||||
android:name="android.hardware.gamepad"
|
||||
android:required="false" />
|
||||
<uses-feature
|
||||
android:name="android.hardware.usb.host"
|
||||
android:required="false" />
|
||||
<uses-feature
|
||||
android:name="android.software.leanback"
|
||||
android:required="false" />
|
||||
|
||||
<application
|
||||
android:allowBackup="true"
|
||||
android:icon="@drawable/ic_launcher"
|
||||
android:banner="@drawable/atv_banner"
|
||||
android:theme="@style/AppTheme" >
|
||||
android:icon="@drawable/ic_launcher"
|
||||
android:theme="@style/AppTheme">
|
||||
|
||||
<!-- Samsung multi-window support -->
|
||||
<uses-library android:name="com.sec.android.app.multiwindow" android:required="false" />
|
||||
<meta-data android:name="com.sec.android.support.multiwindow" android:value="true" />
|
||||
|
||||
<uses-library
|
||||
android:name="com.sec.android.app.multiwindow"
|
||||
android:required="false" />
|
||||
|
||||
<meta-data
|
||||
android:name="com.sec.android.support.multiwindow"
|
||||
android:value="true" />
|
||||
|
||||
<activity
|
||||
android:name=".PcView"
|
||||
android:configChanges="mcc|mnc|locale|touchscreen|keyboard|keyboardHidden|navigation|screenLayout|fontScale|uiMode|orientation|screenSize|smallestScreenSize|layoutDirection">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
<category android:name="android.intent.category.MULTIWINDOW_LAUNCHER" />
|
||||
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
|
||||
<category android:name="tv.ouya.intent.category.APP" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<!-- Small hack to support launcher shortcuts without relaunching over and over again when the back button is pressed -->
|
||||
<activity
|
||||
android:name=".AppViewShortcutTrampoline"
|
||||
android:noHistory="true"
|
||||
android:configChanges="mcc|mnc|locale|touchscreen|keyboard|keyboardHidden|navigation|screenLayout|fontScale|uiMode|orientation|screenSize|smallestScreenSize|layoutDirection">
|
||||
<meta-data
|
||||
android:name="android.support.PARENT_ACTIVITY"
|
||||
android:value="com.limelight.PcView" />
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".AppView"
|
||||
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
|
||||
android:name="android.support.PARENT_ACTIVITY"
|
||||
android:value="com.limelight.PcView" />
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".preferences.StreamSettings"
|
||||
android:label="Streaming Settings" >
|
||||
android:label="Streaming Settings">
|
||||
<meta-data
|
||||
android:name="android.support.PARENT_ACTIVITY"
|
||||
android:value="com.limelight.PcView" />
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".preferences.AddComputerManually"
|
||||
android:label="Add Computer Manually" >
|
||||
android:label="Add Computer Manually">
|
||||
<meta-data
|
||||
android:name="android.support.PARENT_ACTIVITY"
|
||||
android:value="com.limelight.PcView" />
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".Game"
|
||||
android:configChanges="mcc|mnc|locale|touchscreen|keyboard|keyboardHidden|navigation|screenLayout|fontScale|uiMode|orientation|screenSize|smallestScreenSize|layoutDirection"
|
||||
android:screenOrientation="sensorLandscape"
|
||||
android:theme="@style/StreamTheme"
|
||||
android:configChanges="mcc|mnc|locale|touchscreen|keyboard|keyboardHidden|navigation|screenLayout|fontScale|uiMode|orientation|screenSize|smallestScreenSize|layoutDirection" >
|
||||
android:theme="@style/StreamTheme">
|
||||
<meta-data
|
||||
android:name="android.support.PARENT_ACTIVITY"
|
||||
android:value="com.limelight.AppView" />
|
||||
@@ -75,6 +100,8 @@
|
||||
<service
|
||||
android:name=".binding.input.driver.UsbDriverService"
|
||||
android:label="Usb Driver Service" />
|
||||
|
||||
<activity android:name=".HelpActivity"></activity>
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
</manifest>
|
||||
@@ -11,12 +11,14 @@ import com.limelight.grid.AppGridAdapter;
|
||||
import com.limelight.nvstream.http.ComputerDetails;
|
||||
import com.limelight.nvstream.http.NvApp;
|
||||
import com.limelight.nvstream.http.NvHTTP;
|
||||
import com.limelight.nvstream.http.PairingManager;
|
||||
import com.limelight.preferences.PreferenceConfiguration;
|
||||
import com.limelight.ui.AdapterFragment;
|
||||
import com.limelight.ui.AdapterFragmentCallbacks;
|
||||
import com.limelight.utils.CacheHelper;
|
||||
import com.limelight.utils.Dialog;
|
||||
import com.limelight.utils.ServerHelper;
|
||||
import com.limelight.utils.ShortcutHelper;
|
||||
import com.limelight.utils.SpinnerDialog;
|
||||
import com.limelight.utils.UiHelper;
|
||||
|
||||
@@ -43,6 +45,7 @@ import android.widget.AdapterView.AdapterContextMenuInfo;
|
||||
public class AppView extends Activity implements AdapterFragmentCallbacks {
|
||||
private AppGridAdapter appGridAdapter;
|
||||
private String uuidString;
|
||||
private ShortcutHelper shortcutHelper;
|
||||
|
||||
private ComputerDetails computer;
|
||||
private ComputerManagerService.ApplistPoller poller;
|
||||
@@ -90,15 +93,34 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
|
||||
return;
|
||||
}
|
||||
|
||||
// Load the app grid with cached data (if possible).
|
||||
// This must be done _before_ startComputerUpdates()
|
||||
// so the initial serverinfo response can update the running
|
||||
// icon.
|
||||
populateAppGridWithCache();
|
||||
|
||||
// Start updates
|
||||
startComputerUpdates();
|
||||
|
||||
// Load the app grid with cached data (if possible)
|
||||
populateAppGridWithCache();
|
||||
runOnUiThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (isFinishing() || isChangingConfigurations()) {
|
||||
return;
|
||||
}
|
||||
|
||||
getFragmentManager().beginTransaction()
|
||||
.replace(R.id.appFragmentContainer, new AdapterFragment())
|
||||
.commitAllowingStateLoss();
|
||||
// Despite my best efforts to catch all conditions that could
|
||||
// cause the activity to be destroyed when we try to commit
|
||||
// I haven't been able to, so we have this try-catch block.
|
||||
try {
|
||||
getFragmentManager().beginTransaction()
|
||||
.replace(R.id.appFragmentContainer, new AdapterFragment())
|
||||
.commitAllowingStateLoss();
|
||||
} catch (IllegalStateException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}.start();
|
||||
}
|
||||
@@ -116,7 +138,7 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
|
||||
|
||||
managerBinder.startPolling(new ComputerManagerListener() {
|
||||
@Override
|
||||
public void notifyComputerUpdated(ComputerDetails details) {
|
||||
public void notifyComputerUpdated(final ComputerDetails details) {
|
||||
// Do nothing if updates are suspended
|
||||
if (suspendGridUpdates) {
|
||||
return;
|
||||
@@ -141,6 +163,24 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
|
||||
return;
|
||||
}
|
||||
|
||||
// Close immediately if the PC is no longer paired
|
||||
if (details.state == ComputerDetails.State.ONLINE && details.pairState != PairingManager.PairState.PAIRED) {
|
||||
AppView.this.runOnUiThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
// Disable shortcuts referencing this PC for now
|
||||
shortcutHelper.disableShortcut(details.uuid.toString(),
|
||||
getResources().getString(R.string.scut_not_paired));
|
||||
|
||||
// Display a toast to the user and quit the activity
|
||||
Toast.makeText(AppView.this, getResources().getText(R.string.scut_not_paired), Toast.LENGTH_SHORT).show();
|
||||
finish();
|
||||
}
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// App list is the same or empty
|
||||
if (details.rawAppList == null || details.rawAppList.equals(lastRawApplist)) {
|
||||
|
||||
@@ -159,6 +199,7 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
|
||||
|
||||
try {
|
||||
updateUiWithAppList(NvHTTP.getAppListByReader(new StringReader(details.rawAppList)));
|
||||
updateUiWithServerinfo(details);
|
||||
|
||||
if (blockingLoadSpinner != null) {
|
||||
blockingLoadSpinner.dismiss();
|
||||
@@ -192,6 +233,8 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
shortcutHelper = new ShortcutHelper(this);
|
||||
|
||||
String locale = PreferenceConfiguration.readPreferences(this).language;
|
||||
if (!locale.equals(PreferenceConfiguration.DEFAULT_LANGUAGE)) {
|
||||
Configuration config = new Configuration(getResources().getConfiguration());
|
||||
@@ -205,11 +248,17 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
|
||||
|
||||
uuidString = getIntent().getStringExtra(UUID_EXTRA);
|
||||
|
||||
String labelText = getResources().getString(R.string.title_applist)+" "+getIntent().getStringExtra(NAME_EXTRA);
|
||||
String computerName = getIntent().getStringExtra(NAME_EXTRA);
|
||||
|
||||
String labelText = getResources().getString(R.string.title_applist)+" "+computerName;
|
||||
TextView label = (TextView) findViewById(R.id.appListText);
|
||||
setTitle(labelText);
|
||||
label.setText(labelText);
|
||||
|
||||
// Add a launcher shortcut for this PC (forced, since this is user interaction)
|
||||
shortcutHelper.createAppViewShortcut(uuidString, computerName, uuidString, true);
|
||||
shortcutHelper.reportShortcutUsed(uuidString);
|
||||
|
||||
// Bind to the computer manager service
|
||||
bindService(new Intent(this, ComputerManagerService.class), serviceConnection,
|
||||
Service.BIND_AUTO_CREATE);
|
||||
@@ -266,27 +315,14 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
|
||||
stopComputerUpdates();
|
||||
}
|
||||
|
||||
private int getRunningAppId() {
|
||||
int runningAppId = -1;
|
||||
for (int i = 0; i < appGridAdapter.getCount(); i++) {
|
||||
AppObject app = (AppObject) appGridAdapter.getItem(i);
|
||||
if (app.app.getIsRunning()) {
|
||||
runningAppId = app.app.getAppId();
|
||||
break;
|
||||
}
|
||||
}
|
||||
return runningAppId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) {
|
||||
super.onCreateContextMenu(menu, v, menuInfo);
|
||||
|
||||
AdapterContextMenuInfo info = (AdapterContextMenuInfo) menuInfo;
|
||||
AppObject selectedApp = (AppObject) appGridAdapter.getItem(info.position);
|
||||
int runningAppId = getRunningAppId();
|
||||
if (runningAppId != -1) {
|
||||
if (runningAppId == selectedApp.app.getAppId()) {
|
||||
if (lastRunningAppId != 0) {
|
||||
if (lastRunningAppId == selectedApp.app.getAppId()) {
|
||||
menu.add(Menu.NONE, START_OR_RESUME_ID, 1, getResources().getString(R.string.applist_menu_resume));
|
||||
menu.add(Menu.NONE, QUIT_ID, 2, getResources().getString(R.string.applist_menu_quit));
|
||||
}
|
||||
@@ -362,19 +398,19 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
|
||||
AppObject existingApp = (AppObject) appGridAdapter.getItem(i);
|
||||
|
||||
// There can only be one or zero apps running.
|
||||
if (existingApp.app.getIsRunning() &&
|
||||
if (existingApp.isRunning &&
|
||||
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);
|
||||
existingApp.isRunning = true;
|
||||
updated = true;
|
||||
}
|
||||
else if (existingApp.app.getIsRunning()) {
|
||||
else if (existingApp.isRunning) {
|
||||
// This app was running but now isn't
|
||||
existingApp.app.setIsRunning(false);
|
||||
existingApp.isRunning = false;
|
||||
updated = true;
|
||||
}
|
||||
else {
|
||||
@@ -404,10 +440,6 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
|
||||
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;
|
||||
@@ -477,7 +509,7 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
|
||||
AppObject app = (AppObject) appGridAdapter.getItem(pos);
|
||||
|
||||
// Only open the context menu if something is running, otherwise start it
|
||||
if (getRunningAppId() != -1) {
|
||||
if (lastRunningAppId != 0) {
|
||||
openContextMenu(arg1);
|
||||
} else {
|
||||
ServerHelper.doStart(AppView.this, app.app, computer, managerBinder);
|
||||
@@ -490,6 +522,7 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
|
||||
|
||||
public class AppObject {
|
||||
public final NvApp app;
|
||||
public boolean isRunning;
|
||||
|
||||
public AppObject(NvApp app) {
|
||||
if (app == null) {
|
||||
|
||||
@@ -0,0 +1,164 @@
|
||||
package com.limelight;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.Service;
|
||||
import android.content.ComponentName;
|
||||
import android.content.Intent;
|
||||
import android.content.ServiceConnection;
|
||||
import android.os.Bundle;
|
||||
import android.os.IBinder;
|
||||
|
||||
import com.limelight.computers.ComputerManagerListener;
|
||||
import com.limelight.computers.ComputerManagerService;
|
||||
import com.limelight.nvstream.http.ComputerDetails;
|
||||
import com.limelight.nvstream.http.NvApp;
|
||||
import com.limelight.utils.Dialog;
|
||||
import com.limelight.utils.ServerHelper;
|
||||
import com.limelight.utils.SpinnerDialog;
|
||||
import com.limelight.utils.UiHelper;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.UUID;
|
||||
|
||||
public class AppViewShortcutTrampoline extends Activity {
|
||||
private String uuidString;
|
||||
|
||||
private ComputerDetails computer;
|
||||
private SpinnerDialog blockingLoadSpinner;
|
||||
|
||||
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));
|
||||
|
||||
// Force CMS to repoll this machine
|
||||
managerBinder.invalidateStateForComputer(computer.uuid);
|
||||
|
||||
// Start polling
|
||||
managerBinder.startPolling(new ComputerManagerListener() {
|
||||
@Override
|
||||
public void notifyComputerUpdated(final ComputerDetails details) {
|
||||
// Don't care about other computers
|
||||
if (!details.uuid.toString().equalsIgnoreCase(uuidString)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (details.state != ComputerDetails.State.UNKNOWN) {
|
||||
runOnUiThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
// Stop showing the spinner
|
||||
if (blockingLoadSpinner != null) {
|
||||
blockingLoadSpinner.dismiss();
|
||||
blockingLoadSpinner = null;
|
||||
}
|
||||
|
||||
if (details.state == ComputerDetails.State.ONLINE) {
|
||||
// Close this activity
|
||||
finish();
|
||||
|
||||
// Create a new activity stack for this launch
|
||||
ArrayList<Intent> intentStack = new ArrayList<>();
|
||||
Intent i;
|
||||
|
||||
// Add the PC view at the back (and clear the task)
|
||||
i = new Intent(AppViewShortcutTrampoline.this, PcView.class);
|
||||
i.setAction(Intent.ACTION_MAIN);
|
||||
i.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK | Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
intentStack.add(i);
|
||||
|
||||
// Take this intent's data and create an intent to start the app view
|
||||
i = new Intent(getIntent());
|
||||
i.setClass(AppViewShortcutTrampoline.this, AppView.class);
|
||||
intentStack.add(i);
|
||||
|
||||
// If a game is running, we'll make the stream the top level activity
|
||||
if (details.runningGameId != 0) {
|
||||
intentStack.add(ServerHelper.createStartIntent(AppViewShortcutTrampoline.this,
|
||||
new NvApp("app", details.runningGameId), details, managerBinder));
|
||||
}
|
||||
|
||||
// Now start the activities
|
||||
startActivities(intentStack.toArray(new Intent[]{}));
|
||||
}
|
||||
else if (details.state == ComputerDetails.State.OFFLINE) {
|
||||
// Computer offline - display an error dialog
|
||||
Dialog.displayDialog(AppViewShortcutTrampoline.this,
|
||||
getResources().getString(R.string.conn_error_title),
|
||||
getResources().getString(R.string.error_pc_offline),
|
||||
true);
|
||||
}
|
||||
|
||||
// We don't want any more callbacks from now on, so go ahead
|
||||
// and unbind from the service
|
||||
if (managerBinder != null) {
|
||||
managerBinder.stopPolling();
|
||||
unbindService(serviceConnection);
|
||||
managerBinder = null;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}.start();
|
||||
}
|
||||
|
||||
public void onServiceDisconnected(ComponentName className) {
|
||||
managerBinder = null;
|
||||
}
|
||||
};
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
UiHelper.notifyNewRootView(this);
|
||||
|
||||
uuidString = getIntent().getStringExtra(UUID_EXTRA);
|
||||
|
||||
// Bind to the computer manager service
|
||||
bindService(new Intent(this, ComputerManagerService.class), serviceConnection,
|
||||
Service.BIND_AUTO_CREATE);
|
||||
|
||||
blockingLoadSpinner = SpinnerDialog.displayDialog(this, getResources().getString(R.string.conn_establishing_title),
|
||||
getResources().getString(R.string.applist_connect_msg), true);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPause() {
|
||||
super.onPause();
|
||||
|
||||
if (blockingLoadSpinner != null) {
|
||||
blockingLoadSpinner.dismiss();
|
||||
blockingLoadSpinner = null;
|
||||
}
|
||||
|
||||
Dialog.closeDialogs();
|
||||
|
||||
if (managerBinder != null) {
|
||||
managerBinder.stopPolling();
|
||||
unbindService(serviceConnection);
|
||||
managerBinder = null;
|
||||
}
|
||||
|
||||
finish();
|
||||
}
|
||||
}
|
||||
@@ -4,10 +4,10 @@ package com.limelight;
|
||||
import com.limelight.binding.PlatformBinding;
|
||||
import com.limelight.binding.input.ControllerHandler;
|
||||
import com.limelight.binding.input.KeyboardTranslator;
|
||||
import com.limelight.binding.input.NvMouseHelper;
|
||||
import com.limelight.binding.input.capture.InputCaptureManager;
|
||||
import com.limelight.binding.input.capture.InputCaptureProvider;
|
||||
import com.limelight.binding.input.TouchContext;
|
||||
import com.limelight.binding.input.driver.UsbDriverService;
|
||||
import com.limelight.binding.input.evdev.EvdevHandler;
|
||||
import com.limelight.binding.input.evdev.EvdevListener;
|
||||
import com.limelight.binding.input.virtual_controller.VirtualController;
|
||||
import com.limelight.binding.video.EnhancedDecoderRenderer;
|
||||
@@ -22,7 +22,9 @@ import com.limelight.nvstream.input.KeyboardPacket;
|
||||
import com.limelight.nvstream.input.MouseButtonPacket;
|
||||
import com.limelight.preferences.PreferenceConfiguration;
|
||||
import com.limelight.ui.GameGestures;
|
||||
import com.limelight.ui.StreamView;
|
||||
import com.limelight.utils.Dialog;
|
||||
import com.limelight.utils.ShortcutHelper;
|
||||
import com.limelight.utils.SpinnerDialog;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
@@ -38,6 +40,7 @@ import android.hardware.input.InputManager;
|
||||
import android.media.AudioManager;
|
||||
import android.net.ConnectivityManager;
|
||||
import android.net.wifi.WifiManager;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.os.IBinder;
|
||||
@@ -47,12 +50,10 @@ import android.view.InputDevice;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.SurfaceHolder;
|
||||
import android.view.SurfaceView;
|
||||
import android.view.View;
|
||||
import android.view.View.OnGenericMotionListener;
|
||||
import android.view.View.OnSystemUiVisibilityChangeListener;
|
||||
import android.view.View.OnTouchListener;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.Window;
|
||||
import android.view.WindowManager;
|
||||
import android.widget.FrameLayout;
|
||||
@@ -74,8 +75,8 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
||||
private final TouchContext[] touchContextMap = new TouchContext[2];
|
||||
private long threeFingerDownTime = 0;
|
||||
|
||||
private static final double REFERENCE_HORIZ_RES = 1280;
|
||||
private static final double REFERENCE_VERT_RES = 720;
|
||||
private static final int REFERENCE_HORIZ_RES = 1280;
|
||||
private static final int REFERENCE_VERT_RES = 720;
|
||||
|
||||
private static final int THREE_FINGER_TAP_THRESHOLD = 300;
|
||||
|
||||
@@ -84,19 +85,20 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
||||
private KeyboardTranslator keybTranslator;
|
||||
|
||||
private PreferenceConfiguration prefConfig;
|
||||
private final Point screenSize = new Point(0, 0);
|
||||
|
||||
private NvConnection conn;
|
||||
private SpinnerDialog spinner;
|
||||
private boolean displayedFailureDialog = false;
|
||||
private boolean connecting = false;
|
||||
private boolean connected = false;
|
||||
private boolean deferredSurfaceResize = false;
|
||||
|
||||
private EvdevHandler evdevHandler;
|
||||
private InputCaptureProvider inputCaptureProvider;
|
||||
private int modifierFlags = 0;
|
||||
private boolean grabbedInput = true;
|
||||
private boolean grabComboDown = false;
|
||||
private StreamView streamView;
|
||||
|
||||
private ShortcutHelper shortcutHelper;
|
||||
|
||||
private EnhancedDecoderRenderer decoderRenderer;
|
||||
|
||||
@@ -124,11 +126,15 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
||||
public static final String EXTRA_APP_ID = "AppId";
|
||||
public static final String EXTRA_UNIQUEID = "UniqueId";
|
||||
public static final String EXTRA_STREAMING_REMOTE = "Remote";
|
||||
public static final String EXTRA_PC_UUID = "UUID";
|
||||
public static final String EXTRA_PC_NAME = "PcName";
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
shortcutHelper = new ShortcutHelper(this);
|
||||
|
||||
String locale = PreferenceConfiguration.readPreferences(this).language;
|
||||
if (!locale.equals(PreferenceConfiguration.DEFAULT_LANGUAGE)) {
|
||||
Configuration config = new Configuration(getResources().getConfiguration());
|
||||
@@ -175,13 +181,10 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
||||
drFlags |= VideoDecoderRenderer.FLAG_FILL_SCREEN;
|
||||
}
|
||||
|
||||
Display display = getWindowManager().getDefaultDisplay();
|
||||
display.getSize(screenSize);
|
||||
|
||||
// Listen for events on the game surface
|
||||
SurfaceView sv = (SurfaceView) findViewById(R.id.surfaceView);
|
||||
sv.setOnGenericMotionListener(this);
|
||||
sv.setOnTouchListener(this);
|
||||
streamView = (StreamView) findViewById(R.id.surfaceView);
|
||||
streamView.setOnGenericMotionListener(this);
|
||||
streamView.setOnTouchListener(this);
|
||||
|
||||
// Warn the user if they're on a metered connection
|
||||
checkDataConnection();
|
||||
@@ -197,12 +200,18 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
||||
int appId = Game.this.getIntent().getIntExtra(EXTRA_APP_ID, StreamConfiguration.INVALID_APP_ID);
|
||||
String uniqueId = Game.this.getIntent().getStringExtra(EXTRA_UNIQUEID);
|
||||
boolean remote = Game.this.getIntent().getBooleanExtra(EXTRA_STREAMING_REMOTE, false);
|
||||
String uuid = Game.this.getIntent().getStringExtra(EXTRA_PC_UUID);
|
||||
String pcName = Game.this.getIntent().getStringExtra(EXTRA_PC_NAME);
|
||||
|
||||
if (appId == StreamConfiguration.INVALID_APP_ID) {
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
|
||||
// Add a launcher shortcut for this PC (forced, since this is user interaction)
|
||||
shortcutHelper.createAppViewShortcut(uuid, pcName, uuid, true);
|
||||
shortcutHelper.reportShortcutUsed(uuid);
|
||||
|
||||
// Initialize the MediaCodec helper before creating the decoder
|
||||
MediaCodecHelper.initializeWithContext(this);
|
||||
|
||||
@@ -245,49 +254,29 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
||||
// Initialize the connection
|
||||
conn = new NvConnection(host, uniqueId, Game.this, config, PlatformBinding.getCryptoProvider(this));
|
||||
keybTranslator = new KeyboardTranslator(conn);
|
||||
controllerHandler = new ControllerHandler(conn, this, prefConfig.multiController, prefConfig.deadzonePercentage);
|
||||
controllerHandler = new ControllerHandler(this, conn, this, prefConfig.multiController, prefConfig.deadzonePercentage);
|
||||
|
||||
InputManager inputManager = (InputManager) getSystemService(Context.INPUT_SERVICE);
|
||||
inputManager.registerInputDeviceListener(controllerHandler, null);
|
||||
|
||||
boolean aspectRatioMatch = false;
|
||||
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.KITKAT) {
|
||||
// On KitKat and later (where we can use the whole screen via immersive mode), we'll
|
||||
// calculate whether we need to scale by aspect ratio or not. If not, we'll use
|
||||
// setFixedSize so we can handle 4K properly. The only known devices that have
|
||||
// >= 4K screens have exactly 4K screens, so we'll be able to hit this good path
|
||||
// on these devices. On Marshmallow, we can start changing to 4K manually but no
|
||||
// 4K devices run 6.0 at the moment.
|
||||
double screenAspectRatio = ((double)screenSize.y) / screenSize.x;
|
||||
double streamAspectRatio = ((double)prefConfig.height) / prefConfig.width;
|
||||
if (Math.abs(screenAspectRatio - streamAspectRatio) < 0.001) {
|
||||
LimeLog.info("Stream has compatible aspect ratio with output display");
|
||||
aspectRatioMatch = true;
|
||||
}
|
||||
}
|
||||
|
||||
SurfaceHolder sh = sv.getHolder();
|
||||
if (prefConfig.stretchVideo || aspectRatioMatch) {
|
||||
// Set the surface to the size of the video
|
||||
sh.setFixedSize(prefConfig.width, prefConfig.height);
|
||||
}
|
||||
else {
|
||||
deferredSurfaceResize = true;
|
||||
}
|
||||
// Set to the optimal mode for streaming
|
||||
prepareDisplayForRendering();
|
||||
|
||||
// Initialize touch contexts
|
||||
for (int i = 0; i < touchContextMap.length; i++) {
|
||||
touchContextMap[i] = new TouchContext(conn, i,
|
||||
(REFERENCE_HORIZ_RES / (double)screenSize.x),
|
||||
(REFERENCE_VERT_RES / (double)screenSize.y));
|
||||
REFERENCE_HORIZ_RES, REFERENCE_VERT_RES,
|
||||
streamView);
|
||||
}
|
||||
|
||||
if (LimelightBuildProps.ROOT_BUILD) {
|
||||
// Start watching for raw input
|
||||
evdevHandler = new EvdevHandler(this, this);
|
||||
evdevHandler.start();
|
||||
// Use sustained performance mode on N+ to ensure consistent
|
||||
// CPU availability
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
getWindow().setSustainedPerformanceMode(true);
|
||||
}
|
||||
|
||||
inputCaptureProvider = InputCaptureManager.getInputCaptureProvider(this, this);
|
||||
|
||||
if (prefConfig.onscreenController) {
|
||||
// create virtual onscreen controller
|
||||
virtualController = new VirtualController(conn,
|
||||
@@ -303,26 +292,95 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
||||
}
|
||||
|
||||
// The connection will be started when the surface gets created
|
||||
sh.addCallback(this);
|
||||
streamView.getHolder().addCallback(this);
|
||||
}
|
||||
|
||||
private void resizeSurfaceWithAspectRatio(SurfaceView sv, double vidWidth, double vidHeight)
|
||||
{
|
||||
// Get the visible width of the activity
|
||||
double visibleWidth = getWindow().getDecorView().getWidth();
|
||||
private void prepareDisplayForRendering() {
|
||||
Display display = getWindowManager().getDefaultDisplay();
|
||||
WindowManager.LayoutParams windowLayoutParams = getWindow().getAttributes();
|
||||
|
||||
ViewGroup.LayoutParams lp = sv.getLayoutParams();
|
||||
// On M, we can explicitly set the optimal display mode
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
Display.Mode bestMode = display.getMode();
|
||||
for (Display.Mode candidate : display.getSupportedModes()) {
|
||||
boolean refreshRateOk = candidate.getRefreshRate() >= bestMode.getRefreshRate() &&
|
||||
candidate.getRefreshRate() < 63;
|
||||
boolean resolutionOk = candidate.getPhysicalWidth() >= bestMode.getPhysicalWidth() &&
|
||||
candidate.getPhysicalHeight() >= bestMode.getPhysicalHeight() &&
|
||||
candidate.getPhysicalWidth() <= 4096;
|
||||
|
||||
// Calculate the new size of the SurfaceView
|
||||
lp.width = (int) visibleWidth;
|
||||
lp.height = (int) ((vidHeight / vidWidth) * visibleWidth);
|
||||
LimeLog.info("Examining display mode: "+candidate.getPhysicalWidth()+"x"+
|
||||
candidate.getPhysicalHeight()+"x"+candidate.getRefreshRate());
|
||||
|
||||
// Apply the size change
|
||||
sv.setLayoutParams(lp);
|
||||
// On non-4K streams, we force the resolution to never change
|
||||
if (prefConfig.width < 3840) {
|
||||
if (display.getMode().getPhysicalWidth() != candidate.getPhysicalWidth() ||
|
||||
display.getMode().getPhysicalHeight() != candidate.getPhysicalHeight()) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// refresh virtual controller layout
|
||||
if (virtualController != null) {
|
||||
virtualController.refreshLayout();
|
||||
// Make sure the refresh rate doesn't regress
|
||||
if (!refreshRateOk) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Make sure the resolution doesn't regress
|
||||
if (!resolutionOk) {
|
||||
continue;
|
||||
}
|
||||
|
||||
bestMode = candidate;
|
||||
}
|
||||
LimeLog.info("Selected display mode: "+bestMode.getPhysicalWidth()+"x"+
|
||||
bestMode.getPhysicalHeight()+"x"+bestMode.getRefreshRate());
|
||||
windowLayoutParams.preferredDisplayModeId = bestMode.getModeId();
|
||||
}
|
||||
// On L, we can at least tell the OS that we want 60 Hz
|
||||
else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
float bestRefreshRate = display.getRefreshRate();
|
||||
for (float candidate : display.getSupportedRefreshRates()) {
|
||||
if (candidate > bestRefreshRate && candidate < 63) {
|
||||
LimeLog.info("Examining refresh rate: "+candidate);
|
||||
bestRefreshRate = candidate;
|
||||
}
|
||||
}
|
||||
LimeLog.info("Selected refresh rate: "+bestRefreshRate);
|
||||
windowLayoutParams.preferredRefreshRate = bestRefreshRate;
|
||||
}
|
||||
|
||||
// Apply the display mode change
|
||||
getWindow().setAttributes(windowLayoutParams);
|
||||
|
||||
// From 4.4 to 5.1 we can't ask for a 4K display mode, so we'll
|
||||
// need to hint the OS to provide one.
|
||||
boolean aspectRatioMatch = false;
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT &&
|
||||
Build.VERSION.SDK_INT <= Build.VERSION_CODES.LOLLIPOP_MR1) {
|
||||
// On KitKat and later (where we can use the whole screen via immersive mode), we'll
|
||||
// calculate whether we need to scale by aspect ratio or not. If not, we'll use
|
||||
// setFixedSize so we can handle 4K properly. The only known devices that have
|
||||
// >= 4K screens have exactly 4K screens, so we'll be able to hit this good path
|
||||
// on these devices. On Marshmallow, we can start changing to 4K manually but no
|
||||
// 4K devices run 6.0 at the moment.
|
||||
Point screenSize = new Point(0, 0);
|
||||
display.getSize(screenSize);
|
||||
|
||||
double screenAspectRatio = ((double)screenSize.y) / screenSize.x;
|
||||
double streamAspectRatio = ((double)prefConfig.height) / prefConfig.width;
|
||||
if (Math.abs(screenAspectRatio - streamAspectRatio) < 0.001) {
|
||||
LimeLog.info("Stream has compatible aspect ratio with output display");
|
||||
aspectRatioMatch = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (prefConfig.stretchVideo || aspectRatioMatch) {
|
||||
// Set the surface to the size of the video
|
||||
streamView.getHolder().setFixedSize(prefConfig.width, prefConfig.height);
|
||||
}
|
||||
else {
|
||||
// Set the surface to scale based on the aspect ratio of the stream
|
||||
streamView.setDesiredAspectRatio((double)prefConfig.width / (double)prefConfig.height);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -424,16 +482,10 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
||||
@Override
|
||||
public void run() {
|
||||
if (grabbedInput) {
|
||||
NvMouseHelper.setCursorVisibility(Game.this, true);
|
||||
if (evdevHandler != null) {
|
||||
evdevHandler.ungrabAll();
|
||||
}
|
||||
inputCaptureProvider.disableCapture();
|
||||
}
|
||||
else {
|
||||
NvMouseHelper.setCursorVisibility(Game.this, false);
|
||||
if (evdevHandler != null) {
|
||||
evdevHandler.regrabAll();
|
||||
}
|
||||
inputCaptureProvider.enableCapture();
|
||||
}
|
||||
|
||||
grabbedInput = !grabbedInput;
|
||||
@@ -540,11 +592,6 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
||||
return true;
|
||||
}
|
||||
|
||||
// Eat repeat down events
|
||||
if (event.getRepeatCount() > 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Pass through keyboard input if we're not grabbing
|
||||
if (!grabbedInput) {
|
||||
return super.onKeyDown(keyCode, event);
|
||||
@@ -661,10 +708,10 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
||||
}
|
||||
|
||||
// Get relative axis values if we can
|
||||
if (NvMouseHelper.eventHasRelativeMouseAxes(event)) {
|
||||
if (inputCaptureProvider.eventHasRelativeMouseAxes(event)) {
|
||||
// Send the deltas straight from the motion event
|
||||
conn.sendMouseMove((short)NvMouseHelper.getRelativeAxisX(event),
|
||||
(short)NvMouseHelper.getRelativeAxisY(event));
|
||||
conn.sendMouseMove((short) inputCaptureProvider.getRelativeAxisX(event),
|
||||
(short) inputCaptureProvider.getRelativeAxisY(event));
|
||||
|
||||
// We have to also update the position Android thinks the cursor is at
|
||||
// in order to avoid jumping when we stop moving or click.
|
||||
@@ -802,8 +849,8 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
||||
|
||||
// Scale the deltas if the device resolution is different
|
||||
// than the stream resolution
|
||||
deltaX = (int)Math.round((double)deltaX * (REFERENCE_HORIZ_RES / (double)screenSize.x));
|
||||
deltaY = (int)Math.round((double)deltaY * (REFERENCE_VERT_RES / (double)screenSize.y));
|
||||
deltaX = (int)Math.round((double)deltaX * (REFERENCE_HORIZ_RES / (double)streamView.getWidth()));
|
||||
deltaY = (int)Math.round((double)deltaY * (REFERENCE_VERT_RES / (double)streamView.getHeight()));
|
||||
|
||||
conn.sendMouseMove((short)deltaX, (short)deltaY);
|
||||
}
|
||||
@@ -841,14 +888,11 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
||||
conn.stop();
|
||||
}
|
||||
|
||||
// Close the Evdev reader to allow use of captured input devices
|
||||
if (evdevHandler != null) {
|
||||
evdevHandler.stop();
|
||||
evdevHandler = null;
|
||||
}
|
||||
|
||||
// Enable cursor visibility again
|
||||
NvMouseHelper.setCursorVisibility(this, true);
|
||||
inputCaptureProvider.disableCapture();
|
||||
|
||||
// Destroy the capture provider
|
||||
inputCaptureProvider.destroy();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -894,7 +938,7 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
||||
// Hide the mouse cursor now. Doing it before
|
||||
// dismissing the spinner seems to be undone
|
||||
// when the spinner gets displayed.
|
||||
NvMouseHelper.setCursorVisibility(Game.this, false);
|
||||
inputCaptureProvider.enableCapture();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -932,13 +976,6 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
||||
if (!connected && !connecting) {
|
||||
connecting = true;
|
||||
|
||||
// Resize the surface to match the aspect ratio of the video
|
||||
// This must be done after the surface is created.
|
||||
if (deferredSurfaceResize) {
|
||||
resizeSurfaceWithAspectRatio((SurfaceView) findViewById(R.id.surfaceView),
|
||||
prefConfig.width, prefConfig.height);
|
||||
}
|
||||
|
||||
conn.start(PlatformBinding.getDeviceName(), holder, drFlags,
|
||||
PlatformBinding.getAudioRenderer(), decoderRenderer);
|
||||
}
|
||||
@@ -947,6 +984,14 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
||||
@Override
|
||||
public void surfaceDestroyed(SurfaceHolder holder) {
|
||||
if (connected) {
|
||||
// HACK: Android is supposed to let you return from this function
|
||||
// before throwing a fit if you access the surface again. Unfortunately,
|
||||
// MediaCodec often tries to access the destroyed surface and triggers
|
||||
// an IllegalStateException. To workaround this, we will invoke
|
||||
// the DecoderRenderer's stop function ourselves, so it will hopefully
|
||||
// happen early enough to not trigger the bug
|
||||
decoderRenderer.stop();
|
||||
|
||||
stopConnection();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
package com.limelight;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.graphics.Bitmap;
|
||||
import android.os.Bundle;
|
||||
import android.webkit.WebView;
|
||||
import android.webkit.WebViewClient;
|
||||
|
||||
import com.limelight.utils.SpinnerDialog;
|
||||
|
||||
public class HelpActivity extends Activity {
|
||||
|
||||
private SpinnerDialog loadingDialog;
|
||||
private WebView webView;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
webView = new WebView(this);
|
||||
setContentView(webView);
|
||||
|
||||
// These allow the user to zoom the page
|
||||
webView.getSettings().setBuiltInZoomControls(true);
|
||||
webView.getSettings().setDisplayZoomControls(false);
|
||||
|
||||
// This sets the view to display the whole page by default
|
||||
webView.getSettings().setUseWideViewPort(true);
|
||||
webView.getSettings().setLoadWithOverviewMode(true);
|
||||
|
||||
// This allows the links to places on the same page to work
|
||||
webView.getSettings().setJavaScriptEnabled(true);
|
||||
|
||||
webView.setWebViewClient(new WebViewClient() {
|
||||
@Override
|
||||
public void onPageStarted(WebView view, String url, Bitmap favicon) {
|
||||
if (loadingDialog == null) {
|
||||
loadingDialog = SpinnerDialog.displayDialog(HelpActivity.this,
|
||||
getResources().getString(R.string.help_loading_title),
|
||||
getResources().getString(R.string.help_loading_msg), false);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPageFinished(WebView view, String url) {
|
||||
if (loadingDialog != null) {
|
||||
loadingDialog.dismiss();
|
||||
loadingDialog = null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean shouldOverrideUrlLoading(WebView view, String url) {
|
||||
if (url.toUpperCase().startsWith("https://github.com/moonlight-stream/moonlight-docs/wiki/".toUpperCase()) ||
|
||||
url.toUpperCase().startsWith("http://github.com/moonlight-stream/moonlight-docs/wiki/".toUpperCase())) {
|
||||
// Allow navigation to Moonlight docs
|
||||
return false;
|
||||
}
|
||||
else {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
webView.loadUrl(getIntent().getData().toString());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBackPressed() {
|
||||
// Back goes back through the WebView history
|
||||
// until no more history remains
|
||||
if (webView.canGoBack()) {
|
||||
webView.goBack();
|
||||
}
|
||||
else {
|
||||
super.onBackPressed();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -23,7 +23,9 @@ import com.limelight.preferences.StreamSettings;
|
||||
import com.limelight.ui.AdapterFragment;
|
||||
import com.limelight.ui.AdapterFragmentCallbacks;
|
||||
import com.limelight.utils.Dialog;
|
||||
import com.limelight.utils.HelpLauncher;
|
||||
import com.limelight.utils.ServerHelper;
|
||||
import com.limelight.utils.ShortcutHelper;
|
||||
import com.limelight.utils.UiHelper;
|
||||
|
||||
import android.app.Activity;
|
||||
@@ -52,6 +54,7 @@ import android.widget.AdapterView.AdapterContextMenuInfo;
|
||||
public class PcView extends Activity implements AdapterFragmentCallbacks {
|
||||
private RelativeLayout noPcFoundLayout;
|
||||
private PcGridAdapter pcGridAdapter;
|
||||
private ShortcutHelper shortcutHelper;
|
||||
private ComputerManagerService.ComputerManagerBinder managerBinder;
|
||||
private boolean freezeUpdates, runningPolling, inForeground;
|
||||
private final ServiceConnection serviceConnection = new ServiceConnection() {
|
||||
@@ -109,6 +112,7 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
|
||||
// Setup the list view
|
||||
ImageButton settingsButton = (ImageButton) findViewById(R.id.settingsButton);
|
||||
ImageButton addComputerButton = (ImageButton) findViewById(R.id.manuallyAddPc);
|
||||
ImageButton helpButton = (ImageButton) findViewById(R.id.helpButton);
|
||||
|
||||
settingsButton.setOnClickListener(new OnClickListener() {
|
||||
@Override
|
||||
@@ -123,6 +127,12 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
|
||||
startActivity(i);
|
||||
}
|
||||
});
|
||||
helpButton.setOnClickListener(new OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
HelpLauncher.launchSetupGuide(PcView.this);
|
||||
}
|
||||
});
|
||||
|
||||
getFragmentManager().beginTransaction()
|
||||
.replace(R.id.pcFragmentContainer, new AdapterFragment())
|
||||
@@ -142,6 +152,8 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
shortcutHelper = new ShortcutHelper(this);
|
||||
|
||||
String locale = PreferenceConfiguration.readPreferences(this).language;
|
||||
if (!locale.equals(PreferenceConfiguration.DEFAULT_LANGUAGE)) {
|
||||
Configuration config = new Configuration(getResources().getConfiguration());
|
||||
@@ -299,7 +311,7 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
|
||||
// Stop updates and wait while pairing
|
||||
stopComputerUpdates(true);
|
||||
|
||||
InetAddress addr = null;
|
||||
InetAddress addr;
|
||||
if (computer.reachability == ComputerDetails.Reachability.LOCAL) {
|
||||
addr = computer.localIp;
|
||||
}
|
||||
@@ -327,17 +339,24 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
|
||||
Dialog.displayDialog(PcView.this, getResources().getString(R.string.pair_pairing_title),
|
||||
getResources().getString(R.string.pair_pairing_msg)+" "+pinStr, false);
|
||||
|
||||
PairingManager.PairState pairState = httpConn.pair(pinStr);
|
||||
PairingManager.PairState pairState = httpConn.pair(httpConn.getServerInfo(), pinStr);
|
||||
if (pairState == PairingManager.PairState.PIN_WRONG) {
|
||||
message = getResources().getString(R.string.pair_incorrect_pin);
|
||||
}
|
||||
else if (pairState == PairingManager.PairState.FAILED) {
|
||||
message = getResources().getString(R.string.pair_fail);
|
||||
}
|
||||
else if (pairState == PairingManager.PairState.ALREADY_IN_PROGRESS) {
|
||||
message = getResources().getString(R.string.pair_already_in_progress);
|
||||
}
|
||||
else if (pairState == PairingManager.PairState.PAIRED) {
|
||||
// Just navigate to the app view without displaying a toast
|
||||
message = null;
|
||||
success = true;
|
||||
|
||||
// Invalidate reachability information after pairing to force
|
||||
// a refresh before reading pair state again
|
||||
managerBinder.invalidateStateForComputer(computer.uuid);
|
||||
}
|
||||
else {
|
||||
// Should be no other values
|
||||
@@ -429,7 +448,7 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
|
||||
NvHTTP httpConn;
|
||||
String message;
|
||||
try {
|
||||
InetAddress addr = null;
|
||||
InetAddress addr;
|
||||
if (computer.reachability == ComputerDetails.Reachability.LOCAL) {
|
||||
addr = computer.localIp;
|
||||
}
|
||||
@@ -558,6 +577,10 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
|
||||
ComputerObject computer = (ComputerObject) pcGridAdapter.getItem(i);
|
||||
|
||||
if (details.equals(computer.details)) {
|
||||
// Disable or delete shortcuts referencing this PC
|
||||
shortcutHelper.disableShortcut(details.uuid.toString(),
|
||||
getResources().getString(R.string.scut_deleted_pc));
|
||||
|
||||
pcGridAdapter.removeComputer(computer);
|
||||
pcGridAdapter.notifyDataSetChanged();
|
||||
|
||||
@@ -584,6 +607,11 @@ public class PcView extends Activity implements AdapterFragmentCallbacks {
|
||||
}
|
||||
}
|
||||
|
||||
// Add a launcher shortcut for this PC
|
||||
if (details.pairState == PairState.PAIRED) {
|
||||
shortcutHelper.createAppViewShortcut(details.uuid.toString(), details, false);
|
||||
}
|
||||
|
||||
if (existingEntry != null) {
|
||||
// Replace the information in the existing entry
|
||||
existingEntry.details = details;
|
||||
|
||||
@@ -1,24 +1,31 @@
|
||||
package com.limelight.binding.input;
|
||||
|
||||
import android.content.Context;
|
||||
import android.hardware.input.InputManager;
|
||||
import android.os.SystemClock;
|
||||
import android.util.SparseArray;
|
||||
import android.view.InputDevice;
|
||||
import android.view.InputEvent;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.MotionEvent;
|
||||
import android.widget.Toast;
|
||||
|
||||
import com.limelight.LimeLog;
|
||||
import com.limelight.binding.input.driver.UsbDriverListener;
|
||||
import com.limelight.nvstream.NvConnection;
|
||||
import com.limelight.nvstream.input.ControllerPacket;
|
||||
import com.limelight.nvstream.input.MouseButtonPacket;
|
||||
import com.limelight.ui.GameGestures;
|
||||
import com.limelight.utils.Vector2d;
|
||||
|
||||
import java.util.Timer;
|
||||
import java.util.TimerTask;
|
||||
|
||||
public class ControllerHandler implements InputManager.InputDeviceListener, UsbDriverListener {
|
||||
|
||||
private static final int MAXIMUM_BUMPER_UP_DELAY_MS = 100;
|
||||
|
||||
private static final int START_DOWN_TIME_KEYB_MS = 750;
|
||||
private static final int START_DOWN_TIME_MOUSE_MODE_MS = 750;
|
||||
|
||||
private static final int MINIMUM_BUTTON_DOWN_TIME_MS = 25;
|
||||
|
||||
@@ -34,6 +41,7 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
|
||||
private final SparseArray<UsbDeviceContext> usbDeviceContexts = new SparseArray<>();
|
||||
|
||||
private final NvConnection conn;
|
||||
private final Context activityContext;
|
||||
private final double stickDeadzone;
|
||||
private final InputDeviceContext defaultContext = new InputDeviceContext();
|
||||
private final GameGestures gestures;
|
||||
@@ -42,7 +50,8 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
|
||||
private final boolean multiControllerEnabled;
|
||||
private short currentControllers;
|
||||
|
||||
public ControllerHandler(NvConnection conn, GameGestures gestures, boolean multiControllerEnabled, int deadzonePercentage) {
|
||||
public ControllerHandler(Context activityContext, NvConnection conn, GameGestures gestures, boolean multiControllerEnabled, int deadzonePercentage) {
|
||||
this.activityContext = activityContext;
|
||||
this.conn = conn;
|
||||
this.gestures = gestures;
|
||||
this.multiControllerEnabled = multiControllerEnabled;
|
||||
@@ -82,6 +91,7 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
|
||||
defaultContext.leftTriggerAxis = MotionEvent.AXIS_BRAKE;
|
||||
defaultContext.rightTriggerAxis = MotionEvent.AXIS_GAS;
|
||||
defaultContext.controllerNumber = (short) 0;
|
||||
defaultContext.assignedControllerNumber = true;
|
||||
}
|
||||
|
||||
private static InputDevice.MotionRange getMotionRangeForJoystickAxis(InputDevice dev, int axis) {
|
||||
@@ -146,8 +156,9 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
|
||||
InputDeviceContext devContext = (InputDeviceContext) context;
|
||||
|
||||
LimeLog.info(devContext.name+" ("+context.id+") needs a controller number assigned");
|
||||
if (devContext.name != null && devContext.name.contains("gpio-keys")) {
|
||||
// This is the back button on Shield portable consoles
|
||||
if (devContext.name != null &&
|
||||
(devContext.name.contains("gpio-keys") || // This is the back button on Shield portable consoles
|
||||
devContext.name.contains("joy_key"))) { // These are the gamepad buttons on the Archos Gamepad 2
|
||||
LimeLog.info("Built-in buttons hardcoded as controller 0");
|
||||
context.controllerNumber = 0;
|
||||
}
|
||||
@@ -212,6 +223,7 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
|
||||
String devName = dev.getName();
|
||||
|
||||
LimeLog.info("Creating controller context for device: "+devName);
|
||||
LimeLog.info(dev.toString());
|
||||
|
||||
context.name = devName;
|
||||
context.id = dev.getId();
|
||||
@@ -229,6 +241,7 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
|
||||
InputDevice.MotionRange rightTriggerRange = getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_RTRIGGER);
|
||||
InputDevice.MotionRange brakeRange = getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_BRAKE);
|
||||
InputDevice.MotionRange gasRange = getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_GAS);
|
||||
InputDevice.MotionRange throttleRange = getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_THROTTLE);
|
||||
if (leftTriggerRange != null && rightTriggerRange != null)
|
||||
{
|
||||
// Some controllers use LTRIGGER and RTRIGGER (like Ouya)
|
||||
@@ -241,6 +254,12 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
|
||||
context.leftTriggerAxis = MotionEvent.AXIS_BRAKE;
|
||||
context.rightTriggerAxis = MotionEvent.AXIS_GAS;
|
||||
}
|
||||
else if (brakeRange != null && throttleRange != null)
|
||||
{
|
||||
// Others use THROTTLE and BRAKE (like Xiaomi)
|
||||
context.leftTriggerAxis = MotionEvent.AXIS_BRAKE;
|
||||
context.rightTriggerAxis = MotionEvent.AXIS_THROTTLE;
|
||||
}
|
||||
else
|
||||
{
|
||||
InputDevice.MotionRange rxRange = getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_RX);
|
||||
@@ -324,8 +343,22 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
|
||||
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.KITKAT) {
|
||||
boolean[] hasSelectKey = dev.hasKeys(KeyEvent.KEYCODE_BUTTON_SELECT, KeyEvent.KEYCODE_BACK, 0);
|
||||
if (hasSelectKey[0] && hasSelectKey[1]) {
|
||||
LimeLog.info("Ignoring back button because select is present");
|
||||
context.ignoreBack = true;
|
||||
// Xiaomi gamepads claim to have both buttons then only send KEYCODE_BACK events
|
||||
if (dev.getVendorId() != 0x2717) {
|
||||
LimeLog.info("Ignoring back button because select is present");
|
||||
context.ignoreBack = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The ADT-1 controller needs a similar fixup to the ASUS Gamepad
|
||||
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.KITKAT) {
|
||||
// The device name provided is just "Gamepad" which is pretty useless, so we
|
||||
// use VID/PID instead
|
||||
if (dev.getVendorId() == 0x18d1 && dev.getProductId() == 0x2c40) {
|
||||
context.backIsStart = true;
|
||||
context.modeIsSelect = true;
|
||||
context.triggerDeadzone = 0.30f;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -371,6 +404,20 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
|
||||
context.isServal = true;
|
||||
context.ignoreBack = true;
|
||||
}
|
||||
// The Xbox One S Bluetooth controller has some mappings that need fixing up.
|
||||
// However, Microsoft released a firmware update with no change to VID/PID
|
||||
// or device name that fixed the mappings for Android. Since there's
|
||||
// no good way to detect this, we'll use the presence of GAS/BRAKE axes
|
||||
// that were added in the latest firmware. If those are present, the only
|
||||
// required fixup is ignoring the select button.
|
||||
else if (devName.equals("Xbox Wireless Controller")) {
|
||||
if (gasRange == null) {
|
||||
context.isXboxBtController = true;
|
||||
}
|
||||
else {
|
||||
context.ignoreBack = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LimeLog.info("Analog stick deadzone: "+context.leftStickDeadzoneRadius+" "+context.rightStickDeadzoneRadius);
|
||||
@@ -379,31 +426,141 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
|
||||
return context;
|
||||
}
|
||||
|
||||
private InputDeviceContext getContextForDevice(InputDevice dev) {
|
||||
private InputDeviceContext getContextForEvent(InputEvent event) {
|
||||
// Unknown devices use the default context
|
||||
if (dev == null) {
|
||||
if (event.getDeviceId() == 0) {
|
||||
return defaultContext;
|
||||
}
|
||||
else if (event.getDevice() == null) {
|
||||
// During device removal, sometimes we can get events after the
|
||||
// input device has been destroyed. In this case we'll see a
|
||||
// != 0 device ID but no device attached.
|
||||
return null;
|
||||
}
|
||||
|
||||
// Return the existing context if it exists
|
||||
InputDeviceContext context = inputDeviceContexts.get(dev.getId());
|
||||
InputDeviceContext context = inputDeviceContexts.get(event.getDeviceId());
|
||||
if (context != null) {
|
||||
return context;
|
||||
}
|
||||
|
||||
// Otherwise create a new context
|
||||
context = createInputDeviceContextForDevice(dev);
|
||||
inputDeviceContexts.put(dev.getId(), context);
|
||||
context = createInputDeviceContextForDevice(event.getDevice());
|
||||
inputDeviceContexts.put(event.getDeviceId(), context);
|
||||
|
||||
return context;
|
||||
}
|
||||
|
||||
private void sendControllerInputPacket(GenericControllerContext context) {
|
||||
assignControllerNumberIfNeeded(context);
|
||||
conn.sendControllerInput(context.controllerNumber, context.inputMap,
|
||||
context.leftTrigger, context.rightTrigger,
|
||||
context.leftStickX, context.leftStickY,
|
||||
context.rightStickX, context.rightStickY);
|
||||
private byte maxByMagnitude(byte a, byte b) {
|
||||
int absA = Math.abs(a);
|
||||
int absB = Math.abs(b);
|
||||
if (absA > absB) {
|
||||
return a;
|
||||
}
|
||||
else {
|
||||
return b;
|
||||
}
|
||||
}
|
||||
|
||||
private short maxByMagnitude(short a, short b) {
|
||||
int absA = Math.abs(a);
|
||||
int absB = Math.abs(b);
|
||||
if (absA > absB) {
|
||||
return a;
|
||||
}
|
||||
else {
|
||||
return b;
|
||||
}
|
||||
}
|
||||
|
||||
private void sendControllerInputPacket(GenericControllerContext originalContext) {
|
||||
assignControllerNumberIfNeeded(originalContext);
|
||||
|
||||
// Take the context's controller number and fuse all inputs with the same number
|
||||
short controllerNumber = originalContext.controllerNumber;
|
||||
short inputMap = 0;
|
||||
byte leftTrigger = 0;
|
||||
byte rightTrigger = 0;
|
||||
short leftStickX = 0;
|
||||
short leftStickY = 0;
|
||||
short rightStickX = 0;
|
||||
short rightStickY = 0;
|
||||
|
||||
// In order to properly handle controllers that are split into multiple devices,
|
||||
// we must aggregate all controllers with the same controller number into a single
|
||||
// device before we send it.
|
||||
for (int i = 0; i < inputDeviceContexts.size(); i++) {
|
||||
GenericControllerContext context = inputDeviceContexts.valueAt(i);
|
||||
if (context.assignedControllerNumber &&
|
||||
context.controllerNumber == controllerNumber &&
|
||||
context.mouseEmulationActive == originalContext.mouseEmulationActive) {
|
||||
inputMap |= context.inputMap;
|
||||
leftTrigger |= maxByMagnitude(leftTrigger, context.leftTrigger);
|
||||
rightTrigger |= maxByMagnitude(rightTrigger, context.rightTrigger);
|
||||
leftStickX |= maxByMagnitude(leftStickX, context.leftStickX);
|
||||
leftStickY |= maxByMagnitude(leftStickY, context.leftStickY);
|
||||
rightStickX |= maxByMagnitude(rightStickX, context.rightStickX);
|
||||
rightStickY |= maxByMagnitude(rightStickY, context.rightStickY);
|
||||
}
|
||||
}
|
||||
for (int i = 0; i < usbDeviceContexts.size(); i++) {
|
||||
GenericControllerContext context = usbDeviceContexts.valueAt(i);
|
||||
if (context.assignedControllerNumber &&
|
||||
context.controllerNumber == controllerNumber &&
|
||||
context.mouseEmulationActive == originalContext.mouseEmulationActive) {
|
||||
inputMap |= context.inputMap;
|
||||
leftTrigger |= maxByMagnitude(leftTrigger, context.leftTrigger);
|
||||
rightTrigger |= maxByMagnitude(rightTrigger, context.rightTrigger);
|
||||
leftStickX |= maxByMagnitude(leftStickX, context.leftStickX);
|
||||
leftStickY |= maxByMagnitude(leftStickY, context.leftStickY);
|
||||
rightStickX |= maxByMagnitude(rightStickX, context.rightStickX);
|
||||
rightStickY |= maxByMagnitude(rightStickY, context.rightStickY);
|
||||
}
|
||||
}
|
||||
if (defaultContext.controllerNumber == controllerNumber) {
|
||||
inputMap |= defaultContext.inputMap;
|
||||
leftTrigger |= maxByMagnitude(leftTrigger, defaultContext.leftTrigger);
|
||||
rightTrigger |= maxByMagnitude(rightTrigger, defaultContext.rightTrigger);
|
||||
leftStickX |= maxByMagnitude(leftStickX, defaultContext.leftStickX);
|
||||
leftStickY |= maxByMagnitude(leftStickY, defaultContext.leftStickY);
|
||||
rightStickX |= maxByMagnitude(rightStickX, defaultContext.rightStickX);
|
||||
rightStickY |= maxByMagnitude(rightStickY, defaultContext.rightStickY);
|
||||
}
|
||||
|
||||
if (originalContext.mouseEmulationActive) {
|
||||
int changedMask = inputMap ^ originalContext.mouseEmulationLastInputMap;
|
||||
|
||||
boolean aDown = (inputMap & ControllerPacket.A_FLAG) != 0;
|
||||
boolean bDown = (inputMap & ControllerPacket.B_FLAG) != 0;
|
||||
|
||||
originalContext.mouseEmulationLastInputMap = inputMap;
|
||||
|
||||
if ((changedMask & ControllerPacket.A_FLAG) != 0) {
|
||||
if (aDown) {
|
||||
conn.sendMouseButtonDown(MouseButtonPacket.BUTTON_LEFT);
|
||||
}
|
||||
else {
|
||||
conn.sendMouseButtonUp(MouseButtonPacket.BUTTON_LEFT);
|
||||
}
|
||||
}
|
||||
if ((changedMask & ControllerPacket.B_FLAG) != 0) {
|
||||
if (bDown) {
|
||||
conn.sendMouseButtonDown(MouseButtonPacket.BUTTON_RIGHT);
|
||||
}
|
||||
else {
|
||||
conn.sendMouseButtonUp(MouseButtonPacket.BUTTON_RIGHT);
|
||||
}
|
||||
}
|
||||
|
||||
conn.sendControllerInput(controllerNumber,
|
||||
(short)0, (byte)0, (byte)0, (short)0, (short)0, (short)0, (short)0);
|
||||
}
|
||||
else {
|
||||
conn.sendControllerInput(controllerNumber, inputMap,
|
||||
leftTrigger, rightTrigger,
|
||||
leftStickX, leftStickY,
|
||||
rightStickX, rightStickY);
|
||||
}
|
||||
}
|
||||
|
||||
// Return a valid keycode, 0 to consume, or -1 to not consume the event
|
||||
@@ -417,41 +574,38 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
|
||||
}
|
||||
|
||||
if (context.isDualShock4) {
|
||||
switch (event.getKeyCode()) {
|
||||
case KeyEvent.KEYCODE_BUTTON_Y:
|
||||
return KeyEvent.KEYCODE_BUTTON_L1;
|
||||
|
||||
case KeyEvent.KEYCODE_BUTTON_Z:
|
||||
return KeyEvent.KEYCODE_BUTTON_R1;
|
||||
|
||||
case KeyEvent.KEYCODE_BUTTON_C:
|
||||
return KeyEvent.KEYCODE_BUTTON_B;
|
||||
|
||||
case KeyEvent.KEYCODE_BUTTON_X:
|
||||
return KeyEvent.KEYCODE_BUTTON_Y;
|
||||
|
||||
case KeyEvent.KEYCODE_BUTTON_B:
|
||||
return KeyEvent.KEYCODE_BUTTON_A;
|
||||
|
||||
case KeyEvent.KEYCODE_BUTTON_A:
|
||||
return KeyEvent.KEYCODE_BUTTON_X;
|
||||
|
||||
case KeyEvent.KEYCODE_BUTTON_SELECT:
|
||||
return KeyEvent.KEYCODE_BUTTON_THUMBL;
|
||||
|
||||
case KeyEvent.KEYCODE_BUTTON_START:
|
||||
return KeyEvent.KEYCODE_BUTTON_THUMBR;
|
||||
|
||||
case KeyEvent.KEYCODE_BUTTON_L2:
|
||||
return KeyEvent.KEYCODE_BUTTON_SELECT;
|
||||
|
||||
case KeyEvent.KEYCODE_BUTTON_R2:
|
||||
return KeyEvent.KEYCODE_BUTTON_START;
|
||||
|
||||
// These are duplicate trigger events
|
||||
case KeyEvent.KEYCODE_BUTTON_R1:
|
||||
case KeyEvent.KEYCODE_BUTTON_L1:
|
||||
return 0;
|
||||
switch (event.getScanCode()) {
|
||||
case 304:
|
||||
return KeyEvent.KEYCODE_BUTTON_X;
|
||||
case 305:
|
||||
return KeyEvent.KEYCODE_BUTTON_A;
|
||||
case 306:
|
||||
return KeyEvent.KEYCODE_BUTTON_B;
|
||||
case 307:
|
||||
return KeyEvent.KEYCODE_BUTTON_Y;
|
||||
case 308:
|
||||
return KeyEvent.KEYCODE_BUTTON_L1;
|
||||
case 309:
|
||||
return KeyEvent.KEYCODE_BUTTON_R1;
|
||||
/*
|
||||
**** Using analog triggers instead ****
|
||||
case 310:
|
||||
return KeyEvent.KEYCODE_BUTTON_L2;
|
||||
case 311:
|
||||
return KeyEvent.KEYCODE_BUTTON_R2;
|
||||
*/
|
||||
case 312:
|
||||
return KeyEvent.KEYCODE_BUTTON_SELECT;
|
||||
case 313:
|
||||
return KeyEvent.KEYCODE_BUTTON_START;
|
||||
case 314:
|
||||
return KeyEvent.KEYCODE_BUTTON_THUMBL;
|
||||
case 315:
|
||||
return KeyEvent.KEYCODE_BUTTON_THUMBR;
|
||||
case 316:
|
||||
return KeyEvent.KEYCODE_BUTTON_MODE;
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
// If this is a Serval controller sending an unknown key code, it's probably
|
||||
@@ -464,6 +618,35 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
|
||||
return KeyEvent.KEYCODE_BUTTON_START;
|
||||
}
|
||||
}
|
||||
else if (context.isXboxBtController) {
|
||||
switch (event.getScanCode()) {
|
||||
case 306:
|
||||
return KeyEvent.KEYCODE_BUTTON_X;
|
||||
case 307:
|
||||
return KeyEvent.KEYCODE_BUTTON_Y;
|
||||
case 308:
|
||||
return KeyEvent.KEYCODE_BUTTON_L1;
|
||||
case 309:
|
||||
return KeyEvent.KEYCODE_BUTTON_R1;
|
||||
case 310:
|
||||
return KeyEvent.KEYCODE_BUTTON_SELECT;
|
||||
case 311:
|
||||
return KeyEvent.KEYCODE_BUTTON_START;
|
||||
case 312:
|
||||
return KeyEvent.KEYCODE_BUTTON_THUMBL;
|
||||
case 313:
|
||||
return KeyEvent.KEYCODE_BUTTON_THUMBR;
|
||||
case 139:
|
||||
return KeyEvent.KEYCODE_BUTTON_MODE;
|
||||
default:
|
||||
// Other buttons are mapped correctly
|
||||
}
|
||||
|
||||
// The Xbox button is sent as MENU
|
||||
if (event.getKeyCode() == KeyEvent.KEYCODE_MENU) {
|
||||
return KeyEvent.KEYCODE_BUTTON_MODE;
|
||||
}
|
||||
}
|
||||
|
||||
if (context.hatXAxis != -1 && context.hatYAxis != -1) {
|
||||
switch (event.getKeyCode()) {
|
||||
@@ -478,7 +661,12 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
|
||||
}
|
||||
else if (context.hatXAxis == -1 &&
|
||||
context.hatYAxis == -1 &&
|
||||
context.isXboxController &&
|
||||
/* FIXME: There's no good way to know for sure if xpad is bound
|
||||
to this device, so we won't use the name to validate if these
|
||||
scancodes should be mapped to DPAD
|
||||
|
||||
context.isXboxController &&
|
||||
*/
|
||||
event.getKeyCode() == KeyEvent.KEYCODE_UNKNOWN) {
|
||||
// If there's not a proper Xbox controller mapping, we'll translate the raw d-pad
|
||||
// scan codes into proper key codes
|
||||
@@ -568,9 +756,23 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
|
||||
}
|
||||
|
||||
if (context.leftTriggerAxis != -1 && context.rightTriggerAxis != -1) {
|
||||
// Android sends an initial 0 value for trigger axes even if the trigger
|
||||
// should be negative when idle. After the first touch, the axes will go back
|
||||
// to normal behavior, so ignore triggersIdleNegative for each trigger until
|
||||
// first touch.
|
||||
if (lt != 0) {
|
||||
context.leftTriggerUsed = true;
|
||||
}
|
||||
if (rt != 0) {
|
||||
context.rightTriggerUsed = true;
|
||||
}
|
||||
if (context.triggersIdleNegative) {
|
||||
lt = (lt + 1) / 2;
|
||||
rt = (rt + 1) / 2;
|
||||
if (context.leftTriggerUsed) {
|
||||
lt = (lt + 1) / 2;
|
||||
}
|
||||
if (context.rightTriggerUsed) {
|
||||
rt = (rt + 1) / 2;
|
||||
}
|
||||
}
|
||||
|
||||
if (lt <= context.triggerDeadzone) {
|
||||
@@ -606,7 +808,11 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
|
||||
}
|
||||
|
||||
public boolean handleMotionEvent(MotionEvent event) {
|
||||
InputDeviceContext context = getContextForDevice(event.getDevice());
|
||||
InputDeviceContext context = getContextForEvent(event);
|
||||
if (context == null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
float lsX = 0, lsY = 0, rsX = 0, rsY = 0, rt = 0, lt = 0, hatX = 0, hatY = 0;
|
||||
|
||||
// We purposefully ignore the historical values in the motion event as it makes
|
||||
@@ -637,8 +843,51 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
|
||||
return true;
|
||||
}
|
||||
|
||||
private short scaleRawStickAxis(float stickValue) {
|
||||
return (short)Math.pow(stickValue, 3);
|
||||
}
|
||||
|
||||
private void sendEmulatedMouseEvent(short x, short y) {
|
||||
Vector2d vector = new Vector2d();
|
||||
vector.initialize(x, y);
|
||||
vector.scalarMultiply(1 / 32766.0f);
|
||||
vector.scalarMultiply(4);
|
||||
if (vector.getMagnitude() > 0) {
|
||||
// Move faster as the stick is pressed further from center
|
||||
vector.scalarMultiply(Math.pow(vector.getMagnitude(), 2));
|
||||
if (vector.getMagnitude() >= 1) {
|
||||
conn.sendMouseMove((short)vector.getX(), (short)-vector.getY());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void toggleMouseEmulation(final GenericControllerContext context) {
|
||||
if (context.mouseEmulationTimer != null) {
|
||||
context.mouseEmulationTimer.cancel();
|
||||
context.mouseEmulationTimer = null;
|
||||
}
|
||||
|
||||
context.mouseEmulationActive = !context.mouseEmulationActive;
|
||||
Toast.makeText(activityContext, "Mouse emulation is: " + (context.mouseEmulationActive ? "ON" : "OFF"), Toast.LENGTH_SHORT).show();
|
||||
|
||||
if (context.mouseEmulationActive) {
|
||||
context.mouseEmulationTimer = new Timer();
|
||||
context.mouseEmulationTimer.schedule(new TimerTask() {
|
||||
@Override
|
||||
public void run() {
|
||||
// Send mouse movement events from analog sticks
|
||||
sendEmulatedMouseEvent(context.leftStickX, context.leftStickY);
|
||||
sendEmulatedMouseEvent(context.rightStickX, context.rightStickY);
|
||||
}
|
||||
}, 50, 50);
|
||||
}
|
||||
}
|
||||
|
||||
public boolean handleButtonUp(KeyEvent event) {
|
||||
InputDeviceContext context = getContextForDevice(event.getDevice());
|
||||
InputDeviceContext context = getContextForEvent(event);
|
||||
if (context == null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
int keyCode = handleRemapping(context, event);
|
||||
if (keyCode == 0) {
|
||||
@@ -663,10 +912,12 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
|
||||
break;
|
||||
case KeyEvent.KEYCODE_BUTTON_START:
|
||||
case KeyEvent.KEYCODE_MENU:
|
||||
if (SystemClock.uptimeMillis() - context.startDownTime > ControllerHandler.START_DOWN_TIME_KEYB_MS) {
|
||||
// FIXME: The stock keyboard doesn't have controller focus so isn't usable. I'm not enabling this shortcut
|
||||
// until we have a custom keyboard or some other fix
|
||||
//gestures.showKeyboard();
|
||||
// Sometimes we'll get a spurious key up event on controller disconnect.
|
||||
// Make sure it's real by checking that the key is actually down before taking
|
||||
// any action.
|
||||
if ((context.inputMap & ControllerPacket.PLAY_FLAG) != 0 &&
|
||||
SystemClock.uptimeMillis() - context.startDownTime > ControllerHandler.START_DOWN_TIME_MOUSE_MODE_MS) {
|
||||
toggleMouseEmulation(context);
|
||||
}
|
||||
context.inputMap &= ~ControllerPacket.PLAY_FLAG;
|
||||
break;
|
||||
@@ -763,7 +1014,10 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
|
||||
}
|
||||
|
||||
public boolean handleButtonDown(KeyEvent event) {
|
||||
InputDeviceContext context = getContextForDevice(event.getDevice());
|
||||
InputDeviceContext context = getContextForEvent(event);
|
||||
if (context == null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
int keyCode = handleRemapping(context, event);
|
||||
if (keyCode == 0) {
|
||||
@@ -855,11 +1109,11 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
|
||||
context.emulatingButtonFlags |= ControllerHandler.EMULATING_SPECIAL;
|
||||
}
|
||||
|
||||
// Send a new input packet if this is the first instance of a button down event
|
||||
// or anytime if we're emulating a button
|
||||
if (event.getRepeatCount() == 0 || context.emulatingButtonFlags != 0) {
|
||||
sendControllerInputPacket(context);
|
||||
}
|
||||
|
||||
// We don't need to send repeat key down events, but the platform
|
||||
// sends us events that claim to be repeats but they're from different
|
||||
// devices, so we just send them all and deal with some duplicates.
|
||||
sendControllerInputPacket(context);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -933,6 +1187,10 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
|
||||
public short rightStickY = 0x0000;
|
||||
public short leftStickX = 0x0000;
|
||||
public short leftStickY = 0x0000;
|
||||
|
||||
public boolean mouseEmulationActive;
|
||||
public Timer mouseEmulationTimer;
|
||||
public short mouseEmulationLastInputMap;
|
||||
}
|
||||
|
||||
class InputDeviceContext extends GenericControllerContext {
|
||||
@@ -947,12 +1205,14 @@ public class ControllerHandler implements InputManager.InputDeviceListener, UsbD
|
||||
public int leftTriggerAxis = -1;
|
||||
public int rightTriggerAxis = -1;
|
||||
public boolean triggersIdleNegative;
|
||||
public boolean leftTriggerUsed, rightTriggerUsed;
|
||||
|
||||
public int hatXAxis = -1;
|
||||
public int hatYAxis = -1;
|
||||
|
||||
public boolean isDualShock4;
|
||||
public boolean isXboxController;
|
||||
public boolean isXboxBtController;
|
||||
public boolean isServal;
|
||||
public boolean backIsStart;
|
||||
public boolean modeIsSelect;
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package com.limelight.binding.input;
|
||||
|
||||
import android.view.View;
|
||||
|
||||
import com.limelight.nvstream.NvConnection;
|
||||
import com.limelight.nvstream.input.MouseButtonPacket;
|
||||
|
||||
@@ -17,23 +19,27 @@ public class TouchContext {
|
||||
private boolean confirmedDrag;
|
||||
private Timer dragTimer;
|
||||
private double distanceMoved;
|
||||
private double xFactor, yFactor;
|
||||
|
||||
private final NvConnection conn;
|
||||
private final int actionIndex;
|
||||
private final double xFactor;
|
||||
private final double yFactor;
|
||||
private final int referenceWidth;
|
||||
private final int referenceHeight;
|
||||
private final View targetView;
|
||||
|
||||
private static final int TAP_MOVEMENT_THRESHOLD = 20;
|
||||
private static final int TAP_DISTANCE_THRESHOLD = 25;
|
||||
private static final int TAP_TIME_THRESHOLD = 250;
|
||||
private static final int DRAG_TIME_THRESHOLD = 650;
|
||||
|
||||
public TouchContext(NvConnection conn, int actionIndex, double xFactor, double yFactor)
|
||||
public TouchContext(NvConnection conn, int actionIndex,
|
||||
int referenceWidth, int referenceHeight, View view)
|
||||
{
|
||||
this.conn = conn;
|
||||
this.actionIndex = actionIndex;
|
||||
this.xFactor = xFactor;
|
||||
this.yFactor = yFactor;
|
||||
this.referenceWidth = referenceWidth;
|
||||
this.referenceHeight = referenceHeight;
|
||||
this.targetView = view;
|
||||
}
|
||||
|
||||
public int getActionIndex()
|
||||
@@ -68,6 +74,10 @@ public class TouchContext {
|
||||
|
||||
public boolean touchDownEvent(int eventX, int eventY)
|
||||
{
|
||||
// Get the view dimensions to scale inputs on this touch
|
||||
xFactor = referenceWidth / (double)targetView.getWidth();
|
||||
yFactor = referenceHeight / (double)targetView.getHeight();
|
||||
|
||||
originalTouchX = lastTouchX = eventX;
|
||||
originalTouchY = lastTouchY = eventY;
|
||||
originalTouchTime = System.currentTimeMillis();
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
package com.limelight.binding.input.capture;
|
||||
|
||||
import android.annotation.TargetApi;
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.os.Build;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.PointerIcon;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
@TargetApi(Build.VERSION_CODES.N)
|
||||
public class AndroidCaptureProvider extends InputCaptureProvider {
|
||||
private ViewGroup rootViewGroup;
|
||||
private Context context;
|
||||
|
||||
public AndroidCaptureProvider(Activity activity) {
|
||||
this.context = activity;
|
||||
this.rootViewGroup = (ViewGroup) activity.getWindow().getDecorView();
|
||||
}
|
||||
|
||||
public static boolean isCaptureProviderSupported() {
|
||||
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.N;
|
||||
}
|
||||
|
||||
private void setPointerIconOnAllViews(PointerIcon icon) {
|
||||
for (int i = 0; i < rootViewGroup.getChildCount(); i++) {
|
||||
View view = rootViewGroup.getChildAt(i);
|
||||
view.setPointerIcon(icon);
|
||||
}
|
||||
rootViewGroup.setPointerIcon(icon);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void enableCapture() {
|
||||
setPointerIconOnAllViews(PointerIcon.getSystemIcon(context, PointerIcon.TYPE_NULL));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void disableCapture() {
|
||||
setPointerIconOnAllViews(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean eventHasRelativeMouseAxes(MotionEvent event) {
|
||||
return event.getAxisValue(MotionEvent.AXIS_RELATIVE_X) != 0 ||
|
||||
event.getAxisValue(MotionEvent.AXIS_RELATIVE_Y) != 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public float getRelativeAxisX(MotionEvent event) {
|
||||
return event.getAxisValue(MotionEvent.AXIS_RELATIVE_X);
|
||||
}
|
||||
|
||||
@Override
|
||||
public float getRelativeAxisY(MotionEvent event) {
|
||||
return event.getAxisValue(MotionEvent.AXIS_RELATIVE_Y);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package com.limelight.binding.input.capture;
|
||||
|
||||
import android.app.Activity;
|
||||
|
||||
import com.limelight.LimeLog;
|
||||
import com.limelight.binding.input.evdev.EvdevCaptureProvider;
|
||||
import com.limelight.binding.input.evdev.EvdevListener;
|
||||
|
||||
public class InputCaptureManager {
|
||||
public static InputCaptureProvider getInputCaptureProvider(Activity activity, EvdevListener rootListener) {
|
||||
// Shield capture is preferred because it can capture when the cursor is over
|
||||
// the system UI. Android N native capture can only capture over views owned
|
||||
// by the application.
|
||||
if (ShieldCaptureProvider.isCaptureProviderSupported()) {
|
||||
LimeLog.info("Using NVIDIA mouse capture extension");
|
||||
return new ShieldCaptureProvider(activity);
|
||||
}
|
||||
else if (EvdevCaptureProvider.isCaptureProviderSupported()) {
|
||||
LimeLog.info("Using Evdev mouse capture");
|
||||
return new EvdevCaptureProvider(activity, rootListener);
|
||||
}
|
||||
else if (AndroidCaptureProvider.isCaptureProviderSupported()) {
|
||||
LimeLog.info("Using Android N+ native mouse capture");
|
||||
return new AndroidCaptureProvider(activity);
|
||||
}
|
||||
else {
|
||||
LimeLog.info("Mouse capture not available");
|
||||
return new NullCaptureProvider();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package com.limelight.binding.input.capture;
|
||||
|
||||
import android.view.MotionEvent;
|
||||
|
||||
public abstract class InputCaptureProvider {
|
||||
public abstract void enableCapture();
|
||||
public abstract void disableCapture();
|
||||
public void destroy() {}
|
||||
|
||||
public boolean eventHasRelativeMouseAxes(MotionEvent event) {
|
||||
return false;
|
||||
}
|
||||
|
||||
public float getRelativeAxisX(MotionEvent event) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
public float getRelativeAxisY(MotionEvent event) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package com.limelight.binding.input.capture;
|
||||
|
||||
|
||||
public class NullCaptureProvider extends InputCaptureProvider {
|
||||
@Override
|
||||
public void enableCapture() {}
|
||||
|
||||
@Override
|
||||
public void disableCapture() {}
|
||||
}
|
||||
@@ -1,12 +1,10 @@
|
||||
package com.limelight.binding.input;
|
||||
package com.limelight.binding.input.capture;
|
||||
|
||||
|
||||
import android.content.Context;
|
||||
import android.hardware.input.InputManager;
|
||||
import android.view.MotionEvent;
|
||||
|
||||
import com.limelight.LimeLog;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.Method;
|
||||
@@ -18,12 +16,14 @@ import java.lang.reflect.Method;
|
||||
//
|
||||
// http://docs.nvidia.com/gameworks/index.html#technologies/mobile/game_controller_handling_mouse.htm
|
||||
|
||||
public class NvMouseHelper {
|
||||
public class ShieldCaptureProvider extends InputCaptureProvider {
|
||||
private static boolean nvExtensionSupported;
|
||||
private static Method methodSetCursorVisibility;
|
||||
private static int AXIS_RELATIVE_X;
|
||||
private static int AXIS_RELATIVE_Y;
|
||||
|
||||
private Context context;
|
||||
|
||||
static {
|
||||
try {
|
||||
methodSetCursorVisibility = InputManager.class.getMethod("setCursorVisibility", boolean.class);
|
||||
@@ -36,16 +36,19 @@ public class NvMouseHelper {
|
||||
|
||||
nvExtensionSupported = true;
|
||||
} catch (Exception e) {
|
||||
LimeLog.info("NvMouseHelper not supported");
|
||||
nvExtensionSupported = false;
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean setCursorVisibility(Context context, boolean visible) {
|
||||
if (!nvExtensionSupported) {
|
||||
return false;
|
||||
}
|
||||
public ShieldCaptureProvider(Context context) {
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
public static boolean isCaptureProviderSupported() {
|
||||
return nvExtensionSupported;
|
||||
}
|
||||
|
||||
private boolean setCursorVisibility(boolean visible) {
|
||||
try {
|
||||
methodSetCursorVisibility.invoke(context.getSystemService(Context.INPUT_SERVICE), visible);
|
||||
return true;
|
||||
@@ -58,19 +61,29 @@ public class NvMouseHelper {
|
||||
return false;
|
||||
}
|
||||
|
||||
public static boolean eventHasRelativeMouseAxes(MotionEvent event) {
|
||||
if (!nvExtensionSupported) {
|
||||
return false;
|
||||
}
|
||||
@Override
|
||||
public void enableCapture() {
|
||||
setCursorVisibility(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void disableCapture() {
|
||||
setCursorVisibility(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean eventHasRelativeMouseAxes(MotionEvent event) {
|
||||
return event.getAxisValue(AXIS_RELATIVE_X) != 0 ||
|
||||
event.getAxisValue(AXIS_RELATIVE_Y) != 0;
|
||||
}
|
||||
|
||||
public static float getRelativeAxisX(MotionEvent event) {
|
||||
@Override
|
||||
public float getRelativeAxisX(MotionEvent event) {
|
||||
return event.getAxisValue(AXIS_RELATIVE_X);
|
||||
}
|
||||
|
||||
public static float getRelativeAxisY(MotionEvent event) {
|
||||
@Override
|
||||
public float getRelativeAxisY(MotionEvent event) {
|
||||
return event.getAxisValue(AXIS_RELATIVE_Y);
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,5 @@
|
||||
package com.limelight.binding.input.driver;
|
||||
|
||||
import android.hardware.usb.UsbEndpoint;
|
||||
|
||||
import com.limelight.LimeLog;
|
||||
import com.limelight.binding.video.MediaCodecHelper;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
|
||||
public abstract class AbstractController {
|
||||
|
||||
private final int deviceId;
|
||||
|
||||
@@ -114,13 +114,13 @@ public abstract class AbstractXboxController extends AbstractController {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Report that we're added _before_ starting the input thread
|
||||
notifyDeviceAdded();
|
||||
|
||||
// Start listening for controller input
|
||||
inputThread = createInputThread();
|
||||
inputThread.start();
|
||||
|
||||
// Now report we're added
|
||||
notifyDeviceAdded();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -137,11 +137,11 @@ public abstract class AbstractXboxController extends AbstractController {
|
||||
inputThread = null;
|
||||
}
|
||||
|
||||
// Report the device removed
|
||||
notifyDeviceRemoved();
|
||||
|
||||
// Close the USB connection
|
||||
connection.close();
|
||||
|
||||
// Report the device removed
|
||||
notifyDeviceRemoved();
|
||||
}
|
||||
|
||||
protected abstract boolean handleRead(ByteBuffer buffer);
|
||||
|
||||
@@ -72,14 +72,14 @@ public class UsbDriverService extends Service implements UsbDriverListener {
|
||||
|
||||
// Initial attachment broadcast
|
||||
if (action.equals(UsbManager.ACTION_USB_DEVICE_ATTACHED)) {
|
||||
UsbDevice device = (UsbDevice) intent.getParcelableExtra(UsbManager.EXTRA_DEVICE);
|
||||
UsbDevice device = intent.getParcelableExtra(UsbManager.EXTRA_DEVICE);
|
||||
|
||||
// Continue the state machine
|
||||
handleUsbDeviceState(device);
|
||||
}
|
||||
// Subsequent permission dialog completion intent
|
||||
else if (action.equals(ACTION_USB_PERMISSION)) {
|
||||
UsbDevice device = (UsbDevice) intent.getParcelableExtra(UsbManager.EXTRA_DEVICE);
|
||||
UsbDevice device = intent.getParcelableExtra(UsbManager.EXTRA_DEVICE);
|
||||
|
||||
// If we got this far, we've already found we're able to handle this device
|
||||
if (intent.getBooleanExtra(UsbManager.EXTRA_PERMISSION_GRANTED, false)) {
|
||||
|
||||
@@ -10,56 +10,38 @@ import com.limelight.nvstream.input.ControllerPacket;
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
public class Xbox360Controller extends AbstractXboxController {
|
||||
private static final int XB360_IFACE_SUBCLASS = 93;
|
||||
private static final int XB360_IFACE_PROTOCOL = 1; // Wired only
|
||||
|
||||
// This list is taken from the Xpad driver in the Linux kernel.
|
||||
// I've excluded the devices that aren't "controllers" in the traditional sense, but
|
||||
// if people really want to use their dancepads or fight sticks with Moonlight, I can
|
||||
// put them in.
|
||||
private static final DeviceIdTuple[] supportedDeviceTuples = {
|
||||
new DeviceIdTuple(0x045e, 0x028e, "Microsoft X-Box 360 pad"),
|
||||
new DeviceIdTuple(0x044f, 0xb326, "Thrustmaster Gamepad GP XID"),
|
||||
new DeviceIdTuple(0x046d, 0xc21d, "Logitech Gamepad F310"),
|
||||
new DeviceIdTuple(0x046d, 0xc21e, "Logitech Gamepad F510"),
|
||||
new DeviceIdTuple(0x046d, 0xc21f, "Logitech Gamepad F710"),
|
||||
new DeviceIdTuple(0x046d, 0xc242, "Logitech Chillstream Controller"),
|
||||
new DeviceIdTuple(0x0738, 0x4716, "Mad Catz Wired Xbox 360 Controller"),
|
||||
new DeviceIdTuple(0x0738, 0x4726, "Mad Catz Xbox 360 Controller"),
|
||||
new DeviceIdTuple(0x0738, 0xb726, "Mad Catz Xbox controller - MW2"),
|
||||
new DeviceIdTuple(0x0738, 0xbeef, "Mad Catz JOYTECH NEO SE Advanced GamePad"),
|
||||
new DeviceIdTuple(0x0738, 0xcb02, "Saitek Cyborg Rumble Pad - PC/Xbox 360"),
|
||||
new DeviceIdTuple(0x0738, 0xcb03, "Saitek P3200 Rumble Pad - PC/Xbox 360"),
|
||||
new DeviceIdTuple(0x0e6f, 0x0113, "Afterglow AX.1 Gamepad for Xbox 360"),
|
||||
new DeviceIdTuple(0x0e6f, 0x0201, "Pelican PL-3601 'TSZ' Wired Xbox 360 Controller"),
|
||||
new DeviceIdTuple(0x0e6f, 0x0213, "Afterglow Gamepad for Xbox 360"),
|
||||
new DeviceIdTuple(0x0e6f, 0x021f, "Rock Candy Gamepad for Xbox 360"),
|
||||
new DeviceIdTuple(0x0e6f, 0x0301, "Logic3 Controller"),
|
||||
new DeviceIdTuple(0x0e6f, 0x0401, "Logic3 Controller"),
|
||||
new DeviceIdTuple(0x12ab, 0x0301, "PDP AFTERGLOW AX.1"),
|
||||
new DeviceIdTuple(0x146b, 0x0601, "BigBen Interactive XBOX 360 Controller"),
|
||||
new DeviceIdTuple(0x1532, 0x0037, "Razer Sabertooth"),
|
||||
new DeviceIdTuple(0x15e4, 0x3f00, "Power A Mini Pro Elite"),
|
||||
new DeviceIdTuple(0x15e4, 0x3f0a, "Xbox Airflo wired controller"),
|
||||
new DeviceIdTuple(0x15e4, 0x3f10, "Batarang Xbox 360 controller"),
|
||||
new DeviceIdTuple(0x162e, 0xbeef, "Joytech Neo-Se Take2"),
|
||||
new DeviceIdTuple(0x1689, 0xfd00, "Razer Onza Tournament Edition"),
|
||||
new DeviceIdTuple(0x1689, 0xfd01, "Razer Onza Classic Edition"),
|
||||
new DeviceIdTuple(0x24c6, 0x5d04, "Razer Sabertooth"),
|
||||
new DeviceIdTuple(0x1bad, 0xf016, "Mad Catz Xbox 360 Controller"),
|
||||
new DeviceIdTuple(0x1bad, 0xf023, "MLG Pro Circuit Controller (Xbox)"),
|
||||
new DeviceIdTuple(0x1bad, 0xf900, "Harmonix Xbox 360 Controller"),
|
||||
new DeviceIdTuple(0x1bad, 0xf901, "Gamestop Xbox 360 Controller"),
|
||||
new DeviceIdTuple(0x1bad, 0xf903, "Tron Xbox 360 controller"),
|
||||
new DeviceIdTuple(0x24c6, 0x5300, "PowerA MINI PROEX Controller"),
|
||||
new DeviceIdTuple(0x24c6, 0x5303, "Xbox Airflo wired controller"),
|
||||
private static final int[] SUPPORTED_VENDORS = {
|
||||
0x044f, // Thrustmaster
|
||||
0x045e, // Microsoft
|
||||
0x046d, // Logitech
|
||||
0x0738, // Mad Catz
|
||||
0x0e6f, // Unknown
|
||||
0x12ab, // Unknown
|
||||
0x1430, // RedOctane
|
||||
0x146b, // BigBen
|
||||
0x1bad, // Harmonix
|
||||
0x0f0d, // Hori
|
||||
0x1689, // Razer Onza
|
||||
0x24c6, // PowerA
|
||||
0x1532, // Razer Sabertooth
|
||||
0x15e4, // Numark
|
||||
0x162e, // Joytech
|
||||
};
|
||||
|
||||
public static boolean canClaimDevice(UsbDevice device) {
|
||||
for (DeviceIdTuple tuple : supportedDeviceTuples) {
|
||||
if (device.getVendorId() == tuple.vid && device.getProductId() == tuple.pid) {
|
||||
LimeLog.info("XB360 can claim device: " + tuple.name);
|
||||
for (int supportedVid : SUPPORTED_VENDORS) {
|
||||
if (device.getVendorId() == supportedVid &&
|
||||
device.getInterfaceCount() >= 1 &&
|
||||
device.getInterface(0).getInterfaceClass() == UsbConstants.USB_CLASS_VENDOR_SPEC &&
|
||||
device.getInterface(0).getInterfaceSubclass() == XB360_IFACE_SUBCLASS &&
|
||||
device.getInterface(0).getInterfaceProtocol() == XB360_IFACE_PROTOCOL) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -151,16 +133,4 @@ public class Xbox360Controller extends AbstractXboxController {
|
||||
// No need to fail init if the LED command fails
|
||||
return true;
|
||||
}
|
||||
|
||||
private static class DeviceIdTuple {
|
||||
public final int vid;
|
||||
public final int pid;
|
||||
public final String name;
|
||||
|
||||
public DeviceIdTuple(int vid, int pid, String name) {
|
||||
this.vid = vid;
|
||||
this.pid = pid;
|
||||
this.name = name;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,22 +3,25 @@ package com.limelight.binding.input.driver;
|
||||
import android.hardware.usb.UsbConstants;
|
||||
import android.hardware.usb.UsbDevice;
|
||||
import android.hardware.usb.UsbDeviceConnection;
|
||||
import android.hardware.usb.UsbEndpoint;
|
||||
import android.hardware.usb.UsbInterface;
|
||||
|
||||
import com.limelight.LimeLog;
|
||||
import com.limelight.binding.video.MediaCodecHelper;
|
||||
import com.limelight.nvstream.input.ControllerPacket;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
|
||||
public class XboxOneController extends AbstractXboxController {
|
||||
|
||||
private static final int MICROSOFT_VID = 0x045e;
|
||||
private static final int XB1_IFACE_SUBCLASS = 71;
|
||||
private static final int XB1_IFACE_PROTOCOL = 208;
|
||||
|
||||
private static final int[] SUPPORTED_VENDORS = {
|
||||
0x045e, // Microsoft
|
||||
0x0738, // Mad Catz
|
||||
0x0e6f, // Unknown
|
||||
0x0f0d, // Hori
|
||||
0x24c6, // PowerA
|
||||
};
|
||||
|
||||
// FIXME: odata_serial
|
||||
private static final byte[] XB1_INIT_DATA = {0x05, 0x20, 0x00, 0x01, 0x00};
|
||||
|
||||
@@ -59,6 +62,12 @@ public class XboxOneController extends AbstractXboxController {
|
||||
rightStickY = ~buffer.getShort() / 32767.0f;
|
||||
}
|
||||
|
||||
private void ackModeReport(byte seqNum) {
|
||||
byte[] payload = {0x01, 0x20, seqNum, 0x09, 0x00, 0x07, 0x20, 0x02,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00};
|
||||
connection.bulkTransfer(outEndpt, payload, payload.length, 3000);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean handleRead(ByteBuffer buffer) {
|
||||
switch (buffer.get())
|
||||
@@ -69,7 +78,15 @@ public class XboxOneController extends AbstractXboxController {
|
||||
return true;
|
||||
|
||||
case 0x07:
|
||||
buffer.position(buffer.position() + 3);
|
||||
// The Xbox One S controller needs acks for mode reports otherwise
|
||||
// it retransmits them forever.
|
||||
if (buffer.get() == 0x30) {
|
||||
ackModeReport(buffer.get());
|
||||
buffer.position(buffer.position() + 1);
|
||||
}
|
||||
else {
|
||||
buffer.position(buffer.position() + 2);
|
||||
}
|
||||
setButtonFlag(ControllerPacket.SPECIAL_BUTTON_FLAG, buffer.get() & 0x01);
|
||||
return true;
|
||||
}
|
||||
@@ -78,11 +95,17 @@ public class XboxOneController extends AbstractXboxController {
|
||||
}
|
||||
|
||||
public static boolean canClaimDevice(UsbDevice device) {
|
||||
return device.getVendorId() == MICROSOFT_VID &&
|
||||
device.getInterfaceCount() >= 1 &&
|
||||
device.getInterface(0).getInterfaceClass() == UsbConstants.USB_CLASS_VENDOR_SPEC &&
|
||||
device.getInterface(0).getInterfaceSubclass() == XB1_IFACE_SUBCLASS &&
|
||||
device.getInterface(0).getInterfaceProtocol() == XB1_IFACE_PROTOCOL;
|
||||
for (int supportedVid : SUPPORTED_VENDORS) {
|
||||
if (device.getVendorId() == supportedVid &&
|
||||
device.getInterfaceCount() >= 1 &&
|
||||
device.getInterface(0).getInterfaceClass() == UsbConstants.USB_CLASS_VENDOR_SPEC &&
|
||||
device.getInterface(0).getInterfaceSubclass() == XB1_IFACE_SUBCLASS &&
|
||||
device.getInterface(0).getInterfaceProtocol() == XB1_IFACE_PROTOCOL) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
package com.limelight.binding.input.evdev;
|
||||
|
||||
import android.content.Context;
|
||||
import android.app.Activity;
|
||||
import android.os.Build;
|
||||
import android.widget.Toast;
|
||||
|
||||
import com.limelight.LimeLog;
|
||||
import com.limelight.LimelightBuildProps;
|
||||
import com.limelight.binding.input.capture.InputCaptureProvider;
|
||||
|
||||
import java.io.DataOutputStream;
|
||||
import java.io.File;
|
||||
@@ -12,7 +16,7 @@ import java.io.OutputStream;
|
||||
import java.net.ServerSocket;
|
||||
import java.net.Socket;
|
||||
|
||||
public class EvdevHandler {
|
||||
public class EvdevCaptureProvider extends InputCaptureProvider {
|
||||
|
||||
private final EvdevListener listener;
|
||||
private final String libraryPath;
|
||||
@@ -23,6 +27,8 @@ public class EvdevHandler {
|
||||
private Process su;
|
||||
private ServerSocket servSock;
|
||||
private Socket evdevSock;
|
||||
private Activity activity;
|
||||
private boolean started = false;
|
||||
|
||||
private static final byte UNGRAB_REQUEST = 1;
|
||||
private static final byte REGRAB_REQUEST = 2;
|
||||
@@ -42,24 +48,42 @@ public class EvdevHandler {
|
||||
return;
|
||||
}
|
||||
|
||||
// Launch a su shell
|
||||
ProcessBuilder builder = new ProcessBuilder("su");
|
||||
builder.redirectErrorStream(true);
|
||||
final String evdevReaderCmd = libraryPath+File.separatorChar+"libevdev_reader.so "+servSock.getLocalPort();
|
||||
|
||||
try {
|
||||
su = builder.start();
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
return;
|
||||
// On Nougat and later, we'll need to pass the command directly to SU.
|
||||
// Writing to SU's input stream after it has started doesn't seem to work anymore.
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
// Launch evdev_reader directly via SU
|
||||
try {
|
||||
su = Runtime.getRuntime().exec("su -c "+evdevReaderCmd);
|
||||
} catch (IOException e) {
|
||||
reportDeviceNotRooted();
|
||||
e.printStackTrace();
|
||||
return;
|
||||
}
|
||||
}
|
||||
else {
|
||||
// Launch a SU shell on Marshmallow and earlier
|
||||
ProcessBuilder builder = new ProcessBuilder("su");
|
||||
builder.redirectErrorStream(true);
|
||||
|
||||
// Start evdevreader
|
||||
DataOutputStream suOut = new DataOutputStream(su.getOutputStream());
|
||||
try {
|
||||
suOut.writeChars(libraryPath+File.separatorChar+"libevdev_reader.so "+servSock.getLocalPort()+"\n");
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
return;
|
||||
try {
|
||||
su = builder.start();
|
||||
} catch (IOException e) {
|
||||
reportDeviceNotRooted();
|
||||
e.printStackTrace();
|
||||
return;
|
||||
}
|
||||
|
||||
// Start evdevreader
|
||||
DataOutputStream suOut = new DataOutputStream(su.getOutputStream());
|
||||
try {
|
||||
suOut.writeChars(evdevReaderCmd+"\n");
|
||||
} catch (IOException e) {
|
||||
reportDeviceNotRooted();
|
||||
e.printStackTrace();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Wait for evdevreader's connection
|
||||
@@ -155,23 +179,48 @@ public class EvdevHandler {
|
||||
}
|
||||
};
|
||||
|
||||
public EvdevHandler(Context context, EvdevListener listener) {
|
||||
public EvdevCaptureProvider(Activity activity, EvdevListener listener) {
|
||||
this.listener = listener;
|
||||
this.libraryPath = context.getApplicationInfo().nativeLibraryDir;
|
||||
this.activity = activity;
|
||||
this.libraryPath = activity.getApplicationInfo().nativeLibraryDir;
|
||||
}
|
||||
|
||||
public void regrabAll() {
|
||||
if (!shutdown && evdevOut != null) {
|
||||
try {
|
||||
evdevOut.write(REGRAB_REQUEST);
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
public static boolean isCaptureProviderSupported() {
|
||||
return LimelightBuildProps.ROOT_BUILD;
|
||||
}
|
||||
|
||||
private void reportDeviceNotRooted() {
|
||||
activity.runOnUiThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
Toast.makeText(activity, "This device is not rooted - Mouse capture is unavailable", Toast.LENGTH_LONG).show();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void enableCapture() {
|
||||
if (!started) {
|
||||
// Start the handler thread if it's our first time
|
||||
// capturing
|
||||
handlerThread.start();
|
||||
started = true;
|
||||
}
|
||||
else {
|
||||
// Send a request to regrab if we're already capturing
|
||||
if (!shutdown && evdevOut != null) {
|
||||
try {
|
||||
evdevOut.write(REGRAB_REQUEST);
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void ungrabAll() {
|
||||
if (!shutdown && evdevOut != null) {
|
||||
@Override
|
||||
public void disableCapture() {
|
||||
if (started && !shutdown && evdevOut != null) {
|
||||
try {
|
||||
evdevOut.write(UNGRAB_REQUEST);
|
||||
} catch (IOException e) {
|
||||
@@ -180,15 +229,16 @@ public class EvdevHandler {
|
||||
}
|
||||
}
|
||||
|
||||
public void start() {
|
||||
handlerThread.start();
|
||||
}
|
||||
|
||||
public void stop() {
|
||||
@Override
|
||||
public void destroy() {
|
||||
// We need to stop the process in this context otherwise
|
||||
// we could get stuck waiting on output from the process
|
||||
// in order to terminate it.
|
||||
|
||||
if (!started) {
|
||||
return;
|
||||
}
|
||||
|
||||
shutdown = true;
|
||||
handlerThread.interrupt();
|
||||
|
||||
@@ -52,7 +52,7 @@ public class DigitalButton extends VirtualControllerElement {
|
||||
}
|
||||
}
|
||||
|
||||
private List<DigitalButtonListener> listeners = new ArrayList<DigitalButtonListener>();
|
||||
private List<DigitalButtonListener> listeners = new ArrayList<>();
|
||||
private String text = "";
|
||||
private int icon = -1;
|
||||
private long timerLongClickTimeout = 3000;
|
||||
|
||||
@@ -20,7 +20,7 @@ public class DigitalPad extends VirtualControllerElement {
|
||||
public final static int DIGITAL_PAD_DIRECTION_UP = 2;
|
||||
public final static int DIGITAL_PAD_DIRECTION_RIGHT = 4;
|
||||
public final static int DIGITAL_PAD_DIRECTION_DOWN = 8;
|
||||
List<DigitalPadListener> listeners = new ArrayList<DigitalPadListener>();
|
||||
List<DigitalPadListener> listeners = new ArrayList<>();
|
||||
|
||||
private static final int DPAD_MARGIN = 5;
|
||||
|
||||
|
||||
@@ -48,7 +48,7 @@ public class VirtualController {
|
||||
private RelativeLayout.LayoutParams layoutParamsButtonConfigure = null;
|
||||
private Button buttonConfigure = null;
|
||||
|
||||
private List<VirtualControllerElement> elements = new ArrayList<VirtualControllerElement>();
|
||||
private List<VirtualControllerElement> elements = new ArrayList<>();
|
||||
|
||||
public VirtualController(final NvConnection conn, FrameLayout layout, final Context context) {
|
||||
this.connection = conn;
|
||||
@@ -60,7 +60,7 @@ public class VirtualController {
|
||||
frame_layout.addView(relative_layout);
|
||||
|
||||
buttonConfigure = new Button(context);
|
||||
buttonConfigure.setBackgroundResource(R.drawable.settings);
|
||||
buttonConfigure.setBackgroundResource(R.drawable.ic_settings);
|
||||
buttonConfigure.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
|
||||
@@ -25,6 +25,8 @@ import android.view.SurfaceHolder;
|
||||
|
||||
public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer {
|
||||
|
||||
private static final boolean USE_FRAME_RENDER_TIME = false;
|
||||
|
||||
// Used on versions < 5.0
|
||||
private ByteBuffer[] legacyInputBuffers;
|
||||
|
||||
@@ -202,9 +204,31 @@ public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer {
|
||||
videoFormat.setInteger(MediaFormat.KEY_MAX_HEIGHT, height);
|
||||
}
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
// Operate at maximum rate to lower latency as much as possible on
|
||||
// some Qualcomm platforms. We could also set KEY_PRIORITY to 0 (realtime)
|
||||
// but that will actually result in the decoder crashing if it can't satisfy
|
||||
// our (ludicrous) operating rate requirement.
|
||||
videoFormat.setInteger(MediaFormat.KEY_OPERATING_RATE, Short.MAX_VALUE);
|
||||
}
|
||||
|
||||
videoDecoder.configure(videoFormat, ((SurfaceHolder)renderTarget).getSurface(), null, 0);
|
||||
videoDecoder.setVideoScalingMode(MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT);
|
||||
|
||||
if (USE_FRAME_RENDER_TIME && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
videoDecoder.setOnFrameRenderedListener(new MediaCodec.OnFrameRenderedListener() {
|
||||
@Override
|
||||
public void onFrameRendered(MediaCodec mediaCodec, long presentationTimeUs, long renderTimeNanos) {
|
||||
long delta = (renderTimeNanos / 1000000L) - (presentationTimeUs / 1000);
|
||||
if (delta >= 0 && delta < 1000) {
|
||||
if (USE_FRAME_RENDER_TIME) {
|
||||
totalTimeMs += delta;
|
||||
}
|
||||
}
|
||||
}
|
||||
}, null);
|
||||
}
|
||||
|
||||
LimeLog.info("Using codec "+selectedDecoderName+" for hardware decoding "+mimeType);
|
||||
|
||||
return true;
|
||||
@@ -265,7 +289,9 @@ public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer {
|
||||
long delta = MediaCodecHelper.getMonotonicMillis() - (presentationTimeUs / 1000);
|
||||
if (delta >= 0 && delta < 1000) {
|
||||
decoderTimeMs += delta;
|
||||
totalTimeMs += delta;
|
||||
if (!USE_FRAME_RENDER_TIME) {
|
||||
totalTimeMs += delta;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
switch (outIndex) {
|
||||
@@ -413,7 +439,9 @@ public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer {
|
||||
long delta = MediaCodecHelper.getMonotonicMillis()-(presentationTimeUs/1000);
|
||||
if (delta >= 0 && delta < 1000) {
|
||||
decoderTimeMs += delta;
|
||||
totalTimeMs += delta;
|
||||
if (!USE_FRAME_RENDER_TIME) {
|
||||
totalTimeMs += delta;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
switch (outIndex) {
|
||||
@@ -475,8 +503,8 @@ public class MediaCodecDecoderRenderer extends EnhancedDecoderRenderer {
|
||||
} catch (InterruptedException ignored) { }
|
||||
}
|
||||
|
||||
// Stop the decoder
|
||||
videoDecoder.stop();
|
||||
// We could stop the decoder here, but it seems to cause some problems
|
||||
// so we'll just let release take care of it.
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -34,7 +34,7 @@ public class MediaCodecHelper {
|
||||
private static final List<String> whitelistedHevcDecoders;
|
||||
|
||||
static {
|
||||
directSubmitPrefixes = new LinkedList<String>();
|
||||
directSubmitPrefixes = new LinkedList<>();
|
||||
|
||||
// These decoders have low enough input buffer latency that they
|
||||
// can be directly invoked from the receive thread
|
||||
@@ -48,33 +48,38 @@ public class MediaCodecHelper {
|
||||
}
|
||||
|
||||
static {
|
||||
preferredDecoders = new LinkedList<String>();
|
||||
preferredDecoders = new LinkedList<>();
|
||||
}
|
||||
|
||||
static {
|
||||
blacklistedDecoderPrefixes = new LinkedList<String>();
|
||||
blacklistedDecoderPrefixes = new LinkedList<>();
|
||||
|
||||
// Software decoders that don't support H264 high profile
|
||||
blacklistedDecoderPrefixes.add("omx.google");
|
||||
blacklistedDecoderPrefixes.add("AVCDecoder");
|
||||
|
||||
// Without bitstream fixups, we perform horribly on NVIDIA's HEVC
|
||||
// decoder. While not strictly necessary, I'm going to fully blacklist this
|
||||
// one to avoid users getting inaccurate impressions of Tegra X1/Moonlight performance.
|
||||
blacklistedDecoderPrefixes.add("OMX.Nvidia.h265.decode");
|
||||
}
|
||||
|
||||
static {
|
||||
spsFixupBitstreamFixupDecoderPrefixes = new LinkedList<String>();
|
||||
spsFixupBitstreamFixupDecoderPrefixes = new LinkedList<>();
|
||||
spsFixupBitstreamFixupDecoderPrefixes.add("omx.nvidia");
|
||||
spsFixupBitstreamFixupDecoderPrefixes.add("omx.qcom");
|
||||
spsFixupBitstreamFixupDecoderPrefixes.add("omx.brcm");
|
||||
|
||||
baselineProfileHackPrefixes = new LinkedList<String>();
|
||||
baselineProfileHackPrefixes = new LinkedList<>();
|
||||
baselineProfileHackPrefixes.add("omx.intel");
|
||||
|
||||
whitelistedAdaptiveResolutionPrefixes = new LinkedList<String>();
|
||||
whitelistedAdaptiveResolutionPrefixes = new LinkedList<>();
|
||||
whitelistedAdaptiveResolutionPrefixes.add("omx.nvidia");
|
||||
whitelistedAdaptiveResolutionPrefixes.add("omx.qcom");
|
||||
whitelistedAdaptiveResolutionPrefixes.add("omx.sec");
|
||||
whitelistedAdaptiveResolutionPrefixes.add("omx.TI");
|
||||
|
||||
constrainedHighProfilePrefixes = new LinkedList<String>();
|
||||
constrainedHighProfilePrefixes = new LinkedList<>();
|
||||
constrainedHighProfilePrefixes.add("omx.intel");
|
||||
}
|
||||
|
||||
@@ -213,7 +218,7 @@ public class MediaCodecHelper {
|
||||
@SuppressWarnings("deprecation")
|
||||
@SuppressLint("NewApi")
|
||||
private static LinkedList<MediaCodecInfo> getMediaCodecList() {
|
||||
LinkedList<MediaCodecInfo> infoList = new LinkedList<MediaCodecInfo>();
|
||||
LinkedList<MediaCodecInfo> infoList = new LinkedList<>();
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
MediaCodecList mcl = new MediaCodecList(MediaCodecList.REGULAR_CODECS);
|
||||
@@ -371,7 +376,7 @@ public class MediaCodecHelper {
|
||||
break;
|
||||
cpuInfo.append((char)ch);
|
||||
}
|
||||
|
||||
|
||||
return cpuInfo.toString();
|
||||
} finally {
|
||||
br.close();
|
||||
|
||||
@@ -68,7 +68,7 @@ public class ComputerDatabaseManager {
|
||||
|
||||
public List<ComputerDetails> getAllComputers() {
|
||||
Cursor c = computerDb.rawQuery("SELECT * FROM "+COMPUTER_TABLE_NAME, null);
|
||||
LinkedList<ComputerDetails> computerList = new LinkedList<ComputerDetails>();
|
||||
LinkedList<ComputerDetails> computerList = new LinkedList<>();
|
||||
while (c.moveToNext()) {
|
||||
ComputerDetails details = new ComputerDetails();
|
||||
|
||||
|
||||
@@ -37,7 +37,9 @@ public class ComputerManagerService extends Service {
|
||||
private static final int MDNS_QUERY_PERIOD_MS = 1000;
|
||||
private static final int FAST_POLL_TIMEOUT = 500;
|
||||
private static final int OFFLINE_POLL_TRIES = 5;
|
||||
private static final int INITIAL_POLL_TRIES = 2;
|
||||
private static final int EMPTY_LIST_THRESHOLD = 3;
|
||||
private static final int POLL_DATA_TTL_MS = 30000;
|
||||
|
||||
private final ComputerManagerBinder binder = new ComputerManagerBinder();
|
||||
|
||||
@@ -45,7 +47,7 @@ public class ComputerManagerService extends Service {
|
||||
private final AtomicInteger dbRefCount = new AtomicInteger(0);
|
||||
|
||||
private IdentityManager idManager;
|
||||
private final LinkedList<PollingTuple> pollingTuples = new LinkedList<PollingTuple>();
|
||||
private final LinkedList<PollingTuple> pollingTuples = new LinkedList<>();
|
||||
private ComputerManagerListener listener = null;
|
||||
private final AtomicInteger activePolls = new AtomicInteger(0);
|
||||
private boolean pollingActive = false;
|
||||
@@ -76,12 +78,15 @@ public class ComputerManagerService extends Service {
|
||||
return false;
|
||||
}
|
||||
|
||||
final int pollTriesBeforeOffline = details.state == ComputerDetails.State.UNKNOWN ?
|
||||
INITIAL_POLL_TRIES : OFFLINE_POLL_TRIES;
|
||||
|
||||
activePolls.incrementAndGet();
|
||||
|
||||
// Poll the machine
|
||||
try {
|
||||
if (!pollComputer(details)) {
|
||||
if (!newPc && offlineCount < OFFLINE_POLL_TRIES) {
|
||||
if (!newPc && offlineCount < pollTriesBeforeOffline) {
|
||||
// Return without calling the listener
|
||||
releaseLocalDatabaseReference();
|
||||
return false;
|
||||
@@ -136,6 +141,7 @@ public class ComputerManagerService extends Service {
|
||||
LimeLog.warning(tuple.computer.name + " is offline (try " + offlineCount + ")");
|
||||
offlineCount++;
|
||||
} else {
|
||||
tuple.lastSuccessfulPollMs = System.currentTimeMillis();
|
||||
offlineCount = 0;
|
||||
}
|
||||
}
|
||||
@@ -165,11 +171,18 @@ public class ComputerManagerService extends Service {
|
||||
|
||||
synchronized (pollingTuples) {
|
||||
for (PollingTuple tuple : pollingTuples) {
|
||||
// Enforce the poll data TTL
|
||||
if (System.currentTimeMillis() - tuple.lastSuccessfulPollMs > POLL_DATA_TTL_MS) {
|
||||
LimeLog.info("Timing out polled state for "+tuple.computer.name);
|
||||
tuple.computer.state = ComputerDetails.State.UNKNOWN;
|
||||
tuple.computer.reachability = ComputerDetails.Reachability.UNKNOWN;
|
||||
}
|
||||
|
||||
// Report this computer initially
|
||||
listener.notifyComputerUpdated(tuple.computer);
|
||||
|
||||
// This polling thread might already be there
|
||||
if (tuple.thread == null) {
|
||||
// Report this computer initially
|
||||
listener.notifyComputerUpdated(tuple.computer);
|
||||
|
||||
tuple.thread = createPollingThread(tuple);
|
||||
tuple.thread.start();
|
||||
}
|
||||
@@ -229,6 +242,21 @@ public class ComputerManagerService extends Service {
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public void invalidateStateForComputer(UUID uuid) {
|
||||
synchronized (pollingTuples) {
|
||||
for (PollingTuple tuple : pollingTuples) {
|
||||
if (uuid.equals(tuple.computer.uuid)) {
|
||||
// We need the network lock to prevent a concurrent poll
|
||||
// from wiping this change out
|
||||
synchronized (tuple.networkLock) {
|
||||
tuple.computer.state = ComputerDetails.State.UNKNOWN;
|
||||
tuple.computer.reachability = ComputerDetails.Reachability.UNKNOWN;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -778,6 +806,7 @@ class PollingTuple {
|
||||
public Thread thread;
|
||||
public final ComputerDetails computer;
|
||||
public final Object networkLock;
|
||||
public long lastSuccessfulPollMs;
|
||||
|
||||
public PollingTuple(ComputerDetails computer, Thread thread) {
|
||||
this.computer = computer;
|
||||
|
||||
@@ -3,6 +3,7 @@ package com.limelight.grid;
|
||||
import android.app.Activity;
|
||||
import android.graphics.BitmapFactory;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.ProgressBar;
|
||||
import android.widget.TextView;
|
||||
|
||||
import com.limelight.AppView;
|
||||
@@ -14,8 +15,6 @@ import com.limelight.grid.assets.MemoryAssetLoader;
|
||||
import com.limelight.grid.assets.NetworkAssetLoader;
|
||||
import com.limelight.nvstream.http.ComputerDetails;
|
||||
|
||||
import java.security.KeyManagementException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
|
||||
@@ -28,7 +27,7 @@ public class AppGridAdapter extends GenericGridAdapter<AppView.AppObject> {
|
||||
private final CachedAppAssetLoader loader;
|
||||
|
||||
public AppGridAdapter(Activity activity, boolean listMode, boolean small, ComputerDetails computer, String uniqueId) {
|
||||
super(activity, listMode ? R.layout.simple_row : (small ? R.layout.app_grid_item_small : R.layout.app_grid_item), R.drawable.image_loading);
|
||||
super(activity, listMode ? R.layout.simple_row : (small ? R.layout.app_grid_item_small : R.layout.app_grid_item));
|
||||
|
||||
int dpi = activity.getResources().getDisplayMetrics().densityDpi;
|
||||
int dp;
|
||||
@@ -53,9 +52,7 @@ public class AppGridAdapter extends GenericGridAdapter<AppView.AppObject> {
|
||||
this.loader = new CachedAppAssetLoader(computer, scalingDivisor,
|
||||
new NetworkAssetLoader(context, uniqueId),
|
||||
new MemoryAssetLoader(),
|
||||
new DiskAssetLoader(context.getCacheDir()),
|
||||
BitmapFactory.decodeResource(activity.getResources(),
|
||||
R.drawable.image_loading, options));
|
||||
new DiskAssetLoader(context.getCacheDir()));
|
||||
}
|
||||
|
||||
public void cancelQueuedOperations() {
|
||||
@@ -68,7 +65,7 @@ public class AppGridAdapter extends GenericGridAdapter<AppView.AppObject> {
|
||||
Collections.sort(itemList, new Comparator<AppView.AppObject>() {
|
||||
@Override
|
||||
public int compare(AppView.AppObject lhs, AppView.AppObject rhs) {
|
||||
return lhs.app.getAppName().compareTo(rhs.app.getAppName());
|
||||
return lhs.app.getAppName().toLowerCase().compareTo(rhs.app.getAppName().toLowerCase());
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -86,9 +83,10 @@ public class AppGridAdapter extends GenericGridAdapter<AppView.AppObject> {
|
||||
itemList.remove(app);
|
||||
}
|
||||
|
||||
public boolean populateImageView(ImageView imgView, AppView.AppObject obj) {
|
||||
@Override
|
||||
public boolean populateImageView(ImageView imgView, ProgressBar prgView, AppView.AppObject obj) {
|
||||
// Let the cached asset loader handle it
|
||||
loader.populateImageView(obj.app, imgView);
|
||||
loader.populateImageView(obj.app, imgView, prgView);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -103,9 +101,9 @@ public class AppGridAdapter extends GenericGridAdapter<AppView.AppObject> {
|
||||
|
||||
@Override
|
||||
public boolean populateOverlayView(ImageView overlayView, AppView.AppObject obj) {
|
||||
if (obj.app.getIsRunning()) {
|
||||
if (obj.isRunning) {
|
||||
// Show the play button overlay
|
||||
overlayView.setImageResource(R.drawable.play);
|
||||
overlayView.setImageResource(R.drawable.ic_play);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.BaseAdapter;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.ProgressBar;
|
||||
import android.widget.TextView;
|
||||
|
||||
import com.limelight.R;
|
||||
@@ -14,15 +15,13 @@ import java.util.ArrayList;
|
||||
|
||||
public abstract class GenericGridAdapter<T> extends BaseAdapter {
|
||||
protected final Context context;
|
||||
protected final int defaultImageRes;
|
||||
protected final int layoutId;
|
||||
protected final ArrayList<T> itemList = new ArrayList<T>();
|
||||
protected final ArrayList<T> itemList = new ArrayList<>();
|
||||
protected final LayoutInflater inflater;
|
||||
|
||||
public GenericGridAdapter(Context context, int layoutId, int defaultImageRes) {
|
||||
public GenericGridAdapter(Context context, int layoutId) {
|
||||
this.context = context;
|
||||
this.layoutId = layoutId;
|
||||
this.defaultImageRes = defaultImageRes;
|
||||
|
||||
this.inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
|
||||
}
|
||||
@@ -46,7 +45,7 @@ public abstract class GenericGridAdapter<T> extends BaseAdapter {
|
||||
return i;
|
||||
}
|
||||
|
||||
public abstract boolean populateImageView(ImageView imgView, T obj);
|
||||
public abstract boolean populateImageView(ImageView imgView, ProgressBar prgView, T obj);
|
||||
public abstract boolean populateTextView(TextView txtView, T obj);
|
||||
public abstract boolean populateOverlayView(ImageView overlayView, T obj);
|
||||
|
||||
@@ -59,10 +58,11 @@ public abstract class GenericGridAdapter<T> extends BaseAdapter {
|
||||
ImageView imgView = (ImageView) convertView.findViewById(R.id.grid_image);
|
||||
ImageView overlayView = (ImageView) convertView.findViewById(R.id.grid_overlay);
|
||||
TextView txtView = (TextView) convertView.findViewById(R.id.grid_text);
|
||||
ProgressBar prgView = (ProgressBar) convertView.findViewById(R.id.grid_spinner);
|
||||
|
||||
if (imgView != null) {
|
||||
if (!populateImageView(imgView, itemList.get(i))) {
|
||||
imgView.setImageResource(defaultImageRes);
|
||||
if (!populateImageView(imgView, prgView, itemList.get(i))) {
|
||||
imgView.setImageBitmap(null);
|
||||
}
|
||||
}
|
||||
if (!populateTextView(txtView, itemList.get(i))) {
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
package com.limelight.grid;
|
||||
|
||||
import android.content.Context;
|
||||
import android.view.View;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.ProgressBar;
|
||||
import android.widget.TextView;
|
||||
|
||||
import com.limelight.PcView;
|
||||
@@ -14,7 +16,7 @@ import java.util.Comparator;
|
||||
public class PcGridAdapter extends GenericGridAdapter<PcView.ComputerObject> {
|
||||
|
||||
public PcGridAdapter(Context context, boolean listMode, boolean small) {
|
||||
super(context, listMode ? R.layout.simple_row : (small ? R.layout.pc_grid_item_small : R.layout.pc_grid_item), R.drawable.computer);
|
||||
super(context, listMode ? R.layout.simple_row : (small ? R.layout.pc_grid_item_small : R.layout.pc_grid_item));
|
||||
}
|
||||
|
||||
public void addComputer(PcView.ComputerObject computer) {
|
||||
@@ -26,7 +28,7 @@ public class PcGridAdapter extends GenericGridAdapter<PcView.ComputerObject> {
|
||||
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);
|
||||
return lhs.details.name.toLowerCase().compareTo(rhs.details.name.toLowerCase());
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -36,21 +38,28 @@ public class PcGridAdapter extends GenericGridAdapter<PcView.ComputerObject> {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean populateImageView(ImageView imgView, PcView.ComputerObject obj) {
|
||||
if (obj.details.reachability != ComputerDetails.Reachability.OFFLINE) {
|
||||
public boolean populateImageView(ImageView imgView, ProgressBar prgView, PcView.ComputerObject obj) {
|
||||
if (obj.details.state == ComputerDetails.State.ONLINE) {
|
||||
imgView.setAlpha(1.0f);
|
||||
}
|
||||
else {
|
||||
imgView.setAlpha(0.4f);
|
||||
}
|
||||
|
||||
// Return false to use the default drawable
|
||||
return false;
|
||||
if (obj.details.reachability == ComputerDetails.Reachability.UNKNOWN) {
|
||||
prgView.setVisibility(View.VISIBLE);
|
||||
}
|
||||
else {
|
||||
prgView.setVisibility(View.INVISIBLE);
|
||||
}
|
||||
|
||||
imgView.setImageResource(R.drawable.ic_computer);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean populateTextView(TextView txtView, PcView.ComputerObject obj) {
|
||||
if (obj.details.reachability != ComputerDetails.Reachability.OFFLINE) {
|
||||
if (obj.details.state == ComputerDetails.State.ONLINE) {
|
||||
txtView.setAlpha(1.0f);
|
||||
}
|
||||
else {
|
||||
@@ -63,13 +72,11 @@ public class PcGridAdapter extends GenericGridAdapter<PcView.ComputerObject> {
|
||||
|
||||
@Override
|
||||
public boolean populateOverlayView(ImageView overlayView, PcView.ComputerObject obj) {
|
||||
if (obj.details.reachability == ComputerDetails.Reachability.UNKNOWN) {
|
||||
// Still refreshing this PC so display the overlay
|
||||
overlayView.setImageResource(R.drawable.image_loading);
|
||||
if (obj.details.state == ComputerDetails.State.OFFLINE) {
|
||||
overlayView.setImageResource(R.drawable.ic_pc_offline);
|
||||
overlayView.setAlpha(0.4f);
|
||||
return true;
|
||||
}
|
||||
|
||||
// No overlay
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,9 @@ import android.graphics.Bitmap;
|
||||
import android.graphics.drawable.BitmapDrawable;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.os.AsyncTask;
|
||||
import android.view.View;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.ProgressBar;
|
||||
|
||||
import com.limelight.nvstream.http.ComputerDetails;
|
||||
import com.limelight.nvstream.http.NvApp;
|
||||
@@ -53,13 +55,13 @@ public class CachedAppAssetLoader {
|
||||
|
||||
public CachedAppAssetLoader(ComputerDetails computer, double scalingDivider,
|
||||
NetworkAssetLoader networkLoader, MemoryAssetLoader memoryLoader,
|
||||
DiskAssetLoader diskLoader, Bitmap placeholderBitmap) {
|
||||
DiskAssetLoader diskLoader) {
|
||||
this.computer = computer;
|
||||
this.scalingDivider = scalingDivider;
|
||||
this.networkLoader = networkLoader;
|
||||
this.memoryLoader = memoryLoader;
|
||||
this.diskLoader = diskLoader;
|
||||
this.placeholderBitmap = placeholderBitmap;
|
||||
this.placeholderBitmap = Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888);
|
||||
}
|
||||
|
||||
public void cancelBackgroundLoads() {
|
||||
@@ -130,12 +132,14 @@ public class CachedAppAssetLoader {
|
||||
|
||||
private class LoaderTask extends AsyncTask<LoaderTuple, Void, Bitmap> {
|
||||
private final WeakReference<ImageView> imageViewRef;
|
||||
private final WeakReference<ProgressBar> progressViewRef;
|
||||
private final boolean diskOnly;
|
||||
|
||||
private LoaderTuple tuple;
|
||||
|
||||
public LoaderTask(ImageView imageView, boolean diskOnly) {
|
||||
this.imageViewRef = new WeakReference<ImageView>(imageView);
|
||||
public LoaderTask(ImageView imageView, ProgressBar prgView, boolean diskOnly) {
|
||||
this.imageViewRef = new WeakReference<>(imageView);
|
||||
this.progressViewRef = new WeakReference<>(prgView);
|
||||
this.diskOnly = diskOnly;
|
||||
}
|
||||
|
||||
@@ -143,8 +147,8 @@ public class CachedAppAssetLoader {
|
||||
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) {
|
||||
// Check whether it has been cancelled or the views are gone
|
||||
if (isCancelled() || imageViewRef.get() == null || progressViewRef.get() == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -177,11 +181,17 @@ public class CachedAppAssetLoader {
|
||||
|
||||
// If the current loader task for this view isn't us, do nothing
|
||||
final ImageView imageView = imageViewRef.get();
|
||||
final ProgressBar prgView = progressViewRef.get();
|
||||
if (getLoaderTask(imageView) == this) {
|
||||
// Now display the progress bar since we have to hit the network
|
||||
if (prgView != null) {
|
||||
prgView.setVisibility(View.VISIBLE);
|
||||
}
|
||||
|
||||
// Set off another loader task on the network executor
|
||||
LoaderTask task = new LoaderTask(imageView, false);
|
||||
LoaderTask task = new LoaderTask(imageView, prgView, false);
|
||||
AsyncDrawable asyncDrawable = new AsyncDrawable(imageView.getResources(), placeholderBitmap, task);
|
||||
imageView.setAlpha(1.0f);
|
||||
imageView.setVisibility(View.VISIBLE);
|
||||
imageView.setImageDrawable(asyncDrawable);
|
||||
task.executeOnExecutor(networkExecutor, tuple);
|
||||
}
|
||||
@@ -195,14 +205,20 @@ public class CachedAppAssetLoader {
|
||||
}
|
||||
|
||||
final ImageView imageView = imageViewRef.get();
|
||||
final ProgressBar prgView = progressViewRef.get();
|
||||
if (getLoaderTask(imageView) == this) {
|
||||
// Set the bitmap
|
||||
if (bitmap != null) {
|
||||
imageView.setImageBitmap(bitmap);
|
||||
}
|
||||
|
||||
// Hide the progress bar
|
||||
if (prgView != null) {
|
||||
prgView.setVisibility(View.INVISIBLE);
|
||||
}
|
||||
|
||||
// Show the view
|
||||
imageView.setAlpha(1.0f);
|
||||
imageView.setVisibility(View.VISIBLE);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -213,7 +229,7 @@ public class CachedAppAssetLoader {
|
||||
public AsyncDrawable(Resources res, Bitmap bitmap,
|
||||
LoaderTask loaderTask) {
|
||||
super(res, bitmap);
|
||||
loaderTaskReference = new WeakReference<LoaderTask>(loaderTask);
|
||||
loaderTaskReference = new WeakReference<>(loaderTask);
|
||||
}
|
||||
|
||||
public LoaderTask getLoaderTask() {
|
||||
@@ -280,34 +296,38 @@ public class CachedAppAssetLoader {
|
||||
});
|
||||
}
|
||||
|
||||
public void populateImageView(NvApp app, ImageView view) {
|
||||
public boolean populateImageView(NvApp app, ImageView imgView, ProgressBar prgView) {
|
||||
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;
|
||||
if (!cancelPendingLoad(tuple, imgView)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Hide the progress bar always on initial load
|
||||
prgView.setVisibility(View.INVISIBLE);
|
||||
|
||||
// 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;
|
||||
imgView.setVisibility(View.VISIBLE);
|
||||
imgView.setImageBitmap(bmp);
|
||||
return true;
|
||||
}
|
||||
|
||||
// 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);
|
||||
final LoaderTask task = new LoaderTask(imgView, prgView, true);
|
||||
final AsyncDrawable asyncDrawable = new AsyncDrawable(imgView.getResources(), placeholderBitmap, task);
|
||||
imgView.setVisibility(View.INVISIBLE);
|
||||
imgView.setImageDrawable(asyncDrawable);
|
||||
|
||||
// Run the task on our foreground executor
|
||||
task.executeOnExecutor(foregroundExecutor, tuple);
|
||||
return false;
|
||||
}
|
||||
|
||||
public class LoaderTuple {
|
||||
|
||||
@@ -7,7 +7,6 @@ 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;
|
||||
|
||||
@@ -29,7 +29,7 @@ import android.widget.Toast;
|
||||
public class AddComputerManually extends Activity {
|
||||
private TextView hostText;
|
||||
private ComputerManagerService.ComputerManagerBinder managerBinder;
|
||||
private final LinkedBlockingQueue<String> computersToAdd = new LinkedBlockingQueue<String>();
|
||||
private final LinkedBlockingQueue<String> computersToAdd = new LinkedBlockingQueue<>();
|
||||
private Thread addThread;
|
||||
private final ServiceConnection serviceConnection = new ServiceConnection() {
|
||||
public void onServiceConnected(ComponentName className, final IBinder binder) {
|
||||
|
||||
@@ -3,6 +3,7 @@ package com.limelight.preferences;
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.content.res.Configuration;
|
||||
import android.os.Build;
|
||||
import android.preference.PreferenceManager;
|
||||
|
||||
@@ -100,7 +101,7 @@ public class PreferenceConfiguration {
|
||||
}
|
||||
|
||||
// Use small mode on anything smaller than a 7" tablet
|
||||
return context.getResources().getConfiguration().smallestScreenWidthDp < 600;
|
||||
return context.getResources().getConfiguration().smallestScreenWidthDp < 500;
|
||||
}
|
||||
|
||||
public static int getDefaultBitrate(Context context) {
|
||||
|
||||
@@ -2,7 +2,6 @@ package com.limelight.preferences;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.content.res.Configuration;
|
||||
import android.os.Bundle;
|
||||
import android.app.Activity;
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
package com.limelight.ui;
|
||||
|
||||
import android.annotation.TargetApi;
|
||||
import android.content.Context;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.SurfaceView;
|
||||
|
||||
public class StreamView extends SurfaceView {
|
||||
private double desiredAspectRatio;
|
||||
|
||||
public void setDesiredAspectRatio(double aspectRatio) {
|
||||
this.desiredAspectRatio = aspectRatio;
|
||||
}
|
||||
|
||||
public StreamView(Context context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
public StreamView(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
}
|
||||
|
||||
public StreamView(Context context, AttributeSet attrs, int defStyleAttr) {
|
||||
super(context, attrs, defStyleAttr);
|
||||
}
|
||||
|
||||
@TargetApi(21)
|
||||
public StreamView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
|
||||
super(context, attrs, defStyleAttr, defStyleRes);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
|
||||
// If no fixed aspect ratio has been provided, simply use the default onMeasure() behavior
|
||||
if (desiredAspectRatio == 0) {
|
||||
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
|
||||
return;
|
||||
}
|
||||
|
||||
// Based on code from: https://www.buzzingandroid.com/2012/11/easy-measuring-of-custom-views-with-specific-aspect-ratio/
|
||||
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
|
||||
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
|
||||
|
||||
int measuredHeight, measuredWidth;
|
||||
if (widthSize > heightSize * desiredAspectRatio) {
|
||||
measuredHeight = heightSize;
|
||||
measuredWidth = (int)(measuredHeight * desiredAspectRatio);
|
||||
} else {
|
||||
measuredWidth = widthSize;
|
||||
measuredHeight = (int)(measuredWidth / desiredAspectRatio);
|
||||
}
|
||||
|
||||
setMeasuredDimension(measuredWidth, measuredHeight);
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,9 @@ import java.util.ArrayList;
|
||||
import android.app.Activity;
|
||||
import android.app.AlertDialog;
|
||||
import android.content.DialogInterface;
|
||||
import android.widget.Button;
|
||||
|
||||
import com.limelight.R;
|
||||
|
||||
public class Dialog implements Runnable {
|
||||
private final String title;
|
||||
@@ -14,7 +17,7 @@ public class Dialog implements Runnable {
|
||||
|
||||
private AlertDialog alert;
|
||||
|
||||
private static final ArrayList<Dialog> rundownDialogs = new ArrayList<Dialog>();
|
||||
private static final ArrayList<Dialog> rundownDialogs = new ArrayList<>();
|
||||
|
||||
private Dialog(Activity activity, String title, String message, boolean endAfterDismiss)
|
||||
{
|
||||
@@ -55,7 +58,7 @@ public class Dialog implements Runnable {
|
||||
alert.setCancelable(false);
|
||||
alert.setCanceledOnTouchOutside(false);
|
||||
|
||||
alert.setButton(AlertDialog.BUTTON_NEUTRAL, "OK", new DialogInterface.OnClickListener() {
|
||||
alert.setButton(AlertDialog.BUTTON_POSITIVE, activity.getResources().getText(android.R.string.ok), new DialogInterface.OnClickListener() {
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
synchronized (rundownDialogs) {
|
||||
rundownDialogs.remove(Dialog.this);
|
||||
@@ -67,6 +70,31 @@ public class Dialog implements Runnable {
|
||||
}
|
||||
}
|
||||
});
|
||||
alert.setButton(AlertDialog.BUTTON_NEUTRAL, activity.getResources().getText(R.string.help), new DialogInterface.OnClickListener() {
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
synchronized (rundownDialogs) {
|
||||
rundownDialogs.remove(Dialog.this);
|
||||
alert.dismiss();
|
||||
}
|
||||
|
||||
if (endAfterDismiss) {
|
||||
activity.finish();
|
||||
}
|
||||
|
||||
HelpLauncher.launchTroubleshooting(activity);
|
||||
}
|
||||
});
|
||||
alert.setOnShowListener(new DialogInterface.OnShowListener(){
|
||||
|
||||
@Override
|
||||
public void onShow(DialogInterface dialog) {
|
||||
// Set focus to the OK button by default
|
||||
Button button = alert.getButton(AlertDialog.BUTTON_POSITIVE);
|
||||
button.setFocusable(true);
|
||||
button.setFocusableInTouchMode(true);
|
||||
button.requestFocus();
|
||||
}
|
||||
});
|
||||
|
||||
synchronized (rundownDialogs) {
|
||||
rundownDialogs.add(this);
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
package com.limelight.utils;
|
||||
|
||||
import android.content.ActivityNotFoundException;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
|
||||
import com.limelight.HelpActivity;
|
||||
|
||||
public class HelpLauncher {
|
||||
|
||||
private static void launchUrl(Context context, String url) {
|
||||
// Try to launch the default browser
|
||||
try {
|
||||
// Fire TV devices will lie and say they do have a browser
|
||||
// even though the OS just shows an error dialog if we
|
||||
// try to use it.
|
||||
if (!"Amazon".equalsIgnoreCase(Build.MANUFACTURER)) {
|
||||
Intent i = new Intent(Intent.ACTION_VIEW);
|
||||
i.setData(Uri.parse(url));
|
||||
context.startActivity(i);
|
||||
return;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// This is only supposed to throw ActivityNotFoundException but
|
||||
// it can (at least) also throw SecurityException if a user's default
|
||||
// browser is not exported. We'll catch everything to workaround this.
|
||||
|
||||
// Fall through
|
||||
}
|
||||
|
||||
// This platform has no browser (possibly a leanback device)
|
||||
// We'll launch our WebView activity
|
||||
Intent i = new Intent(context, HelpActivity.class);
|
||||
i.setData(Uri.parse(url));
|
||||
context.startActivity(i);
|
||||
}
|
||||
|
||||
public static void launchSetupGuide(Context context) {
|
||||
launchUrl(context, "https://github.com/moonlight-stream/moonlight-docs/wiki/Setup-Guide");
|
||||
}
|
||||
|
||||
public static void launchTroubleshooting(Context context) {
|
||||
launchUrl(context, "https://github.com/moonlight-stream/moonlight-docs/wiki/Troubleshooting");
|
||||
}
|
||||
}
|
||||
@@ -23,8 +23,8 @@ public class ServerHelper {
|
||||
computer.localIp : computer.remoteIp;
|
||||
}
|
||||
|
||||
public static void doStart(Activity parent, NvApp app, ComputerDetails computer,
|
||||
ComputerManagerService.ComputerManagerBinder managerBinder) {
|
||||
public static Intent createStartIntent(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 ?
|
||||
@@ -34,7 +34,14 @@ public class ServerHelper {
|
||||
intent.putExtra(Game.EXTRA_UNIQUEID, managerBinder.getUniqueId());
|
||||
intent.putExtra(Game.EXTRA_STREAMING_REMOTE,
|
||||
computer.reachability != ComputerDetails.Reachability.LOCAL);
|
||||
parent.startActivity(intent);
|
||||
intent.putExtra(Game.EXTRA_PC_UUID, computer.uuid.toString());
|
||||
intent.putExtra(Game.EXTRA_PC_NAME, computer.name);
|
||||
return intent;
|
||||
}
|
||||
|
||||
public static void doStart(Activity parent, NvApp app, ComputerDetails computer,
|
||||
ComputerManagerService.ComputerManagerBinder managerBinder) {
|
||||
parent.startActivity(createStartIntent(parent, app, computer, managerBinder));
|
||||
}
|
||||
|
||||
public static void doQuit(final Activity parent,
|
||||
|
||||
@@ -0,0 +1,138 @@
|
||||
package com.limelight.utils;
|
||||
|
||||
import android.annotation.TargetApi;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.ShortcutInfo;
|
||||
import android.content.pm.ShortcutManager;
|
||||
import android.graphics.drawable.Icon;
|
||||
import android.os.Build;
|
||||
|
||||
import com.limelight.AppView;
|
||||
import com.limelight.AppViewShortcutTrampoline;
|
||||
import com.limelight.R;
|
||||
import com.limelight.nvstream.http.ComputerDetails;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
public class ShortcutHelper {
|
||||
|
||||
private final ShortcutManager sm;
|
||||
private final Context context;
|
||||
|
||||
public ShortcutHelper(Context context) {
|
||||
this.context = context;
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) {
|
||||
sm = context.getSystemService(ShortcutManager.class);
|
||||
}
|
||||
else {
|
||||
sm = null;
|
||||
}
|
||||
}
|
||||
|
||||
@TargetApi(Build.VERSION_CODES.N_MR1)
|
||||
private void reapShortcutsForDynamicAdd() {
|
||||
List<ShortcutInfo> dynamicShortcuts = sm.getDynamicShortcuts();
|
||||
while (dynamicShortcuts.size() >= sm.getMaxShortcutCountPerActivity()) {
|
||||
ShortcutInfo maxRankShortcut = dynamicShortcuts.get(0);
|
||||
for (ShortcutInfo scut : dynamicShortcuts) {
|
||||
if (maxRankShortcut.getRank() < scut.getRank()) {
|
||||
maxRankShortcut = scut;
|
||||
}
|
||||
}
|
||||
sm.removeDynamicShortcuts(Collections.singletonList(maxRankShortcut.getId()));
|
||||
}
|
||||
}
|
||||
|
||||
@TargetApi(Build.VERSION_CODES.N_MR1)
|
||||
private List<ShortcutInfo> getAllShortcuts() {
|
||||
LinkedList<ShortcutInfo> list = new LinkedList<>();
|
||||
list.addAll(sm.getDynamicShortcuts());
|
||||
list.addAll(sm.getPinnedShortcuts());
|
||||
return list;
|
||||
}
|
||||
|
||||
@TargetApi(Build.VERSION_CODES.N_MR1)
|
||||
private ShortcutInfo getInfoForId(String id) {
|
||||
List<ShortcutInfo> shortcuts = getAllShortcuts();
|
||||
|
||||
for (ShortcutInfo info : shortcuts) {
|
||||
if (info.getId().equals(id)) {
|
||||
return info;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@TargetApi(Build.VERSION_CODES.N_MR1)
|
||||
private boolean isExistingDynamicShortcut(String id) {
|
||||
for (ShortcutInfo si : sm.getDynamicShortcuts()) {
|
||||
if (si.getId().equals(id)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public void reportShortcutUsed(String id) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) {
|
||||
ShortcutInfo sinfo = getInfoForId(id);
|
||||
if (sinfo != null) {
|
||||
sm.reportShortcutUsed(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void createAppViewShortcut(String id, String computerName, String computerUuid, boolean forceAdd) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) {
|
||||
Intent i = new Intent(context, AppViewShortcutTrampoline.class);
|
||||
i.putExtra(AppView.NAME_EXTRA, computerName);
|
||||
i.putExtra(AppView.UUID_EXTRA, computerUuid);
|
||||
i.setAction(Intent.ACTION_DEFAULT);
|
||||
|
||||
ShortcutInfo sinfo = new ShortcutInfo.Builder(context, id)
|
||||
.setIntent(i)
|
||||
.setShortLabel(computerName)
|
||||
.setLongLabel(computerName)
|
||||
.setIcon(Icon.createWithResource(context, R.mipmap.ic_pc_scut))
|
||||
.build();
|
||||
|
||||
ShortcutInfo existingSinfo = getInfoForId(id);
|
||||
if (existingSinfo != null) {
|
||||
// Update in place
|
||||
sm.updateShortcuts(Collections.singletonList(sinfo));
|
||||
sm.enableShortcuts(Collections.singletonList(id));
|
||||
}
|
||||
|
||||
// Reap shortcuts to make space for this if it's new
|
||||
// NOTE: This CAN'T be an else on the above if, because it's
|
||||
// possible that we have an existing shortcut but it's not a dynamic one.
|
||||
if (!isExistingDynamicShortcut(id)) {
|
||||
// To avoid a random carousel of shortcuts popping in and out based on polling status,
|
||||
// we only add shortcuts if it's not at the limit or the user made a conscious action
|
||||
// to interact with this PC.
|
||||
if (forceAdd || sm.getDynamicShortcuts().size() < sm.getMaxShortcutCountPerActivity()) {
|
||||
reapShortcutsForDynamicAdd();
|
||||
sm.addDynamicShortcuts(Collections.singletonList(sinfo));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void createAppViewShortcut(String id, ComputerDetails details, boolean forceAdd) {
|
||||
createAppViewShortcut(id, details.name, details.uuid.toString(), forceAdd);
|
||||
}
|
||||
|
||||
public void disableShortcut(String id, CharSequence reason) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) {
|
||||
ShortcutInfo sinfo = getInfoForId(id);
|
||||
if (sinfo != null) {
|
||||
sm.disableShortcuts(Collections.singletonList(id), reason);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -15,7 +15,7 @@ public class SpinnerDialog implements Runnable,OnCancelListener {
|
||||
private ProgressDialog progress;
|
||||
private final boolean finish;
|
||||
|
||||
private static final ArrayList<SpinnerDialog> rundownDialogs = new ArrayList<SpinnerDialog>();
|
||||
private static final ArrayList<SpinnerDialog> rundownDialogs = new ArrayList<>();
|
||||
|
||||
private SpinnerDialog(Activity activity, String title, String message, boolean finish)
|
||||
{
|
||||
|
||||
@@ -15,6 +15,8 @@
|
||||
#include <netinet/in.h>
|
||||
#include <netinet/tcp.h>
|
||||
|
||||
#include <arpa/inet.h>
|
||||
|
||||
#include <android/log.h>
|
||||
|
||||
#define EVDEV_MAX_EVENT_SIZE 24
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
#include "enet/enet.h"
|
||||
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#include <jni.h>
|
||||
|
||||
#define CLIENT_TO_LONG(x) ((intptr_t)(x))
|
||||
@@ -15,9 +17,21 @@ Java_com_limelight_nvstream_enet_EnetConnection_initializeEnet(JNIEnv *env, jobj
|
||||
}
|
||||
|
||||
JNIEXPORT jlong JNICALL
|
||||
Java_com_limelight_nvstream_enet_EnetConnection_createClient(JNIEnv *env, jobject class) {
|
||||
Java_com_limelight_nvstream_enet_EnetConnection_createClient(JNIEnv *env, jobject class, jstring address) {
|
||||
ENetAddress enetAddress;
|
||||
const char *addrStr;
|
||||
int err;
|
||||
|
||||
// Perform a lookup on the address to determine the address family
|
||||
addrStr = (*env)->GetStringUTFChars(env, address, 0);
|
||||
err = enet_address_set_host(&enetAddress, addrStr);
|
||||
(*env)->ReleaseStringUTFChars(env, address, addrStr);
|
||||
if (err < 0) {
|
||||
return CLIENT_TO_LONG(NULL);
|
||||
}
|
||||
|
||||
// Create a client that can use 1 outgoing connection and 1 channel
|
||||
return CLIENT_TO_LONG(enet_host_create(NULL, 1, 1, 0, 0));
|
||||
return CLIENT_TO_LONG(enet_host_create(enetAddress.address.ss_family, NULL, 1, 1, 0, 0));
|
||||
}
|
||||
|
||||
JNIEXPORT jlong JNICALL
|
||||
@@ -26,12 +40,16 @@ Java_com_limelight_nvstream_enet_EnetConnection_connectToPeer(JNIEnv *env, jobje
|
||||
ENetAddress enetAddress;
|
||||
ENetEvent event;
|
||||
const char *addrStr;
|
||||
int err;
|
||||
|
||||
// Initialize the ENet address
|
||||
addrStr = (*env)->GetStringUTFChars(env, address, 0);
|
||||
enet_address_set_host(&enetAddress, addrStr);
|
||||
err = enet_address_set_host(&enetAddress, addrStr);
|
||||
enet_address_set_port(&enetAddress, port);
|
||||
(*env)->ReleaseStringUTFChars(env, address, addrStr);
|
||||
if (err < 0) {
|
||||
return PEER_TO_LONG(NULL);
|
||||
}
|
||||
|
||||
// Start the connection
|
||||
peer = enet_host_connect(LONG_TO_CLIENT(client), &enetAddress, 1, 0);
|
||||
@@ -99,9 +117,16 @@ Java_com_limelight_nvstream_enet_EnetConnection_writePacket(JNIEnv *env, jobject
|
||||
packet = enet_packet_create(dataPtr, length, packetFlags);
|
||||
if (packet != NULL) {
|
||||
// Send the message to the peer
|
||||
enet_peer_send(LONG_TO_PEER(peer), 0, packet);
|
||||
enet_host_flush(LONG_TO_CLIENT(client));
|
||||
ret = JNI_TRUE;
|
||||
if (enet_peer_send(LONG_TO_PEER(peer), 0, packet) < 0) {
|
||||
// This can fail if the peer has been disconnected
|
||||
enet_packet_destroy(packet);
|
||||
ret = JNI_FALSE;
|
||||
}
|
||||
else {
|
||||
// Force the client to send the packet now
|
||||
enet_host_flush(LONG_TO_CLIENT(client));
|
||||
ret = JNI_TRUE;
|
||||
}
|
||||
}
|
||||
else {
|
||||
ret = JNI_FALSE;
|
||||
@@ -112,12 +137,12 @@ Java_com_limelight_nvstream_enet_EnetConnection_writePacket(JNIEnv *env, jobject
|
||||
return ret;
|
||||
}
|
||||
|
||||
JNIEXPORT jint JNICALL
|
||||
JNIEXPORT void JNICALL
|
||||
Java_com_limelight_nvstream_enet_EnetConnection_destroyClient(JNIEnv *env, jobject class, jlong client) {
|
||||
enet_host_destroy(LONG_TO_CLIENT(client));
|
||||
}
|
||||
|
||||
JNIEXPORT jint JNICALL
|
||||
JNIEXPORT void JNICALL
|
||||
Java_com_limelight_nvstream_enet_EnetConnection_disconnectPeer(JNIEnv *env, jobject class, jlong peer) {
|
||||
enet_peer_disconnect_now(LONG_TO_PEER(peer), 0);
|
||||
}
|
||||
|
||||
@@ -19,7 +19,8 @@ Java_com_limelight_nvstream_av_audio_OpusDecoder_init(JNIEnv *env, jobject this,
|
||||
ChannelCount = channelCount;
|
||||
|
||||
jni_mapping_data = (*env)->GetByteArrayElements(env, mapping, 0);
|
||||
ret = nv_opus_init(sampleRate, channelCount, streams, coupledStreams, jni_mapping_data);
|
||||
ret = nv_opus_init(sampleRate, channelCount, streams, coupledStreams,
|
||||
(const unsigned char*)jni_mapping_data);
|
||||
(*env)->ReleaseByteArrayElements(env, mapping, jni_mapping_data, JNI_ABORT);
|
||||
|
||||
return ret;
|
||||
@@ -49,7 +50,8 @@ Java_com_limelight_nvstream_av_audio_OpusDecoder_decode(
|
||||
if (indata != NULL) {
|
||||
jni_input_data = (*env)->GetByteArrayElements(env, indata, 0);
|
||||
|
||||
ret = nv_opus_decode(&jni_input_data[inoff], inlen, (jshort*)jni_pcm_data, SamplesPerChannel);
|
||||
ret = nv_opus_decode((unsigned char*)&jni_input_data[inoff], inlen,
|
||||
(jshort*)jni_pcm_data, SamplesPerChannel);
|
||||
|
||||
// The input data isn't changed so it can be safely aborted
|
||||
(*env)->ReleaseByteArrayElements(env, indata, jni_input_data, JNI_ABORT);
|
||||
|
||||
|
Before Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 40 KiB |
@@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="256dp"
|
||||
android:height="256dp"
|
||||
android:viewportWidth="24.0"
|
||||
android:viewportHeight="24.0">
|
||||
<path
|
||||
android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM17,13h-4v4h-2v-4L7,13v-2h4L11,7h2v4h4v2z"
|
||||
android:fillColor="#FFFFFF"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="512dp"
|
||||
android:height="512dp"
|
||||
android:viewportWidth="24.0"
|
||||
android:viewportHeight="24.0">
|
||||
<path
|
||||
android:pathData="M21,2L3,2c-1.1,0 -2,0.9 -2,2v12c0,1.1 0.9,2 2,2h7v2L8,20v2h8v-2h-2v-2h7c1.1,0 2,-0.9 2,-2L23,4c0,-1.1 -0.9,-2 -2,-2zM21,16L3,16L3,4h18v12z"
|
||||
android:fillColor="#FFFFFF"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="256dp"
|
||||
android:height="256dp"
|
||||
android:viewportWidth="24.0"
|
||||
android:viewportHeight="24.0">
|
||||
<path
|
||||
android:fillColor="#FFFFFF"
|
||||
android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM13,19h-2v-2h2v2zM15.07,11.25l-0.9,0.92C13.45,12.9 13,13.5 13,15h-2v-0.5c0,-1.1 0.45,-2.1 1.17,-2.83l1.24,-1.26c0.37,-0.36 0.59,-0.86 0.59,-1.41 0,-1.1 -0.9,-2 -2,-2s-2,0.9 -2,2L8,9c0,-2.21 1.79,-4 4,-4s4,1.79 4,4c0,0.88 -0.36,1.68 -0.93,2.25z"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="128dp"
|
||||
android:height="128dp"
|
||||
android:viewportWidth="24.0"
|
||||
android:viewportHeight="24.0">
|
||||
<path
|
||||
android:pathData="M23.64,7c-0.45,-0.34 -4.93,-4 -11.64,-4 -1.5,0 -2.89,0.19 -4.15,0.48L18.18,13.8 23.64,7zM17.04,15.22L3.27,1.44 2,2.72l2.05,2.06C1.91,5.76 0.59,6.82 0.36,7l11.63,14.49 0.01,0.01 0.01,-0.01 3.9,-4.86 3.32,3.32 1.27,-1.27 -3.46,-3.46z"
|
||||
android:fillColor="#FFFFFF"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="128dp"
|
||||
android:height="128dp"
|
||||
android:viewportWidth="24.0"
|
||||
android:viewportHeight="24.0">
|
||||
<path
|
||||
android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM10,16.5v-9l6,4.5 -6,4.5z"
|
||||
android:fillColor="#FFFFFF"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="256dp"
|
||||
android:height="256dp"
|
||||
android:viewportWidth="24.0"
|
||||
android:viewportHeight="24.0">
|
||||
<path
|
||||
android:pathData="M19.43,12.98c0.04,-0.32 0.07,-0.64 0.07,-0.98s-0.03,-0.66 -0.07,-0.98l2.11,-1.65c0.19,-0.15 0.24,-0.42 0.12,-0.64l-2,-3.46c-0.12,-0.22 -0.39,-0.3 -0.61,-0.22l-2.49,1c-0.52,-0.4 -1.08,-0.73 -1.69,-0.98l-0.38,-2.65C14.46,2.18 14.25,2 14,2h-4c-0.25,0 -0.46,0.18 -0.49,0.42l-0.38,2.65c-0.61,0.25 -1.17,0.59 -1.69,0.98l-2.49,-1c-0.23,-0.09 -0.49,0 -0.61,0.22l-2,3.46c-0.13,0.22 -0.07,0.49 0.12,0.64l2.11,1.65c-0.04,0.32 -0.07,0.65 -0.07,0.98s0.03,0.66 0.07,0.98l-2.11,1.65c-0.19,0.15 -0.24,0.42 -0.12,0.64l2,3.46c0.12,0.22 0.39,0.3 0.61,0.22l2.49,-1c0.52,0.4 1.08,0.73 1.69,0.98l0.38,2.65c0.03,0.24 0.24,0.42 0.49,0.42h4c0.25,0 0.46,-0.18 0.49,-0.42l0.38,-2.65c0.61,-0.25 1.17,-0.59 1.69,-0.98l2.49,1c0.23,0.09 0.49,0 0.61,-0.22l2,-3.46c0.12,-0.22 0.07,-0.49 -0.12,-0.64l-2.11,-1.65zM12,15.5c-1.93,0 -3.5,-1.57 -3.5,-3.5s1.57,-3.5 3.5,-3.5 3.5,1.57 3.5,3.5 -1.57,3.5 -3.5,3.5z"
|
||||
android:fillColor="#FFFFFF"/>
|
||||
</vector>
|
||||
|
Before Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 24 KiB |
@@ -9,41 +9,40 @@
|
||||
tools:context=".PcView" >
|
||||
|
||||
<RelativeLayout
|
||||
android:id="@+id/no_pc_found_layout"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_centerInParent="true"
|
||||
android:layout_centerVertical="true"
|
||||
android:layout_centerHorizontal="true">
|
||||
<ImageView
|
||||
android:id="@+id/pcs_loading"
|
||||
android:layout_width="75dp"
|
||||
android:layout_height="75dp"
|
||||
android:src="@drawable/image_loading"/>
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_toRightOf="@+id/pcs_loading"
|
||||
android:layout_toEndOf="@+id/pcs_loading"
|
||||
android:layout_marginLeft="5dp"
|
||||
android:layout_marginStart="5dp"
|
||||
android:layout_centerVertical="true"
|
||||
android:textAppearance="?android:attr/textAppearanceLarge"
|
||||
android:gravity="center"
|
||||
android:text="@string/searching_pc"/>
|
||||
</RelativeLayout>
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/pcFragmentContainer"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:layout_alignParentBottom="true"
|
||||
android:layout_alignParentTop="true"
|
||||
android:layout_toLeftOf="@+id/manuallyAddPc"
|
||||
android:layout_toStartOf="@+id/manuallyAddPc"
|
||||
android:layout_toRightOf="@+id/settingsButton"
|
||||
android:layout_toEndOf="@+id/settingsButton"/>
|
||||
android:layout_toEndOf="@+id/settingsButton">
|
||||
|
||||
<RelativeLayout
|
||||
android:id="@+id/no_pc_found_layout"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_centerVertical="true"
|
||||
android:layout_centerHorizontal="true">
|
||||
<ProgressBar
|
||||
android:id="@+id/pcs_loading"
|
||||
android:layout_width="75dp"
|
||||
android:layout_height="75dp"
|
||||
android:indeterminate="true"/>
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_toRightOf="@+id/pcs_loading"
|
||||
android:layout_toEndOf="@+id/pcs_loading"
|
||||
android:layout_marginLeft="10dp"
|
||||
android:layout_marginStart="10dp"
|
||||
android:layout_centerVertical="true"
|
||||
android:textAppearance="?android:attr/textAppearanceLarge"
|
||||
android:gravity="center"
|
||||
android:text="@string/searching_pc"/>
|
||||
</RelativeLayout>
|
||||
|
||||
</RelativeLayout>
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/settingsButton"
|
||||
@@ -51,11 +50,22 @@
|
||||
android:layout_height="65dp"
|
||||
android:cropToPadding="false"
|
||||
android:scaleType="fitXY"
|
||||
android:nextFocusDown="@+id/pcGridView"
|
||||
android:layout_alignParentStart="true"
|
||||
android:layout_alignParentLeft="true"
|
||||
android:layout_alignParentTop="true"
|
||||
android:src="@drawable/settings"
|
||||
android:src="@drawable/ic_settings"
|
||||
style="?android:attr/borderlessButtonStyle"/>
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/helpButton"
|
||||
android:layout_width="70dp"
|
||||
android:layout_height="65dp"
|
||||
android:cropToPadding="false"
|
||||
android:scaleType="fitXY"
|
||||
android:layout_alignParentStart="true"
|
||||
android:layout_alignParentLeft="true"
|
||||
android:layout_below="@id/settingsButton"
|
||||
android:src="@drawable/ic_help"
|
||||
style="?android:attr/borderlessButtonStyle"/>
|
||||
|
||||
<ImageButton
|
||||
@@ -64,11 +74,10 @@
|
||||
android:layout_height="65dp"
|
||||
android:cropToPadding="false"
|
||||
android:scaleType="fitXY"
|
||||
android:nextFocusDown="@+id/pcGridView"
|
||||
android:layout_alignParentTop="true"
|
||||
android:layout_alignParentRight="true"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:src="@drawable/add_computer"
|
||||
android:layout_alignParentStart="true"
|
||||
android:layout_alignParentLeft="true"
|
||||
android:layout_below="@id/helpButton"
|
||||
android:src="@drawable/ic_add"
|
||||
style="?android:attr/borderlessButtonStyle"/>
|
||||
|
||||
</RelativeLayout>
|
||||
|
||||
@@ -9,41 +9,44 @@
|
||||
tools:context=".PcView" >
|
||||
|
||||
<RelativeLayout
|
||||
android:id="@+id/no_pc_found_layout"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_centerInParent="true"
|
||||
android:layout_centerVertical="true"
|
||||
android:layout_centerHorizontal="true">
|
||||
<ImageView
|
||||
android:id="@+id/pcs_loading"
|
||||
android:layout_width="75dp"
|
||||
android:layout_height="75dp"
|
||||
android:src="@drawable/image_loading"/>
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_toRightOf="@+id/pcs_loading"
|
||||
android:layout_toEndOf="@+id/pcs_loading"
|
||||
android:layout_marginLeft="5dp"
|
||||
android:layout_marginStart="5dp"
|
||||
android:layout_centerVertical="true"
|
||||
android:textAppearance="?android:attr/textAppearanceLarge"
|
||||
android:gravity="center"
|
||||
android:text="@string/searching_pc"/>
|
||||
</RelativeLayout>
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/pcFragmentContainer"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:layout_toLeftOf="@+id/manuallyAddPc"
|
||||
android:layout_toStartOf="@+id/manuallyAddPc"
|
||||
android:layout_toRightOf="@+id/settingsButton"
|
||||
android:layout_toEndOf="@+id/settingsButton"
|
||||
android:layout_below="@+id/settingsButton"
|
||||
android:layout_alignParentLeft="true"
|
||||
android:layout_alignParentStart="true"
|
||||
android:layout_alignParentRight="true"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:layout_alignParentBottom="true"
|
||||
android:layout_alignParentTop="true"/>
|
||||
>
|
||||
|
||||
<RelativeLayout
|
||||
android:id="@+id/no_pc_found_layout"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_centerInParent="true"
|
||||
android:layout_centerVertical="true"
|
||||
android:layout_centerHorizontal="true">
|
||||
<ProgressBar
|
||||
android:id="@+id/pcs_loading"
|
||||
android:layout_width="75dp"
|
||||
android:layout_height="75dp"
|
||||
android:indeterminate="true"/>
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_toRightOf="@+id/pcs_loading"
|
||||
android:layout_toEndOf="@+id/pcs_loading"
|
||||
android:layout_marginLeft="10dp"
|
||||
android:layout_marginStart="10dp"
|
||||
android:layout_centerVertical="true"
|
||||
android:textAppearance="?android:attr/textAppearanceLarge"
|
||||
android:gravity="center"
|
||||
android:text="@string/searching_pc"/>
|
||||
</RelativeLayout>
|
||||
|
||||
</RelativeLayout>
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/settingsButton"
|
||||
@@ -54,7 +57,19 @@
|
||||
android:layout_alignParentLeft="true"
|
||||
android:layout_alignParentStart="true"
|
||||
android:layout_alignParentTop="true"
|
||||
android:src="@drawable/settings"
|
||||
android:src="@drawable/ic_settings"
|
||||
style="?android:attr/borderlessButtonStyle"/>
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/helpButton"
|
||||
android:layout_width="70dp"
|
||||
android:layout_height="65dp"
|
||||
android:cropToPadding="false"
|
||||
android:scaleType="fitXY"
|
||||
android:layout_alignParentTop="true"
|
||||
android:layout_toRightOf="@+id/settingsButton"
|
||||
android:layout_toEndOf="@+id/settingsButton"
|
||||
android:src="@drawable/ic_help"
|
||||
style="?android:attr/borderlessButtonStyle"/>
|
||||
|
||||
<ImageButton
|
||||
@@ -66,7 +81,7 @@
|
||||
android:layout_alignParentTop="true"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:layout_alignParentRight="true"
|
||||
android:src="@drawable/add_computer"
|
||||
android:src="@drawable/ic_add"
|
||||
style="?android:attr/borderlessButtonStyle"/>
|
||||
|
||||
</RelativeLayout>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
android:layout_height="match_parent"
|
||||
tools:context=".Game" >
|
||||
|
||||
<SurfaceView
|
||||
<com.limelight.ui.StreamView
|
||||
android:id="@+id/surfaceView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/activity_help"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:paddingBottom="@dimen/activity_vertical_margin"
|
||||
android:paddingLeft="@dimen/activity_horizontal_margin"
|
||||
android:paddingRight="@dimen/activity_horizontal_margin"
|
||||
android:paddingTop="@dimen/activity_vertical_margin"
|
||||
tools:context="com.limelight.HelpActivity">
|
||||
|
||||
<WebView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_alignParentTop="true"
|
||||
android:layout_alignParentLeft="true"
|
||||
android:layout_alignParentStart="true" />
|
||||
</RelativeLayout>
|
||||
@@ -8,6 +8,14 @@
|
||||
android:layout_centerHorizontal="true"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content">
|
||||
<ProgressBar
|
||||
android:id="@+id/grid_spinner"
|
||||
android:layout_centerHorizontal="true"
|
||||
android:layout_centerVertical="true"
|
||||
android:layout_width="75dp"
|
||||
android:layout_height="75dp"
|
||||
android:indeterminate="true">
|
||||
</ProgressBar>
|
||||
<ImageView
|
||||
android:id="@+id/grid_image"
|
||||
android:cropToPadding="false"
|
||||
|
||||
@@ -8,6 +8,14 @@
|
||||
android:layout_centerHorizontal="true"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content">
|
||||
<ProgressBar
|
||||
android:id="@+id/grid_spinner"
|
||||
android:layout_centerHorizontal="true"
|
||||
android:layout_centerVertical="true"
|
||||
android:layout_width="50dp"
|
||||
android:layout_height="50dp"
|
||||
android:indeterminate="true">
|
||||
</ProgressBar>
|
||||
<ImageView
|
||||
android:id="@+id/grid_image"
|
||||
android:cropToPadding="false"
|
||||
|
||||
@@ -10,23 +10,28 @@
|
||||
<ImageView
|
||||
android:id="@+id/grid_image"
|
||||
android:layout_centerHorizontal="true"
|
||||
android:layout_width="150dp"
|
||||
android:layout_height="100dp">
|
||||
android:layout_width="125dp"
|
||||
android:layout_height="125dp">
|
||||
</ImageView>
|
||||
<ImageView
|
||||
android:id="@+id/grid_overlay"
|
||||
android:layout_marginTop="15dp"
|
||||
android:layout_marginLeft="65dp"
|
||||
android:layout_marginStart="65dp"
|
||||
android:layout_marginRight="20dp"
|
||||
android:layout_marginEnd="20dp"
|
||||
android:layout_centerHorizontal="true"
|
||||
android:layout_marginTop="28dp"
|
||||
android:layout_width="50dp"
|
||||
android:layout_height="50dp">
|
||||
</ImageView>
|
||||
<ProgressBar
|
||||
android:id="@+id/grid_spinner"
|
||||
android:layout_centerHorizontal="true"
|
||||
android:layout_marginTop="28dp"
|
||||
android:layout_width="50dp"
|
||||
android:layout_height="50dp"
|
||||
android:indeterminate="true">
|
||||
</ProgressBar>
|
||||
</RelativeLayout>
|
||||
<TextView
|
||||
android:id="@+id/grid_text"
|
||||
android:layout_width="150dp"
|
||||
android:layout_width="125dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@id/grid_image_layout"
|
||||
android:layout_marginTop="10dp"
|
||||
|
||||
@@ -10,23 +10,28 @@
|
||||
<ImageView
|
||||
android:id="@+id/grid_image"
|
||||
android:layout_centerHorizontal="true"
|
||||
android:layout_width="100dp"
|
||||
android:layout_height="67dp">
|
||||
android:layout_width="75dp"
|
||||
android:layout_height="75dp">
|
||||
</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_centerHorizontal="true"
|
||||
android:layout_marginTop="16dp"
|
||||
android:layout_width="33dp"
|
||||
android:layout_height="33dp">
|
||||
</ImageView>
|
||||
<ProgressBar
|
||||
android:id="@+id/grid_spinner"
|
||||
android:layout_centerHorizontal="true"
|
||||
android:layout_marginTop="16dp"
|
||||
android:layout_width="33dp"
|
||||
android:layout_height="33dp"
|
||||
android:indeterminate="true">
|
||||
</ProgressBar>
|
||||
</RelativeLayout>
|
||||
<TextView
|
||||
android:id="@+id/grid_text"
|
||||
android:layout_width="100dp"
|
||||
android:layout_width="75dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@id/grid_image_layout"
|
||||
android:layout_marginTop="10dp"
|
||||
|
||||
|
After Width: | Height: | Size: 2.6 KiB |
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 3.7 KiB |
|
After Width: | Height: | Size: 5.9 KiB |
|
After Width: | Height: | Size: 8.6 KiB |
@@ -1,14 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string-array name="resolution_names">
|
||||
<item>720p - 30 FPS</item>
|
||||
<item>720p - 60 FPS</item>
|
||||
<item>1080p - 30 FPS</item>
|
||||
<item>1080p - 60 FPS</item>
|
||||
<item>4K - 30 FPS</item>
|
||||
<item>4K - 60 FPS</item>
|
||||
</string-array>
|
||||
|
||||
<string-array name="decoder_names">
|
||||
<item>Scegli decoder automaticamente</item>
|
||||
<item>Forza decoder software</item>
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<resources>
|
||||
<string-array name="decoder_names">
|
||||
<item>デコーダを自動選択</item>
|
||||
<item>ソフトウェアデコードを強制</item>
|
||||
<item>ハードウェアデコードを強制</item>
|
||||
</string-array>
|
||||
|
||||
<string-array name="video_format_names">
|
||||
<item>安定している場合にH.265を有効化</item>
|
||||
<item>H.265を強制的に有効化(クラッシュする可能性があります)</item>
|
||||
<item>H.265を無効化</item>
|
||||
</string-array>
|
||||
</resources>
|
||||
@@ -0,0 +1,127 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
|
||||
<!-- PC view menu entries -->
|
||||
<string name="pcview_menu_app_list">ゲームリストを表示</string>
|
||||
<string name="pcview_menu_pair_pc">コンピュータとペアリング</string>
|
||||
<string name="pcview_menu_unpair_pc">ペアリングを解除r</string>
|
||||
<string name="pcview_menu_send_wol">Wake-On-LANリクエストを送信する</string>
|
||||
<string name="pcview_menu_delete_pc">コンピュータをリストから削除する</string>
|
||||
|
||||
<!-- Pair messages -->
|
||||
<string name="pairing">ペアリング中</string>
|
||||
<string name="pair_pc_offline">コンピュータはオフラインです</string>
|
||||
<string name="pair_pc_ingame">現在このコンピュータはゲームを実行中のため、ペアリングする前にゲームを閉じる必要があります</string>
|
||||
<string name="pair_pairing_title">ペアリング</string>
|
||||
<string name="pair_pairing_msg">ペアリングしたいコンピュータのPINコードを入力してください:</string>
|
||||
<string name="pair_incorrect_pin">PINコードが間違っています</string>
|
||||
<string name="pair_fail">ペアリングに失敗しました</string>
|
||||
|
||||
<!-- WOL messages -->
|
||||
<string name="wol_pc_online">コンピュータはオンラインです</string>
|
||||
<string name="wol_no_mac">GFEがMACアドレスを送信しなかったため、コンピュータを起動することができません</string>
|
||||
<string name="wol_waking_pc">コンピュータを起動中</string>
|
||||
<string name="wol_waking_msg">コンピュータが起動するまで数秒かかる可能性があります。Wake-On-LANが正しく構成されているか確認してください</string>
|
||||
<string name="wol_fail">Wake-On-LANパケットの送信に失敗しました</string>
|
||||
|
||||
<!-- Unpair messages -->
|
||||
<string name="unpairing">ペアリングの解除中</string>
|
||||
<string name="unpair_success">ペアリングの解除に成功しました</string>
|
||||
<string name="unpair_fail">ペアリングの解除に失敗しました</string>
|
||||
<string name="unpair_error">このデバイスはペアリングされませんでした</string>
|
||||
|
||||
<!-- Errors -->
|
||||
<string name="error_pc_offline">コンピュータはオフラインです</string>
|
||||
<string name="error_manager_not_running">ComputerManagerサービスが動作していません。しばらく待つか、Moonlightを再起動してください</string>
|
||||
<string name="error_unknown_host">ホストが解決できません</string>
|
||||
<string name="error_404">GFEがHTTP 404を返しました。コンピュータでサポートされているGPUが動作しているか確認してください。リモートデスクトップソフトウェアでもこのエラーが発生する可能性があります。コンピュータを再起動するか、GFEを再インストールしてください</string>
|
||||
|
||||
<!-- Start application messages -->
|
||||
<string name="conn_establishing_title">接続を確立</string>
|
||||
<string name="conn_establishing_msg">接続を開始</string>
|
||||
<string name="conn_metered">警告: 現在使用中のネットワークは従量制課金接続です</string>
|
||||
<string name="conn_client_latency"> デコードの平均待ち時間:</string>
|
||||
<string name="conn_client_latency_hw">ハードウェアデコーダーの待ち時間</string>
|
||||
<string name="conn_hardware_latency">ハードウェアデコーダーの平均待ち時間:</string>
|
||||
<string name="conn_starting">開始</string>
|
||||
<string name="conn_error_title">接続エラー</string>
|
||||
<string name="conn_error_msg">開始できません</string>
|
||||
<string name="conn_terminated_title">接続を終了</string>
|
||||
<string name="conn_terminated_msg">接続は終了しました</string>
|
||||
|
||||
<!-- General strings -->
|
||||
<string name="ip_hint">コンピュータのIPアドレス</string>
|
||||
<string name="searching_pc">コンピュータを検索中</string>
|
||||
<string name="yes">はい</string>
|
||||
<string name="no">いいえ</string>
|
||||
<string name="lost_connection">コンピュータとの接続が失われました</string>
|
||||
|
||||
<!-- AppList activity -->
|
||||
<string name="title_applist">ゲーム</string>
|
||||
<string name="applist_menu_resume">セッションを続ける</string>
|
||||
<string name="applist_menu_quit">セッションを終了する</string>
|
||||
<string name="applist_menu_quit_and_start">現在のゲームを終了して新しいゲームを始める</string>
|
||||
<string name="applist_menu_cancel">キャンセル</string>
|
||||
<string name="applist_refresh_title">ゲームリスト</string>
|
||||
<string name="applist_refresh_msg">ゲームリストを更新中</string>
|
||||
<string name="applist_refresh_error_title">エラー</string>
|
||||
<string name="applist_refresh_error_msg">ゲームリストの取得に失敗しました</string>
|
||||
<string name="applist_quit_app">終了中</string>
|
||||
<string name="applist_quit_success">ゲームを終了しました</string>
|
||||
<string name="applist_quit_fail">ゲームが終了できませんでした</string>
|
||||
<string name="applist_quit_confirmation">本当にゲームを終了しますか? 保存されていないデータは破棄されます</string>
|
||||
|
||||
<!-- Add computer manually activity -->
|
||||
<string name="title_add_pc">手動でコンピュータを追加する</string>
|
||||
<string name="msg_add_pc">コンピュータに接続中</string>
|
||||
<string name="addpc_fail">指定されたコンピュータに接続できませんでした。ファイアーウォールの設定を確認してください</string>
|
||||
<string name="addpc_success">コンピュータの追加に成功しました</string>
|
||||
<string name="addpc_unknown_host">コンピュータのアドレスが解決できません。コンピュータのアドレスを確認してください</string>
|
||||
<string name="addpc_enter_ip">IPアドレスを入力してください</string>
|
||||
|
||||
<!-- Preferences -->
|
||||
<string name="category_basic_settings">基本的な設定</string>
|
||||
<string name="title_resolution_list">解像度とフレームレート</string>
|
||||
<string name="summary_resolution_list">品質が高いほどラグとクラッシュが発生しやすくなります</string>
|
||||
<string name="title_seekbar_bitrate">映像のビットレート</string>
|
||||
<string name="summary_seekbar_bitrate">ビットレートを低くすればカクつきが抑制され、高くすれば画質が向上します</string>
|
||||
<string name="suffix_seekbar_bitrate">Mbps</string>
|
||||
<string name="title_checkbox_stretch_video">映像を全画面に拡大</string>
|
||||
<string name="title_checkbox_disable_warnings">警告を無効化</string>
|
||||
<string name="summary_checkbox_disable_warnings">ストリーミング中に画面に警告メッセージを表示しない</string>
|
||||
|
||||
<string name="category_audio_settings">音声</string>
|
||||
<string name="title_checkbox_51_surround">5.1chサラウンド</string>
|
||||
<string name="summary_checkbox_51_surround">音声に問題が生じる場合はチェックを外してください。バージョン2.7以降のGFEが必要です</string>
|
||||
|
||||
<string name="category_gamepad_settings">ゲームコントローラ</string>
|
||||
<string name="title_checkbox_multi_controller">複数のゲームコントローラ</string>
|
||||
<string name="summary_checkbox_multi_controller">チェックを外すと、全てのゲームコントローラが単一の物として認識されます</string>
|
||||
<string name="title_seekbar_deadzone">アナログゲームコントローラのデッドゾーン</string>
|
||||
<string name="suffix_seekbar_deadzone">%</string>
|
||||
<string name="title_checkbox_xb1_driver">Xbox 360/Oneコントローラ</string>
|
||||
<string name="summary_checkbox_xb1_driver">XboxゲームコントローラをサポートしないデバイスでXboxゲームコントローラ用のドライバを有効にします</string>
|
||||
|
||||
<string name="category_on_screen_controls_settings">オンスクリーンコントローラ</string>
|
||||
<string name="title_checkbox_show_onscreen_controls">オンスクリーンコントローラ</string>
|
||||
<string name="summary_checkbox_show_onscreen_controls">タッチスクリーン上にオンスクリーンコントローラを表示します</string>
|
||||
|
||||
<string name="category_ui_settings">インターフェース</string>
|
||||
<string name="title_language_list">言語</string>
|
||||
<string name="summary_language_list">Moonlightで使用する言語</string>
|
||||
<string name="title_checkbox_list_mode">リストメニュー</string>
|
||||
<string name="summary_checkbox_list_mode">ゲームをグリッドではなく、リストで表示します</string>
|
||||
<string name="title_checkbox_small_icon_mode">小さなアイコン</string>
|
||||
<string name="summary_checkbox_small_icon_mode">グリッドで小さなアイコンを使用します</string>
|
||||
|
||||
<string name="category_host_settings">ホスト</string>
|
||||
<string name="title_checkbox_enable_sops">ゲーム設定の最適化</string>
|
||||
<string name="summary_checkbox_enable_sops">GFEがゲームの設定を最適化します</string>
|
||||
<string name="title_checkbox_host_audio">コンピュータで音声を再生</string>
|
||||
<string name="summary_checkbox_host_audio">音声をコンピュータとこのデバイスの両方で再生します</string>
|
||||
|
||||
<string name="category_advanced_settings">高度な設定</string>
|
||||
<string name="title_video_format">H.265</string>
|
||||
<string name="summary_video_format">H.265は動画に必要な帯域幅を圧縮します。この機能にはなるべく新しいデバイスが必要です</string>
|
||||
|
||||
</resources>
|
||||
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string-array name="decoder_names">
|
||||
<item>Selecteer Decoder Automatisch</item>
|
||||
<item>Forceer Software Decoderen</item>
|
||||
<item>Forceer Hardware Decoderen</item>
|
||||
</string-array>
|
||||
</resources>
|
||||
@@ -0,0 +1,120 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
|
||||
<!-- PC view menu entries -->
|
||||
<string name="pcview_menu_app_list">Toon Spel Lijst</string>
|
||||
<string name="pcview_menu_pair_pc">Koppel met PC</string>
|
||||
<string name="pcview_menu_unpair_pc">Ontkoppen</string>
|
||||
<string name="pcview_menu_send_wol">Stuur Wake-On-LAN aanvraag</string>
|
||||
<string name="pcview_menu_delete_pc">Verwijder PC</string>
|
||||
|
||||
<!-- Pair messages -->
|
||||
<string name="pairing">Koppelen…</string>
|
||||
<string name="pair_pc_offline">Computer is onbereikbaar</string>
|
||||
<string name="pair_pc_ingame">Computer is op dit moment in een spel. Sluit de spel voordat je koppelt.</string>
|
||||
<string name="pair_pairing_title">Koppelen</string>
|
||||
<string name="pair_pairing_msg">Voer de volgende PIN in op de doel PC:</string>
|
||||
<string name="pair_incorrect_pin">Ongeldige PIN</string>
|
||||
<string name="pair_fail">Koppelen mislukt</string>
|
||||
|
||||
<!-- WOL messages -->
|
||||
<string name="wol_pc_online">Computer is online</string>
|
||||
<string name="wol_no_mac">Het is niet mogelijk om de PC uit slaapstand te halen, omdat GFE geen MAC adres heeft verstuurd.</string>
|
||||
<string name="wol_waking_pc">PC uit slaapstand halen…</string>
|
||||
<string name="wol_waking_msg"> Het ken even duren voordat de PC reageert.
|
||||
Als dit niet gebeurt dan moet je de Wake-On-LAN instellingen controleren.
|
||||
</string>
|
||||
<string name="wol_fail">Wake-On-LAN packets versturen mislukt</string>
|
||||
|
||||
<!-- Unpair messages -->
|
||||
<string name="unpairing">Ontkoppelen…</string>
|
||||
<string name="unpair_success">Ontkoppeling succesvol</string>
|
||||
<string name="unpair_fail">Ontkoppeling mislukt</string>
|
||||
<string name="unpair_error">Apparaat niet gekoppeld</string>
|
||||
|
||||
<!-- Errors -->
|
||||
<string name="error_pc_offline">Computer is offline</string>
|
||||
<string name="error_manager_not_running">The ComputerManager service is niet gestart. Wacht een ogenblik of herstart de app.</string>
|
||||
<string name="error_unknown_host">Host achterhalen mislukt.</string>
|
||||
<string name="error_404">GFE geeft een HTTP 404 fout. Kijk na of de PC een ondersteunde GPU heeft.
|
||||
Remote desktop software kan soms ook voor deze fout zorgen. Probeer de computer opnieuw op te starten of GFE te herinstalleren.
|
||||
</string>
|
||||
|
||||
<!-- Start application messages -->
|
||||
<string name="conn_establishing_title">Verbinding maken</string>
|
||||
<string name="conn_establishing_msg">Verbinding starten</string>
|
||||
<string name="conn_metered">Waarschuwing: Dit internet verbinding bevat een datalimiet!</string>
|
||||
<string name="conn_client_latency">Gemiddelde frame decoding reactietijd:</string>
|
||||
<string name="conn_client_latency_hw">hardware decoder reactietijd:</string>
|
||||
<string name="conn_hardware_latency">Gemiddelde hardware decoding reactietijd:</string>
|
||||
<string name="conn_starting">Starten</string>
|
||||
<string name="conn_error_title">Verbindingsprobleem</string>
|
||||
<string name="conn_error_msg">Starten mislukt</string>
|
||||
<string name="conn_terminated_title">Verbinding beëindigd</string>
|
||||
<string name="conn_terminated_msg">De verbinding is beëindigd</string>
|
||||
|
||||
<!-- General strings -->
|
||||
<string name="ip_hint">IP adres van GeForce PC</string>
|
||||
<string name="searching_pc">Zoeken naar PCs…</string>
|
||||
<string name="yes">Ja</string>
|
||||
<string name="no">Nee</string>
|
||||
<string name="lost_connection">Verbinding met PC verloren</string>
|
||||
|
||||
<!-- AppList activity -->
|
||||
<string name="title_applist">Apps op</string>
|
||||
<string name="applist_menu_resume">Hervat Sessie</string>
|
||||
<string name="applist_menu_quit">Stop Sessie</string>
|
||||
<string name="applist_menu_quit_and_start">Stop Huidige Spel en Start</string>
|
||||
<string name="applist_menu_cancel">Annuleer</string>
|
||||
<string name="applist_refresh_title">App Lijst</string>
|
||||
<string name="applist_refresh_msg">Apps Verversen…</string>
|
||||
<string name="applist_refresh_error_title">Fout</string>
|
||||
<string name="applist_refresh_error_msg">App lijst ophalen mislukt</string>
|
||||
<string name="applist_quit_app">Stoppen</string>
|
||||
<string name="applist_quit_success">Succesvol gestopt.</string>
|
||||
<string name="applist_quit_fail">Stoppen mislukt</string>
|
||||
<string name="applist_quit_confirmation">Weet je het zeker dat je de app wilt sluiten? Alle niet-opgeslagen gegevens gaan verloren.</string>
|
||||
|
||||
<!-- Add computer manually activity -->
|
||||
<string name="title_add_pc">Voeg handmatig PC toe</string>
|
||||
<string name="msg_add_pc">Verbinding maken met PC…</string>
|
||||
<string name="addpc_fail">Verbinden mislukt. Controleer of de benodigde poorten openstaan in de firewall.</string>
|
||||
<string name="addpc_success">Computer toevoegen succesvol</string>
|
||||
<string name="addpc_unknown_host">Adres achterhalen is mislukt. Controleer het ingevoerde adres op typfouten.</string>
|
||||
<string name="addpc_enter_ip">IP-adres invoeren is verplicht.</string>
|
||||
|
||||
<!-- Preferences -->
|
||||
<string name="category_basic_settings">Algemene Instellingen</string>
|
||||
<string name="title_resolution_list">Selecteer resolutie en FPS doel</string>
|
||||
<string name="summary_resolution_list">Instellingen te hoog zetten kunnen crashes en haperingen veroorzaken.</string>
|
||||
<string name="title_seekbar_bitrate">Selecteer doel video bitsnelheid</string>
|
||||
<string name="summary_seekbar_bitrate">Verlaag bitsnelheid om haperingen te verminderen. Verhoog de bitsnelheid voor een betere videokwaliteit.</string>
|
||||
<string name="suffix_seekbar_bitrate">Mbps</string>
|
||||
<string name="title_checkbox_stretch_video">Rek video uit tot volledig scherm</string>
|
||||
<string name="title_checkbox_disable_warnings">Verberg waarschuwingsberichten</string>
|
||||
<string name="summary_checkbox_disable_warnings">Verberg on-screen verbindingswaarschuwingen tijdens het streamen</string>
|
||||
|
||||
<string name="category_gamepad_settings">Gamepad Instellingen</string>
|
||||
<string name="title_checkbox_multi_controller">Multi-gamepad support</string>
|
||||
<string name="summary_checkbox_multi_controller">Wanneer uitgevinkt, alle controllers verschijnen als één.</string>
|
||||
<string name="title_seekbar_deadzone">Pas analoge dodezone aan.</string>
|
||||
<string name="suffix_seekbar_deadzone">%</string>
|
||||
<string name="title_checkbox_xb1_driver">Xbox One controller stuurprogramma</string>
|
||||
<string name="summary_checkbox_xb1_driver">Gebruikt de ingebouwde USB stuurprogramma voor apparaten zonder Xbox One controller ondersteuning.</string>
|
||||
|
||||
<string name="category_ui_settings">UI Installingen</string>
|
||||
<string name="title_language_list">Taal</string>
|
||||
<string name="summary_language_list">Taal te gebruiken in Moonlight</string>
|
||||
<string name="title_checkbox_list_mode">Gebruik lijsten in plaats van kolommen</string>
|
||||
<string name="summary_checkbox_list_mode">Display apps and PCs in lists instead of grids</string>
|
||||
<string name="title_checkbox_small_icon_mode">Gebruik kleine iconen</string>
|
||||
<string name="summary_checkbox_small_icon_mode">Gebruik kleine iconen in kolom onderdelen zodat meer items tegelijk zichtbaar worden.</string>
|
||||
|
||||
<string name="category_host_settings">Host Instellingen</string>
|
||||
<string name="title_checkbox_enable_sops">Optimaliseer spel instellingen</string>
|
||||
<string name="summary_checkbox_enable_sops">Sta GFE toe om spel instellingen te veranderen voor een optimale stream</string>
|
||||
<string name="title_checkbox_host_audio">Speel audio af op PC</string>
|
||||
<string name="summary_checkbox_host_audio">Speel audio af op de computer en op dit apparaat</string>
|
||||
|
||||
<string name="category_advanced_settings">Geavanceerde Instellingen</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,14 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string-array name="decoder_names">
|
||||
<item>Автоматический выбор декодера</item>
|
||||
<item>Принудительное программное декодирование</item>
|
||||
<item>Принудительное аппаратное декодирование</item>
|
||||
</string-array>
|
||||
|
||||
<string-array name="video_format_names">
|
||||
<item>Использовать H.265 только если безопасно</item>
|
||||
<item>Всегда использовать H.265 если доступно</item>
|
||||
<item>Никогда не использовать H.265</item>
|
||||
</string-array>
|
||||
</resources>
|
||||
@@ -0,0 +1,127 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
|
||||
<!-- PC view menu entries -->
|
||||
<string name="pcview_menu_app_list">Посмотреть список игр</string>
|
||||
<string name="pcview_menu_pair_pc">Создать пару с PC</string>
|
||||
<string name="pcview_menu_unpair_pc">Разорвать пару</string>
|
||||
<string name="pcview_menu_send_wol">Отправить Wake-On-LAN запрос</string>
|
||||
<string name="pcview_menu_delete_pc">Удалить PC</string>
|
||||
|
||||
<!-- Pair messages -->
|
||||
<string name="pairing">Создание пары…</string>
|
||||
<string name="pair_pc_offline">Компьютер выключен или находится не в сети</string>
|
||||
<string name="pair_pc_ingame">Компьютер в данный момент находится в игре. Вы должны закрыть игру перед создание пары.</string>
|
||||
<string name="pair_pairing_title">Создание пары</string>
|
||||
<string name="pair_pairing_msg">Пожалуйста введите этот PIN на PC:</string>
|
||||
<string name="pair_incorrect_pin">Неправильный PIN</string>
|
||||
<string name="pair_fail">Создание пары не удалось</string>
|
||||
|
||||
<!-- WOL messages -->
|
||||
<string name="wol_pc_online">Компьютер в сети</string>
|
||||
<string name="wol_no_mac">Невозможно разбудить PC потому что GFE не отправило MAC адрес</string>
|
||||
<string name="wol_waking_pc">Пробуждение PC…</string>
|
||||
<string name="wol_waking_msg">Пробуждение PC может занять несколько секунд.
|
||||
Если этого не происходит, удостоверьтесь что Wake-On-LAN настроен правильно.
|
||||
</string>
|
||||
<string name="wol_fail">Ошибка при отправке Wake-On-LAN пакетов</string>
|
||||
|
||||
<!-- Unpair messages -->
|
||||
<string name="unpairing">Разрыв пары…</string>
|
||||
<string name="unpair_success">Разрыв пары закончился успешно.</string>
|
||||
<string name="unpair_fail">Разрыв пары не удался</string>
|
||||
<string name="unpair_error">Устройство не было спарено</string>
|
||||
|
||||
<!-- Errors -->
|
||||
<string name="error_pc_offline">Компьютер выключен или находится не в сети</string>
|
||||
<string name="error_manager_not_running">Сервис ComputerManager не запущен. Пожалуйста, подождите несколько секунд или перезапустите приложение.</string>
|
||||
<string name="error_unknown_host">Не удалось найти хост</string>
|
||||
<string name="error_404">GFE вернул ошибку HTTP 404. Убедитесь что ваш PC is испольщует поддерживаемый GPU.
|
||||
Использование программ для удалённого доступа также можнт вызывать эту ошибку. Попробуйте перезагрузить компьютер или переустановить GFE.
|
||||
</string>
|
||||
|
||||
<!-- Start application messages -->
|
||||
<string name="conn_establishing_title">Создание соединения.</string>
|
||||
<string name="conn_establishing_msg">Подключение</string>
|
||||
<string name="conn_metered">Внимание: Происходит измерение вашего сетевого соединения!</string>
|
||||
<string name="conn_client_latency">Средняя задержка декодирования кадра: </string>
|
||||
<string name="conn_client_latency_hw">задержка аппаратного декодирования:</string>
|
||||
<string name="conn_hardware_latency">Средняя задержка апаратного декодирования:</string>
|
||||
<string name="conn_starting">Запуск</string>
|
||||
<string name="conn_error_title">Ошибка соединения</string>
|
||||
<string name="conn_error_msg">Запуск не удался</string>
|
||||
<string name="conn_terminated_title">Соединение прекращено</string>
|
||||
<string name="conn_terminated_msg">Подключение было прервано</string>
|
||||
|
||||
<!-- General strings -->
|
||||
<string name="ip_hint">IP адресс компьютера с GeForce</string>
|
||||
<string name="searching_pc">Поиск компьютеров…</string>
|
||||
<string name="yes">Да</string>
|
||||
<string name="no">Нет</string>
|
||||
<string name="lost_connection">Потеряно соединение с PC</string>
|
||||
|
||||
<!-- AppList activity -->
|
||||
<string name="title_applist">Приложения на</string>
|
||||
<string name="applist_menu_resume">Возобновить сессию</string>
|
||||
<string name="applist_menu_quit">Выйти из сессии</string>
|
||||
<string name="applist_menu_quit_and_start">Выйти из текущей игры и запустить</string>
|
||||
<string name="applist_menu_cancel">Отмена</string>
|
||||
<string name="applist_refresh_title">Список приложений</string>
|
||||
<string name="applist_refresh_msg">Обновление приложений…</string>
|
||||
<string name="applist_refresh_error_title">Ошибка</string>
|
||||
<string name="applist_refresh_error_msg">Ошибка при получении списка приложений</string>
|
||||
<string name="applist_quit_app">Выход из</string>
|
||||
<string name="applist_quit_success">Выход произошёл успешно из</string>
|
||||
<string name="applist_quit_fail">Ошибка при выходе</string>
|
||||
<string name="applist_quit_confirmation">Вы уверены, что хотите выйти из запущенного приложения? Все несохраненные данные будут потеряны.</string>
|
||||
|
||||
<!-- Add computer manually activity -->
|
||||
<string name="title_add_pc">Добавление PC вручную</string>
|
||||
<string name="msg_add_pc">Соединение с PC…</string>
|
||||
<string name="addpc_fail">Не удалось подключиться к выбранному компьютеру. Удостоверьтесь, что необходимые порты разрешены в настройках брандмауэра.</string>
|
||||
<string name="addpc_success">Компьютер добавлен успешно.</string>
|
||||
<string name="addpc_unknown_host">Не удалось найти PC по указанному адресу. Убедитесь, что вы не совершили ошибок во время его написания.</string>
|
||||
<string name="addpc_enter_ip">Вы должны ввести IP адрес</string>
|
||||
|
||||
<!-- Preferences -->
|
||||
<string name="category_basic_settings">Базовые Настройки</string>
|
||||
<string name="title_resolution_list">Выберите разрешение и частоту кадров.</string>
|
||||
<string name="summary_resolution_list">Выбор слишком высокого значеня для своего устройства может вызвать тормоза или вылеты.</string>
|
||||
<string name="title_seekbar_bitrate">Выберите битрейт видео.</string>
|
||||
<string name="summary_seekbar_bitrate">Низкий битрейт уменьшит зависания. Увеличение битрейта улучшит качество изображения.</string>
|
||||
<string name="suffix_seekbar_bitrate">Mbps</string>
|
||||
<string name="title_checkbox_stretch_video">Растягивать видео на весь экран</string>
|
||||
<string name="title_checkbox_disable_warnings">Отключить сообщения с предупреждениями</string>
|
||||
<string name="summary_checkbox_disable_warnings">Выключить экранные предупреждения о соединении во время стрима.</string>
|
||||
|
||||
<string name="category_audio_settings">Аудио Настройки</string>
|
||||
<string name="title_checkbox_51_surround">Включить объёмный звук 5.1</string>
|
||||
<string name="summary_checkbox_51_surround">Отключите, если появляются аудио проблемы. Требуется GFE 2.7 или выше.</string>
|
||||
|
||||
<string name="category_gamepad_settings">Настройки Гемпада</string>
|
||||
<string name="title_checkbox_multi_controller">Поддержка нескольких контроллеров</string>
|
||||
<string name="summary_checkbox_multi_controller">Когда отключена, все контроллеры определяются как один. </string>
|
||||
<string name="title_seekbar_deadzone">Регулировать мертвую зону аналогового стика.</string>
|
||||
<string name="suffix_seekbar_deadzone">%</string>
|
||||
<string name="title_checkbox_xb1_driver">Драйвер контроллера от Xbox One</string>
|
||||
<string name="summary_checkbox_xb1_driver">Включить встроенный USB драйвер для устройств без встроенной поддержки контроллера от Xbox One.</string>
|
||||
|
||||
<string name="category_ui_settings">Настройки Интерфейса</string>
|
||||
<string name="title_language_list">Язык</string>
|
||||
<string name="summary_language_list">Язык, который будет использоваться в Moonlight</string>
|
||||
<string name="title_checkbox_list_mode">Использовать списки вместо сеток.</string>
|
||||
<string name="summary_checkbox_list_mode">Выводить приложения и компьютеры списком, вместо использования сетки.</string>
|
||||
<string name="title_checkbox_small_icon_mode">Использовать маленькие иконки</string>
|
||||
<string name="summary_checkbox_small_icon_mode">Использовать маленькие иконки в сетки для увеличения числа элементов, отображаемых на экране.</string>
|
||||
|
||||
<string name="category_host_settings">Настройки Хоста</string>
|
||||
<string name="title_checkbox_enable_sops">Оптимизировать игровые настройки</string>
|
||||
<string name="summary_checkbox_enable_sops">Разрешить GFE изменять настройки игр для оптимальной потоковой передачи</string>
|
||||
<string name="title_checkbox_host_audio">Проигрывать звук на PC</string>
|
||||
<string name="summary_checkbox_host_audio">Проигрывать звук на компьютере и текущем устройстве.</string>
|
||||
|
||||
<string name="category_advanced_settings">Расширенные Настройки</string>
|
||||
<string name="title_video_format">Изменить настройки H.265</string>
|
||||
<string name="summary_video_format">H.265 снижает требования к пропускной способности, но требует очень свежих устройств.</string>
|
||||
|
||||
</resources>
|
||||
@@ -21,11 +21,17 @@
|
||||
<item>Default</item>
|
||||
<item>English</item>
|
||||
<item>Italiano</item>
|
||||
<item>Japanese</item>
|
||||
<item>Русский</item>
|
||||
<item>Nederlands</item>
|
||||
</string-array>
|
||||
<string-array name="language_values" translatable="false">
|
||||
<item>default</item>
|
||||
<item>en</item>
|
||||
<item>it</item>
|
||||
<item>ja</item>
|
||||
<item>ru</item>
|
||||
<item>nl</item>
|
||||
</string-array>
|
||||
|
||||
<string-array name="decoder_names">
|
||||
@@ -40,8 +46,8 @@
|
||||
</string-array>
|
||||
|
||||
<string-array name="video_format_names">
|
||||
<item>Use H.265 only if safe</item>
|
||||
<item>Always use H.265 if available</item>
|
||||
<item>Use H.265 only if stable</item>
|
||||
<item>Always use H.265 (may crash)</item>
|
||||
<item>Never use H.265</item>
|
||||
</string-array>
|
||||
<string-array name="video_format_values" translatable="false">
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<!-- Shortcut strings -->
|
||||
<string name="scut_deleted_pc">PC deleted</string>
|
||||
<string name="scut_not_paired">PC not paired</string>
|
||||
|
||||
<!-- Help strings -->
|
||||
<string name="help_loading_title">Help Viewer</string>
|
||||
<string name="help_loading_msg">Loading help page…</string>
|
||||
|
||||
<!-- PC view menu entries -->
|
||||
<string name="pcview_menu_app_list">View Game List</string>
|
||||
@@ -16,6 +23,7 @@
|
||||
<string name="pair_pairing_msg">Please enter the following PIN on the target PC:</string>
|
||||
<string name="pair_incorrect_pin">Incorrect PIN</string>
|
||||
<string name="pair_fail">Pairing failed</string>
|
||||
<string name="pair_already_in_progress">Pairing already in progress</string>
|
||||
|
||||
<!-- WOL messages -->
|
||||
<string name="wol_pc_online">Computer is online</string>
|
||||
@@ -55,13 +63,16 @@
|
||||
|
||||
<!-- General strings -->
|
||||
<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 with GameStream running…\n\n
|
||||
Ensure GameStream is enabled in the GeForce Experience SHIELD settings.</string>
|
||||
<string name="yes">Yes</string>
|
||||
<string name="no">No</string>
|
||||
<string name="lost_connection">Lost connection to PC</string>
|
||||
<string name="help">Help</string>
|
||||
|
||||
<!-- AppList activity -->
|
||||
<string name="title_applist">Apps on</string>
|
||||
<string name="applist_connect_msg">Connecting to PC…</string>
|
||||
<string name="applist_menu_resume">Resume Session</string>
|
||||
<string name="applist_menu_quit">Quit Session</string>
|
||||
<string name="applist_menu_quit_and_start">Quit Current Game and Start</string>
|
||||
|
||||
@@ -4,7 +4,7 @@ buildscript {
|
||||
jcenter()
|
||||
}
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:2.0.0-beta6'
|
||||
classpath 'com.android.tools.build:gradle:2.2.2'
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
# Project-wide Gradle settings.
|
||||
|
||||
# IDE (e.g. Android Studio) users:
|
||||
# Settings specified in this file will override any Gradle settings
|
||||
# configured through the IDE.
|
||||
|
||||
# For more details on how to configure your build environment visit
|
||||
# http://www.gradle.org/docs/current/userguide/build_environment.html
|
||||
|
||||
# Specifies the JVM arguments used for the daemon process.
|
||||
# The setting is particularly useful for tweaking memory settings.
|
||||
# Default value: -Xmx10248m -XX:MaxPermSize=256m
|
||||
# org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
|
||||
org.gradle.jvmargs=-Xmx3072m
|
||||
|
||||
# When configured, Gradle will run in incubating parallel mode.
|
||||
# This option should only be used with decoupled projects. More details, visit
|
||||
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
|
||||
# org.gradle.parallel=true
|
||||
@@ -1,6 +1,6 @@
|
||||
#Sat Feb 06 16:21:20 EST 2016
|
||||
#Fri Aug 26 22:01:33 PDT 2016
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-2.10-all.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-2.14.1-all.zip
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
<content url="file://$MODULE_DIR$">
|
||||
<excludeFolder url="file://$MODULE_DIR$/.gradle" />
|
||||
</content>
|
||||
<orderEntry type="inheritedJdk" />
|
||||
<orderEntry type="jdk" jdkName="1.8" jdkType="JavaSDK" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
</component>
|
||||
</module>
|
||||