mirror of
https://github.com/moonlight-stream/moonlight-android.git
synced 2026-10-20 15:27:18 +00:00
Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fdbecfa6dd | ||
|
|
5e45aeb70b |
No files matched your search
+1
-3
@@ -30,8 +30,6 @@ Thumbs.db
|
|||||||
#.idea/workspace.xml - remove # and delete .idea if it better suit your needs.
|
#.idea/workspace.xml - remove # and delete .idea if it better suit your needs.
|
||||||
.gradle
|
.gradle
|
||||||
build/
|
build/
|
||||||
app/app.iml
|
|
||||||
|
|
||||||
# Compiled JNI libraries folder
|
# Compiled JNI libraries folder
|
||||||
**/jniLibs
|
**/jniLibs
|
||||||
app/.externalNativeBuild/
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
[submodule "moonlight-common"]
|
|
||||||
path = moonlight-common
|
|
||||||
url = https://github.com/moonlight-stream/moonlight-common.git
|
|
||||||
@@ -20,7 +20,7 @@ function p_h264raw.dissector(buf, pkt, root)
|
|||||||
|
|
||||||
local i = 0
|
local i = 0
|
||||||
local data_start = -1
|
local data_start = -1
|
||||||
while i < buf:len() do
|
while i < buf:len do
|
||||||
-- Make sure we have a potential start sequence and type
|
-- Make sure we have a potential start sequence and type
|
||||||
if buf:len() - i < 5 then
|
if buf:len() - i < 5 then
|
||||||
-- We need more data
|
-- We need more data
|
||||||
|
|||||||
@@ -1,44 +1,44 @@
|
|||||||
# Moonlight
|
#Limelight
|
||||||
|
|
||||||
[Moonlight](http://moonlight-stream.com) is an open source implementation of NVIDIA's GameStream, as used by the NVIDIA Shield.
|
Limelight is an open source implementation of NVIDIA's GameStream, as used by the NVIDIA Shield.
|
||||||
We reverse engineered the Shield streaming software and created a version that can be run on any Android device.
|
We reverse engineered the Shield streaming software and created a version that can be run on any Android device.
|
||||||
|
|
||||||
Moonlight will allow you to stream your full collection of games from your Windows PC to your Android device,
|
Limelight will allow you to stream your full collection of games from your Windows PC to your Android device,
|
||||||
whether in your own home or over the internet.
|
whether in your own home or over the internet.
|
||||||
|
|
||||||
[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.
|
[Limelight-pc](https://github.com/limelight-stream/limelight-pc) is also currently in development for Windows, OS X and Linux. Versions for [iOS](https://github.com/limelight-stream/limelight-ios) and [Windows and Windows Phone](https://github.com/limelight-stream/limelight-windows) are also in development.
|
||||||
|
|
||||||
Check our [wiki](https://github.com/moonlight-stream/moonlight-docs/wiki) for more detailed information or a troubleshooting guide.
|
Check our [wiki](https://github.com/limelight-stream/limelight-android/wiki) for more detailed information or a troubleshooting guide.
|
||||||
|
|
||||||
## Features
|
##Features
|
||||||
|
|
||||||
* Streams any of your games from your PC to your Android device
|
* Streams any of your games from your PC to your Android device
|
||||||
* Full gamepad support for MOGA, Xbox 360, PS3, OUYA, and Shield
|
* Full gamepad support for MOGA, Xbox 360, PS3, OUYA, and Shield
|
||||||
* Automatically finds GameStream-compatible PCs on your network
|
* Automatically finds GameStream-compatible PCs on your network
|
||||||
|
|
||||||
## Installation
|
##Installation
|
||||||
|
|
||||||
* Download and install Moonlight for Android from
|
* Download and install Limelight for Android from
|
||||||
[Google Play](https://play.google.com/store/apps/details?id=com.limelight), [Amazon App Store](http://www.amazon.com/gp/product/B00JK4MFN2), or directly from the [releases page](https://github.com/moonlight-stream/moonlight-android/releases)
|
[Google Play](https://play.google.com/store/apps/details?id=com.limelight)
|
||||||
* Download [GeForce Experience](http://www.geforce.com/geforce-experience) and install on your Windows PC
|
* Download [GeForce Experience](http://www.geforce.com/geforce-experience) and install on your Windows PC
|
||||||
|
|
||||||
## Requirements
|
##Requirements
|
||||||
|
|
||||||
* [GameStream compatible](http://shield.nvidia.com/play-pc-games/) computer with an NVIDIA GeForce GTX 600 series or higher desktop or mobile GPU (GT-series and AMD GPUs not supported)
|
* [GameStream compatible](http://shield.nvidia.com/play-pc-games/) computer with GTX 600/700 series GPU
|
||||||
* Android device running 4.1 (Jelly Bean) or higher
|
* Android device running 4.1 (Jelly Bean) or higher
|
||||||
* High-end wireless router (802.11n dual-band recommended)
|
* High-end wireless router (802.11n dual-band recommended)
|
||||||
|
|
||||||
## Usage
|
##Usage
|
||||||
|
|
||||||
* Turn on GameStream in the GFE settings
|
* Turn on GameStream in the GFE settings
|
||||||
* If you are connecting from outside the same network, turn on internet
|
* If you are connecting from outside the same network, turn on internet
|
||||||
streaming
|
streaming
|
||||||
* When on the same network as your PC, open Moonlight and tap on your PC in the list
|
* When on the same network as your PC, open Limelight and tap on your PC in the list
|
||||||
* Accept the pairing confirmation on your PC and add the PIN if needed
|
* Accept the pairing confirmation on your PC
|
||||||
* Tap your PC again to view the list of apps to stream
|
* Tap your PC again to view the list of apps to stream
|
||||||
* Play games!
|
* Play games!
|
||||||
|
|
||||||
## Contribute
|
##Contribute
|
||||||
|
|
||||||
This project is being actively developed at [XDA Developers](http://forum.xda-developers.com/showthread.php?t=2505510)
|
This project is being actively developed at [XDA Developers](http://forum.xda-developers.com/showthread.php?t=2505510)
|
||||||
|
|
||||||
@@ -46,18 +46,14 @@ This project is being actively developed at [XDA Developers](http://forum.xda-de
|
|||||||
2. Write code
|
2. Write code
|
||||||
3. Send Pull Requests
|
3. Send Pull Requests
|
||||||
|
|
||||||
## Building
|
Check out our [website](http://limelight-stream.com) for project links and information.
|
||||||
* 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
|
##Authors
|
||||||
|
|
||||||
* [Cameron Gutman](https://github.com/cgutman)
|
* [Cameron Gutman](https://github.com/cgutman)
|
||||||
* [Diego Waxemberg](https://github.com/dwaxemberg)
|
* [Diego Waxemberg](https://github.com/dwaxemberg)
|
||||||
* [Aaron Neyer](https://github.com/Aaronneyer)
|
* [Aaron Neyer](https://github.com/Aaronneyer)
|
||||||
* [Andrew Hennessy](https://github.com/yetanothername)
|
* [Andrew Hennessy](https://github.com/yetanothername)
|
||||||
|
|
||||||
Moonlight is the work of students at [Case Western](http://case.edu) and was
|
Limelight is the work of students at [Case Western](http://case.edu) and was
|
||||||
started as a project at [MHacks](http://mhacks.org).
|
started as a project at [MHacks](http://mhacks.org).
|
||||||
+120
@@ -0,0 +1,120 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<module external.linked.project.path="$MODULE_DIR$" external.root.project.path="$MODULE_DIR$/.." external.system.id="GRADLE" external.system.module.group="limelight-android" external.system.module.version="unspecified" type="JAVA_MODULE" version="4">
|
||||||
|
<component name="FacetManager">
|
||||||
|
<facet type="android-gradle" name="Android-Gradle">
|
||||||
|
<configuration>
|
||||||
|
<option name="GRADLE_PROJECT_PATH" value=":app" />
|
||||||
|
</configuration>
|
||||||
|
</facet>
|
||||||
|
<facet type="android" name="Android">
|
||||||
|
<configuration>
|
||||||
|
<option name="SELECTED_BUILD_VARIANT" value="nonRootDebug" />
|
||||||
|
<option name="ASSEMBLE_TASK_NAME" value="assembleNonRootDebug" />
|
||||||
|
<option name="COMPILE_JAVA_TASK_NAME" value="compileNonRootDebugSources" />
|
||||||
|
<option name="ASSEMBLE_TEST_TASK_NAME" value="assembleNonRootDebugTest" />
|
||||||
|
<option name="SOURCE_GEN_TASK_NAME" value="generateNonRootDebugSources" />
|
||||||
|
<option name="TEST_SOURCE_GEN_TASK_NAME" value="generateNonRootDebugTestSources" />
|
||||||
|
<option name="ALLOW_USER_CONFIGURATION" value="false" />
|
||||||
|
<option name="MANIFEST_FILE_RELATIVE_PATH" value="/src/main/AndroidManifest.xml" />
|
||||||
|
<option name="RES_FOLDER_RELATIVE_PATH" value="/src/main/res" />
|
||||||
|
<option name="RES_FOLDERS_RELATIVE_PATH" value="file://$MODULE_DIR$/src/main/res" />
|
||||||
|
<option name="ASSETS_FOLDER_RELATIVE_PATH" value="/src/main/assets" />
|
||||||
|
</configuration>
|
||||||
|
</facet>
|
||||||
|
</component>
|
||||||
|
<component name="NewModuleRootManager" inherit-compiler-output="false">
|
||||||
|
<output url="file://$MODULE_DIR$/build/intermediates/classes/nonRoot/debug" />
|
||||||
|
<exclude-output />
|
||||||
|
<content url="file://$MODULE_DIR$">
|
||||||
|
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/r/nonRoot/debug" isTestSource="false" generated="true" />
|
||||||
|
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/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/res/rs/nonRoot/debug" type="java-resource" />
|
||||||
|
<sourceFolder url="file://$MODULE_DIR$/build/generated/res/generated/nonRoot/debug" type="java-resource" />
|
||||||
|
<sourceFolder url="file://$MODULE_DIR$/src/nonRootDebug/res" type="java-resource" />
|
||||||
|
<sourceFolder url="file://$MODULE_DIR$/src/nonRootDebug/resources" type="java-resource" />
|
||||||
|
<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$/build/generated/source/r/test/nonRoot/debug" isTestSource="true" generated="true" />
|
||||||
|
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/aidl/test/nonRoot/debug" isTestSource="true" generated="true" />
|
||||||
|
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/buildConfig/test/nonRoot/debug" isTestSource="true" generated="true" />
|
||||||
|
<sourceFolder url="file://$MODULE_DIR$/build/generated/source/rs/test/nonRoot/debug" isTestSource="true" generated="true" />
|
||||||
|
<sourceFolder url="file://$MODULE_DIR$/build/generated/res/rs/test/nonRoot/debug" type="java-test-resource" />
|
||||||
|
<sourceFolder url="file://$MODULE_DIR$/build/generated/res/generated/test/nonRoot/debug" type="java-test-resource" />
|
||||||
|
<sourceFolder url="file://$MODULE_DIR$/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/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/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/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/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" />
|
||||||
|
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/assets" />
|
||||||
|
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/bundles" />
|
||||||
|
<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/manifests" />
|
||||||
|
<excludeFolder url="file://$MODULE_DIR$/build/intermediates/ndk" />
|
||||||
|
<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/symbols" />
|
||||||
|
<excludeFolder url="file://$MODULE_DIR$/build/outputs" />
|
||||||
|
<excludeFolder url="file://$MODULE_DIR$/build/tmp" />
|
||||||
|
</content>
|
||||||
|
<orderEntry type="jdk" jdkName="Android API 21 Platform" jdkType="Android SDK" />
|
||||||
|
<orderEntry type="sourceFolder" forTests="false" />
|
||||||
|
<orderEntry type="library" exported="" name="bcprov-jdk15on-1.51" level="project" />
|
||||||
|
<orderEntry type="library" exported="" name="androidasync-2.0.5" level="project" />
|
||||||
|
<orderEntry type="library" exported="" name="gson-2.3.1" level="project" />
|
||||||
|
<orderEntry type="library" exported="" name="support-v4-r7" level="project" />
|
||||||
|
<orderEntry type="library" exported="" name="jmdns-fixed" level="project" />
|
||||||
|
<orderEntry type="library" exported="" name="bcpkix-jdk15on-1.51" level="project" />
|
||||||
|
<orderEntry type="library" exported="" name="tinyrtsp" level="project" />
|
||||||
|
<orderEntry type="library" exported="" name="limelight-common" level="project" />
|
||||||
|
<orderEntry type="library" exported="" name="ion-2.0.5" level="project" />
|
||||||
|
<orderEntry type="library" exported="" name="okhttp-2.2.0" level="project" />
|
||||||
|
<orderEntry type="library" exported="" name="jcodec-0.1.9" level="project" />
|
||||||
|
<orderEntry type="library" exported="" name="okio-1.2.0" level="project" />
|
||||||
|
</component>
|
||||||
|
</module>
|
||||||
|
|
||||||
+45
-33
@@ -4,65 +4,77 @@ import org.apache.tools.ant.taskdefs.condition.Os
|
|||||||
apply plugin: 'com.android.application'
|
apply plugin: 'com.android.application'
|
||||||
|
|
||||||
android {
|
android {
|
||||||
compileSdkVersion 26
|
compileSdkVersion 21
|
||||||
buildToolsVersion '26.0.1'
|
buildToolsVersion "21.1.2"
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
minSdkVersion 16
|
minSdkVersion 16
|
||||||
targetSdkVersion 26
|
targetSdkVersion 21
|
||||||
|
|
||||||
versionName "5.2"
|
versionName "3.1-beta1"
|
||||||
versionCode = 131
|
versionCode = 50
|
||||||
}
|
}
|
||||||
|
|
||||||
flavorDimensions "root"
|
|
||||||
|
|
||||||
productFlavors {
|
productFlavors {
|
||||||
root {
|
root {
|
||||||
// Android O has native mouse capture, so don't show the rooted
|
|
||||||
// version to devices running O on the Play Store.
|
|
||||||
maxSdkVersion 25
|
|
||||||
|
|
||||||
applicationId "com.limelight.root"
|
applicationId "com.limelight.root"
|
||||||
ndk {
|
|
||||||
abiFilters "armeabi-v7a", "arm64-v8a", "x86", "x86_64", "mips", "mips64"
|
|
||||||
}
|
|
||||||
|
|
||||||
dimension "root"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
nonRoot {
|
nonRoot {
|
||||||
applicationId "com.limelight"
|
applicationId "com.limelight"
|
||||||
ndk {
|
|
||||||
abiFilters "armeabi-v7a", "arm64-v8a", "x86", "x86_64", "mips", "mips64"
|
|
||||||
}
|
|
||||||
|
|
||||||
dimension "root"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
lintOptions {
|
|
||||||
disable 'MissingTranslation'
|
|
||||||
}
|
|
||||||
|
|
||||||
buildTypes {
|
buildTypes {
|
||||||
release {
|
release {
|
||||||
minifyEnabled false
|
minifyEnabled false
|
||||||
proguardFiles getDefaultProguardFile('proguard-android.txt')
|
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
externalNativeBuild {
|
sourceSets.main.jni.srcDirs = []
|
||||||
ndkBuild {
|
|
||||||
path "src/main/jni/Android.mk"
|
//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'
|
||||||
}
|
}
|
||||||
|
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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation 'org.bouncycastle:bcprov-jdk15on:1.57'
|
compile group: 'org.jcodec', name: 'jcodec', version: '+'
|
||||||
implementation 'org.bouncycastle:bcpkix-jdk15on:1.57'
|
|
||||||
implementation files('libs/jcodec-0.1.9-patched.jar')
|
|
||||||
|
|
||||||
implementation project(':moonlight-common')
|
compile group: 'org.bouncycastle', name: 'bcprov-jdk15on', version: '+'
|
||||||
|
compile group: 'org.bouncycastle', name: 'bcpkix-jdk15on', version: '+'
|
||||||
|
|
||||||
|
compile group: 'com.google.android', name: 'support-v4', version:'+'
|
||||||
|
compile group: 'com.koushikdutta.ion', name: 'ion', version:'+'
|
||||||
|
compile group: 'com.google.code.gson', name: 'gson', version:'+'
|
||||||
|
|
||||||
|
compile group: 'com.squareup.okhttp', name: 'okhttp', version:'+'
|
||||||
|
compile group: 'com.squareup.okio', name:'okio', version:'+'
|
||||||
|
|
||||||
|
compile files('libs/jmdns-fixed.jar')
|
||||||
|
compile files('libs/limelight-common.jar')
|
||||||
|
compile files('libs/tinyrtsp.jar')
|
||||||
}
|
}
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -7,127 +7,75 @@
|
|||||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||||
<uses-permission android:name="android.permission.CHANGE_WIFI_MULTICAST_STATE" />
|
<uses-permission android:name="android.permission.CHANGE_WIFI_MULTICAST_STATE" />
|
||||||
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
|
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
|
||||||
|
|
||||||
<uses-feature
|
<uses-feature android:name="android.hardware.touchscreen" android:required="false" />
|
||||||
android:name="android.hardware.touchscreen"
|
<uses-feature android:name="android.hardware.wifi" android:required="false" />
|
||||||
android:required="false" />
|
<uses-feature android:name="android.hardware.gamepad" 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" />
|
|
||||||
|
|
||||||
<!-- Disable legacy input emulation on ChromeOS -->
|
|
||||||
<uses-feature
|
|
||||||
android:name="android.hardware.type.pc"
|
|
||||||
android:required="false"/>
|
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:allowBackup="true"
|
android:allowBackup="true"
|
||||||
android:fullBackupContent="@xml/backup_rules"
|
android:icon="@drawable/ic_launcher"
|
||||||
android:isGame="true"
|
android:theme="@style/AppTheme" >
|
||||||
android:banner="@drawable/atv_banner"
|
|
||||||
android:appCategory="game"
|
<!-- Launcher for traditional devices -->
|
||||||
android:icon="@mipmap/ic_launcher"
|
|
||||||
android:roundIcon="@mipmap/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" />
|
|
||||||
|
|
||||||
<!-- Samsung DeX support requires explicit placement of android:resizeableActivity="true"
|
|
||||||
in each activity even though it is implied by targeting API 24+ -->
|
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".PcView"
|
android:name=".PcView"
|
||||||
android:resizeableActivity="true"
|
android:configChanges="mcc|mnc|locale|touchscreen|keyboard|keyboardHidden|navigation|screenLayout|fontScale|uiMode|orientation|screenSize|smallestScreenSize|layoutDirection">
|
||||||
android:configChanges="mcc|mnc|touchscreen|keyboard|keyboardHidden|navigation|screenLayout|fontScale|uiMode|orientation|screenSize|smallestScreenSize|layoutDirection">
|
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN" />
|
<action android:name="android.intent.action.MAIN" />
|
||||||
|
|
||||||
<category android:name="android.intent.category.LAUNCHER" />
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
<category android:name="android.intent.category.MULTIWINDOW_LAUNCHER" />
|
|
||||||
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
|
|
||||||
<category android:name="tv.ouya.intent.category.APP" />
|
<category android:name="tv.ouya.intent.category.APP" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
<!-- Small hack to support launcher shortcuts without relaunching over and over again when the back button is pressed -->
|
|
||||||
|
<!-- Launcher for Android TV devices -->
|
||||||
<activity
|
<activity
|
||||||
android:name=".AppViewShortcutTrampoline"
|
android:name=".PcViewTv"
|
||||||
android:noHistory="true"
|
android:logo="@drawable/atv_banner"
|
||||||
android:resizeableActivity="true"
|
android:icon="@drawable/atv_banner"
|
||||||
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
|
<intent-filter>
|
||||||
android:name="android.support.PARENT_ACTIVITY"
|
<action android:name="android.intent.action.MAIN" />
|
||||||
android:value="com.limelight.PcView" />
|
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
|
||||||
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".AppView"
|
android:name=".AppView"
|
||||||
android:resizeableActivity="true"
|
android:configChanges="mcc|mnc|locale|touchscreen|keyboard|keyboardHidden|navigation|screenLayout|fontScale|uiMode|orientation|screenSize|smallestScreenSize|layoutDirection" >
|
||||||
android:configChanges="mcc|mnc|touchscreen|keyboard|keyboardHidden|navigation|screenLayout|fontScale|uiMode|orientation|screenSize|smallestScreenSize|layoutDirection">
|
|
||||||
<meta-data
|
<meta-data
|
||||||
android:name="android.support.PARENT_ACTIVITY"
|
android:name="android.support.PARENT_ACTIVITY"
|
||||||
android:value="com.limelight.PcView" />
|
android:value="com.limelight.PcView" />
|
||||||
</activity>
|
</activity>
|
||||||
<activity
|
<activity
|
||||||
android:name=".preferences.StreamSettings"
|
android:name=".preferences.StreamSettings"
|
||||||
android:resizeableActivity="true"
|
android:label="Streaming Settings" >
|
||||||
android:label="Streaming Settings">
|
|
||||||
<meta-data
|
<meta-data
|
||||||
android:name="android.support.PARENT_ACTIVITY"
|
android:name="android.support.PARENT_ACTIVITY"
|
||||||
android:value="com.limelight.PcView" />
|
android:value="com.limelight.PcView" />
|
||||||
</activity>
|
</activity>
|
||||||
<activity
|
<activity
|
||||||
android:name=".preferences.AddComputerManually"
|
android:name=".preferences.AddComputerManually"
|
||||||
android:resizeableActivity="true"
|
android:label="Add Computer Manually" >
|
||||||
android:label="Add Computer Manually">
|
|
||||||
<meta-data
|
<meta-data
|
||||||
android:name="android.support.PARENT_ACTIVITY"
|
android:name="android.support.PARENT_ACTIVITY"
|
||||||
android:value="com.limelight.PcView" />
|
android:value="com.limelight.PcView" />
|
||||||
</activity>
|
</activity>
|
||||||
<activity
|
<activity
|
||||||
android:name=".Game"
|
android:name=".Game"
|
||||||
android:configChanges="mcc|mnc|locale|touchscreen|keyboard|keyboardHidden|navigation|screenLayout|fontScale|uiMode|orientation|screenSize|smallestScreenSize|layoutDirection"
|
|
||||||
android:screenOrientation="sensorLandscape"
|
android:screenOrientation="sensorLandscape"
|
||||||
android:noHistory="true"
|
android:theme="@style/StreamTheme"
|
||||||
android:resizeableActivity="true"
|
android:configChanges="mcc|mnc|locale|touchscreen|keyboard|keyboardHidden|navigation|screenLayout|fontScale|uiMode|orientation|screenSize|smallestScreenSize|layoutDirection" >
|
||||||
android:theme="@style/StreamTheme">
|
|
||||||
<meta-data
|
<meta-data
|
||||||
android:name="android.support.PARENT_ACTIVITY"
|
android:name="android.support.PARENT_ACTIVITY"
|
||||||
android:value="com.limelight.AppView" />
|
android:value="com.limelight.AppView" />
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
<service
|
<service
|
||||||
android:name=".discovery.DiscoveryService"
|
android:name=".discovery.DiscoveryService"
|
||||||
android:label="mDNS PC Auto-Discovery Service" />
|
android:label="mDNS PC Auto-Discovery Service" />
|
||||||
<service
|
<service
|
||||||
android:name=".computers.ComputerManagerService"
|
android:name=".computers.ComputerManagerService"
|
||||||
android:label="Computer Management Service" />
|
android:label="Computer Management Service" />
|
||||||
<service
|
|
||||||
android:name=".binding.input.driver.UsbDriverService"
|
|
||||||
android:label="Usb Driver Service" />
|
|
||||||
|
|
||||||
<activity
|
|
||||||
android:name=".HelpActivity"
|
|
||||||
android:resizeableActivity="true"
|
|
||||||
android:configChanges="mcc|mnc|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>
|
|
||||||
</application>
|
</application>
|
||||||
|
|
||||||
</manifest>
|
</manifest>
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 58 KiB |
@@ -1,31 +1,43 @@
|
|||||||
package com.limelight;
|
package com.limelight;
|
||||||
|
|
||||||
|
import java.io.FileNotFoundException;
|
||||||
|
import java.io.FileOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStreamReader;
|
||||||
import java.io.StringReader;
|
import java.io.StringReader;
|
||||||
|
import java.net.InetAddress;
|
||||||
|
import java.net.UnknownHostException;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
|
import org.xmlpull.v1.XmlPullParserException;
|
||||||
|
|
||||||
|
import com.limelight.binding.PlatformBinding;
|
||||||
|
import com.limelight.binding.crypto.AndroidCryptoProvider;
|
||||||
import com.limelight.computers.ComputerManagerListener;
|
import com.limelight.computers.ComputerManagerListener;
|
||||||
import com.limelight.computers.ComputerManagerService;
|
import com.limelight.computers.ComputerManagerService;
|
||||||
import com.limelight.grid.AppGridAdapter;
|
import com.limelight.grid.AppGridAdapter;
|
||||||
import com.limelight.nvstream.http.ComputerDetails;
|
import com.limelight.nvstream.http.ComputerDetails;
|
||||||
|
import com.limelight.nvstream.http.GfeHttpResponseException;
|
||||||
import com.limelight.nvstream.http.NvApp;
|
import com.limelight.nvstream.http.NvApp;
|
||||||
import com.limelight.nvstream.http.NvHTTP;
|
import com.limelight.nvstream.http.NvHTTP;
|
||||||
import com.limelight.nvstream.http.PairingManager;
|
|
||||||
import com.limelight.preferences.PreferenceConfiguration;
|
import com.limelight.preferences.PreferenceConfiguration;
|
||||||
import com.limelight.ui.AdapterFragment;
|
import com.limelight.ui.AdapterFragment;
|
||||||
import com.limelight.ui.AdapterFragmentCallbacks;
|
import com.limelight.ui.AdapterFragmentCallbacks;
|
||||||
import com.limelight.utils.CacheHelper;
|
import com.limelight.utils.CacheHelper;
|
||||||
import com.limelight.utils.Dialog;
|
import com.limelight.utils.Dialog;
|
||||||
import com.limelight.utils.ServerHelper;
|
|
||||||
import com.limelight.utils.ShortcutHelper;
|
|
||||||
import com.limelight.utils.SpinnerDialog;
|
import com.limelight.utils.SpinnerDialog;
|
||||||
import com.limelight.utils.UiHelper;
|
import com.limelight.utils.UiHelper;
|
||||||
|
|
||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
|
import android.app.AlertDialog;
|
||||||
import android.app.Service;
|
import android.app.Service;
|
||||||
import android.content.ComponentName;
|
import android.content.ComponentName;
|
||||||
|
import android.content.DialogInterface;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.content.ServiceConnection;
|
import android.content.ServiceConnection;
|
||||||
|
import android.content.res.Configuration;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.os.IBinder;
|
import android.os.IBinder;
|
||||||
import android.view.ContextMenu;
|
import android.view.ContextMenu;
|
||||||
@@ -36,6 +48,7 @@ import android.view.ContextMenu.ContextMenuInfo;
|
|||||||
import android.widget.AbsListView;
|
import android.widget.AbsListView;
|
||||||
import android.widget.AdapterView;
|
import android.widget.AdapterView;
|
||||||
import android.widget.AdapterView.OnItemClickListener;
|
import android.widget.AdapterView.OnItemClickListener;
|
||||||
|
import android.widget.Spinner;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
import android.widget.Toast;
|
import android.widget.Toast;
|
||||||
import android.widget.AdapterView.AdapterContextMenuInfo;
|
import android.widget.AdapterView.AdapterContextMenuInfo;
|
||||||
@@ -43,26 +56,25 @@ import android.widget.AdapterView.AdapterContextMenuInfo;
|
|||||||
public class AppView extends Activity implements AdapterFragmentCallbacks {
|
public class AppView extends Activity implements AdapterFragmentCallbacks {
|
||||||
private AppGridAdapter appGridAdapter;
|
private AppGridAdapter appGridAdapter;
|
||||||
private String uuidString;
|
private String uuidString;
|
||||||
private ShortcutHelper shortcutHelper;
|
|
||||||
|
|
||||||
private ComputerDetails computer;
|
private ComputerDetails computer;
|
||||||
private ComputerManagerService.ApplistPoller poller;
|
private ComputerManagerService.ApplistPoller poller;
|
||||||
private SpinnerDialog blockingLoadSpinner;
|
private SpinnerDialog blockingLoadSpinner;
|
||||||
private String lastRawApplist;
|
private String lastRawApplist;
|
||||||
private int lastRunningAppId;
|
|
||||||
private boolean suspendGridUpdates;
|
|
||||||
private boolean inForeground;
|
|
||||||
|
|
||||||
private final static int START_OR_RESUME_ID = 1;
|
private int consecutiveAppListFailures = 0;
|
||||||
private final static int QUIT_ID = 2;
|
private final static int CONSECUTIVE_FAILURE_LIMIT = 3;
|
||||||
private final static int CANCEL_ID = 3;
|
|
||||||
|
private final static int START_OR_RESUME_ID = 1;
|
||||||
|
private final static int QUIT_ID = 2;
|
||||||
|
private final static int CANCEL_ID = 3;
|
||||||
private final static int START_WTIH_QUIT = 4;
|
private final static int START_WTIH_QUIT = 4;
|
||||||
|
|
||||||
public final static String NAME_EXTRA = "Name";
|
public final static String NAME_EXTRA = "Name";
|
||||||
public final static String UUID_EXTRA = "UUID";
|
public final static String UUID_EXTRA = "UUID";
|
||||||
|
|
||||||
private ComputerManagerService.ComputerManagerBinder managerBinder;
|
private ComputerManagerService.ComputerManagerBinder managerBinder;
|
||||||
private final ServiceConnection serviceConnection = new ServiceConnection() {
|
private ServiceConnection serviceConnection = new ServiceConnection() {
|
||||||
public void onServiceConnected(ComponentName className, IBinder binder) {
|
public void onServiceConnected(ComponentName className, IBinder binder) {
|
||||||
final ComputerManagerService.ComputerManagerBinder localBinder =
|
final ComputerManagerService.ComputerManagerBinder localBinder =
|
||||||
((ComputerManagerService.ComputerManagerBinder)binder);
|
((ComputerManagerService.ComputerManagerBinder)binder);
|
||||||
@@ -79,10 +91,6 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
|
|
||||||
// Get the computer object
|
// Get the computer object
|
||||||
computer = managerBinder.getComputer(UUID.fromString(uuidString));
|
computer = managerBinder.getComputer(UUID.fromString(uuidString));
|
||||||
if (computer == null) {
|
|
||||||
finish();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
appGridAdapter = new AppGridAdapter(AppView.this,
|
appGridAdapter = new AppGridAdapter(AppView.this,
|
||||||
@@ -95,34 +103,15 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
return;
|
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
|
// Start updates
|
||||||
startComputerUpdates();
|
startComputerUpdates();
|
||||||
|
|
||||||
runOnUiThread(new Runnable() {
|
// Load the app grid with cached data (if possible)
|
||||||
@Override
|
populateAppGridWithCache();
|
||||||
public void run() {
|
|
||||||
if (isFinishing() || isChangingConfigurations()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Despite my best efforts to catch all conditions that could
|
getFragmentManager().beginTransaction()
|
||||||
// cause the activity to be destroyed when we try to commit
|
.replace(R.id.appFragmentContainer, new AdapterFragment())
|
||||||
// I haven't been able to, so we have this try-catch block.
|
.commitAllowingStateLoss();
|
||||||
try {
|
|
||||||
getFragmentManager().beginTransaction()
|
|
||||||
.replace(R.id.appFragmentContainer, new AdapterFragment())
|
|
||||||
.commitAllowingStateLoss();
|
|
||||||
} catch (IllegalStateException e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}.start();
|
}.start();
|
||||||
}
|
}
|
||||||
@@ -132,82 +121,58 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private InetAddress getAddress() {
|
||||||
|
return computer.reachability == ComputerDetails.Reachability.LOCAL ?
|
||||||
|
computer.localIp : computer.remoteIp;
|
||||||
|
}
|
||||||
|
|
||||||
private void startComputerUpdates() {
|
private void startComputerUpdates() {
|
||||||
// Don't start polling if we're not bound or in the foreground
|
if (managerBinder == null) {
|
||||||
if (managerBinder == null || !inForeground) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
managerBinder.startPolling(new ComputerManagerListener() {
|
managerBinder.startPolling(new ComputerManagerListener() {
|
||||||
@Override
|
@Override
|
||||||
public void notifyComputerUpdated(final ComputerDetails details) {
|
public void notifyComputerUpdated(ComputerDetails details) {
|
||||||
// Do nothing if updates are suspended
|
|
||||||
if (suspendGridUpdates) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Don't care about other computers
|
// Don't care about other computers
|
||||||
if (!details.uuid.toString().equalsIgnoreCase(uuidString)) {
|
if (details != computer) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (details.state == ComputerDetails.State.OFFLINE) {
|
if (details.state != ComputerDetails.State.ONLINE) {
|
||||||
// The PC is unreachable now
|
consecutiveAppListFailures++;
|
||||||
AppView.this.runOnUiThread(new Runnable() {
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
// Display a toast to the user and quit the activity
|
|
||||||
Toast.makeText(AppView.this, getResources().getText(R.string.lost_connection), Toast.LENGTH_SHORT).show();
|
|
||||||
finish();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return;
|
if (consecutiveAppListFailures >= CONSECUTIVE_FAILURE_LIMIT) {
|
||||||
}
|
// The PC is unreachable now
|
||||||
|
AppView.this.runOnUiThread(new Runnable() {
|
||||||
// Close immediately if the PC is no longer paired
|
@Override
|
||||||
if (details.state == ComputerDetails.State.ONLINE && details.pairState != PairingManager.PairState.PAIRED) {
|
public void run() {
|
||||||
AppView.this.runOnUiThread(new Runnable() {
|
// Display a toast to the user and quit the activity
|
||||||
@Override
|
Toast.makeText(AppView.this, getResources().getText(R.string.lost_connection), Toast.LENGTH_SHORT).show();
|
||||||
public void run() {
|
finish();
|
||||||
// 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)) {
|
|
||||||
|
|
||||||
// Let's check if the running app ID changed
|
|
||||||
if (details.runningGameId != lastRunningAppId) {
|
|
||||||
// Update the currently running game using the app ID
|
|
||||||
lastRunningAppId = details.runningGameId;
|
|
||||||
updateUiWithServerinfo(details);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
lastRunningAppId = details.runningGameId;
|
consecutiveAppListFailures = 0;
|
||||||
lastRawApplist = details.rawAppList;
|
|
||||||
|
// App list is the same or empty; nothing to do
|
||||||
|
if (details.rawAppList == null || details.rawAppList.equals(lastRawApplist)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
lastRawApplist = details.rawAppList;
|
||||||
updateUiWithAppList(NvHTTP.getAppListByReader(new StringReader(details.rawAppList)));
|
updateUiWithAppList(NvHTTP.getAppListByReader(new StringReader(details.rawAppList)));
|
||||||
updateUiWithServerinfo(details);
|
|
||||||
|
|
||||||
if (blockingLoadSpinner != null) {
|
if (blockingLoadSpinner != null) {
|
||||||
blockingLoadSpinner.dismiss();
|
blockingLoadSpinner.dismiss();
|
||||||
blockingLoadSpinner = null;
|
blockingLoadSpinner = null;
|
||||||
}
|
}
|
||||||
} catch (Exception ignored) {}
|
} catch (Exception e) {}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -225,41 +190,34 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
if (managerBinder != null) {
|
if (managerBinder != null) {
|
||||||
managerBinder.stopPolling();
|
managerBinder.stopPolling();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (appGridAdapter != null) {
|
|
||||||
appGridAdapter.cancelQueuedOperations();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
|
||||||
@Override
|
String locale = PreferenceConfiguration.readPreferences(this).language;
|
||||||
protected void onCreate(Bundle savedInstanceState) {
|
if (!locale.equals(PreferenceConfiguration.DEFAULT_LANGUAGE)) {
|
||||||
super.onCreate(savedInstanceState);
|
Configuration config = new Configuration(getResources().getConfiguration());
|
||||||
|
config.locale = new Locale(locale);
|
||||||
|
getResources().updateConfiguration(config, getResources().getDisplayMetrics());
|
||||||
|
}
|
||||||
|
|
||||||
shortcutHelper = new ShortcutHelper(this);
|
setContentView(R.layout.activity_app_view);
|
||||||
|
|
||||||
UiHelper.setLocale(this);
|
|
||||||
|
|
||||||
setContentView(R.layout.activity_app_view);
|
|
||||||
|
|
||||||
UiHelper.notifyNewRootView(this);
|
UiHelper.notifyNewRootView(this);
|
||||||
|
|
||||||
uuidString = getIntent().getStringExtra(UUID_EXTRA);
|
uuidString = getIntent().getStringExtra(UUID_EXTRA);
|
||||||
|
|
||||||
String computerName = getIntent().getStringExtra(NAME_EXTRA);
|
String labelText = getResources().getString(R.string.title_applist)+" "+getIntent().getStringExtra(NAME_EXTRA);
|
||||||
|
TextView label = (TextView) findViewById(R.id.appListText);
|
||||||
String labelText = getResources().getString(R.string.title_applist)+" "+computerName;
|
setTitle(labelText);
|
||||||
TextView label = findViewById(R.id.appListText);
|
label.setText(labelText);
|
||||||
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
|
// Bind to the computer manager service
|
||||||
bindService(new Intent(this, ComputerManagerService.class), serviceConnection,
|
bindService(new Intent(this, ComputerManagerService.class), serviceConnection,
|
||||||
Service.BIND_AUTO_CREATE);
|
Service.BIND_AUTO_CREATE);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void populateAppGridWithCache() {
|
private void populateAppGridWithCache() {
|
||||||
try {
|
try {
|
||||||
@@ -283,55 +241,101 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
blockingLoadSpinner = SpinnerDialog.displayDialog(this, getResources().getString(R.string.applist_refresh_title),
|
blockingLoadSpinner = SpinnerDialog.displayDialog(this, getResources().getString(R.string.applist_refresh_title),
|
||||||
getResources().getString(R.string.applist_refresh_msg), true);
|
getResources().getString(R.string.applist_refresh_msg), true);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onDestroy() {
|
protected void onDestroy() {
|
||||||
super.onDestroy();
|
super.onDestroy();
|
||||||
|
|
||||||
SpinnerDialog.closeDialogs(this);
|
SpinnerDialog.closeDialogs(this);
|
||||||
Dialog.closeDialogs();
|
Dialog.closeDialogs();
|
||||||
|
|
||||||
if (managerBinder != null) {
|
if (managerBinder != null) {
|
||||||
unbindService(serviceConnection);
|
unbindService(serviceConnection);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onResume() {
|
||||||
|
super.onResume();
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onResume() {
|
|
||||||
super.onResume();
|
|
||||||
|
|
||||||
inForeground = true;
|
|
||||||
startComputerUpdates();
|
startComputerUpdates();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onPause() {
|
protected void onPause() {
|
||||||
super.onPause();
|
super.onPause();
|
||||||
|
|
||||||
inForeground = false;
|
|
||||||
stopComputerUpdates();
|
stopComputerUpdates();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
private int getRunningAppId() {
|
||||||
public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) {
|
int runningAppId = -1;
|
||||||
super.onCreateContextMenu(menu, v, menuInfo);
|
for (int i = 0; i < appGridAdapter.getCount(); i++) {
|
||||||
|
AppObject app = (AppObject) appGridAdapter.getItem(i);
|
||||||
|
if (app.app == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (app.app.getIsRunning()) {
|
||||||
|
runningAppId = app.app.getAppId();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return runningAppId;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) {
|
||||||
|
super.onCreateContextMenu(menu, v, menuInfo);
|
||||||
|
|
||||||
AdapterContextMenuInfo info = (AdapterContextMenuInfo) menuInfo;
|
AdapterContextMenuInfo info = (AdapterContextMenuInfo) menuInfo;
|
||||||
AppObject selectedApp = (AppObject) appGridAdapter.getItem(info.position);
|
AppObject selectedApp = (AppObject) appGridAdapter.getItem(info.position);
|
||||||
if (lastRunningAppId != 0) {
|
if (selectedApp == null || selectedApp.app == null) {
|
||||||
if (lastRunningAppId == selectedApp.app.getAppId()) {
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int runningAppId = getRunningAppId();
|
||||||
|
if (runningAppId != -1) {
|
||||||
|
if (runningAppId == selectedApp.app.getAppId()) {
|
||||||
menu.add(Menu.NONE, START_OR_RESUME_ID, 1, getResources().getString(R.string.applist_menu_resume));
|
menu.add(Menu.NONE, START_OR_RESUME_ID, 1, getResources().getString(R.string.applist_menu_resume));
|
||||||
menu.add(Menu.NONE, QUIT_ID, 2, getResources().getString(R.string.applist_menu_quit));
|
menu.add(Menu.NONE, QUIT_ID, 2, getResources().getString(R.string.applist_menu_quit));
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
menu.add(Menu.NONE, START_WTIH_QUIT, 1, getResources().getString(R.string.applist_menu_quit_and_start));
|
menu.add(Menu.NONE, START_WTIH_QUIT, 1, getResources().getString(R.string.applist_menu_quit_and_start));
|
||||||
menu.add(Menu.NONE, CANCEL_ID, 2, getResources().getString(R.string.applist_menu_cancel));
|
menu.add(Menu.NONE, CANCEL_ID, 2, getResources().getString(R.string.applist_menu_cancel));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onContextMenuClosed(Menu menu) {
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
private void displayQuitConfirmationDialog(final Runnable onYes, final Runnable onNo) {
|
||||||
public void onContextMenuClosed(Menu menu) {
|
DialogInterface.OnClickListener dialogClickListener = new DialogInterface.OnClickListener() {
|
||||||
|
@Override
|
||||||
|
public void onClick(DialogInterface dialog, int which) {
|
||||||
|
switch (which){
|
||||||
|
case DialogInterface.BUTTON_POSITIVE:
|
||||||
|
if (onYes != null) {
|
||||||
|
onYes.run();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case DialogInterface.BUTTON_NEGATIVE:
|
||||||
|
if (onNo != null) {
|
||||||
|
onNo.run();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
AlertDialog.Builder builder = new AlertDialog.Builder(this);
|
||||||
|
builder.setMessage(getResources().getString(R.string.applist_quit_confirmation))
|
||||||
|
.setPositiveButton(getResources().getString(R.string.yes), dialogClickListener)
|
||||||
|
.setNegativeButton(getResources().getString(R.string.no), dialogClickListener)
|
||||||
|
.show();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -341,37 +345,25 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
switch (item.getItemId()) {
|
switch (item.getItemId()) {
|
||||||
case START_WTIH_QUIT:
|
case START_WTIH_QUIT:
|
||||||
// Display a confirmation dialog first
|
// Display a confirmation dialog first
|
||||||
UiHelper.displayQuitConfirmationDialog(this, new Runnable() {
|
displayQuitConfirmationDialog(new Runnable() {
|
||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
ServerHelper.doStart(AppView.this, app.app, computer, managerBinder);
|
doStart(app.app);
|
||||||
}
|
}
|
||||||
}, null);
|
}, null);
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
case START_OR_RESUME_ID:
|
case START_OR_RESUME_ID:
|
||||||
// Resume is the same as start for us
|
// Resume is the same as start for us
|
||||||
ServerHelper.doStart(AppView.this, app.app, computer, managerBinder);
|
doStart(app.app);
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
case QUIT_ID:
|
case QUIT_ID:
|
||||||
// Display a confirmation dialog first
|
// Display a confirmation dialog first
|
||||||
UiHelper.displayQuitConfirmationDialog(this, new Runnable() {
|
displayQuitConfirmationDialog(new Runnable() {
|
||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
suspendGridUpdates = true;
|
doQuit(app.app);
|
||||||
ServerHelper.doQuit(AppView.this,
|
|
||||||
ServerHelper.getCurrentAddressFromComputer(computer),
|
|
||||||
app.app, managerBinder, new Runnable() {
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
// Trigger a poll immediately
|
|
||||||
suspendGridUpdates = false;
|
|
||||||
if (poller != null) {
|
|
||||||
poller.pollNow();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}, null);
|
}, null);
|
||||||
return true;
|
return true;
|
||||||
@@ -384,59 +376,28 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updateUiWithServerinfo(final ComputerDetails details) {
|
|
||||||
AppView.this.runOnUiThread(new Runnable() {
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
boolean updated = false;
|
|
||||||
|
|
||||||
// Look through our current app list to tag the running app
|
|
||||||
for (int i = 0; i < appGridAdapter.getCount(); i++) {
|
|
||||||
AppObject existingApp = (AppObject) appGridAdapter.getItem(i);
|
|
||||||
|
|
||||||
// There can only be one or zero apps running.
|
|
||||||
if (existingApp.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.isRunning = true;
|
|
||||||
updated = true;
|
|
||||||
}
|
|
||||||
else if (existingApp.isRunning) {
|
|
||||||
// This app was running but now isn't
|
|
||||||
existingApp.isRunning = false;
|
|
||||||
updated = true;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
// This app wasn't running and still isn't
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (updated) {
|
|
||||||
appGridAdapter.notifyDataSetChanged();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private void updateUiWithAppList(final List<NvApp> appList) {
|
private void updateUiWithAppList(final List<NvApp> appList) {
|
||||||
AppView.this.runOnUiThread(new Runnable() {
|
AppView.this.runOnUiThread(new Runnable() {
|
||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
boolean updated = false;
|
boolean updated = false;
|
||||||
|
|
||||||
// First handle app updates and additions
|
|
||||||
for (NvApp app : appList) {
|
for (NvApp app : appList) {
|
||||||
boolean foundExistingApp = false;
|
boolean foundExistingApp = false;
|
||||||
|
|
||||||
// Try to update an existing app in the list first
|
// Try to update an existing app in the list first
|
||||||
for (int i = 0; i < appGridAdapter.getCount(); i++) {
|
for (int i = 0; i < appGridAdapter.getCount(); i++) {
|
||||||
AppObject existingApp = (AppObject) appGridAdapter.getItem(i);
|
AppObject existingApp = (AppObject) appGridAdapter.getItem(i);
|
||||||
|
if (existingApp.app == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
if (existingApp.app.getAppId() == app.getAppId()) {
|
if (existingApp.app.getAppId() == app.getAppId()) {
|
||||||
// Found the app; update its properties
|
// Found the app; update its properties
|
||||||
|
if (existingApp.app.getIsRunning() != app.getIsRunning()) {
|
||||||
|
existingApp.app.setIsRunningBoolean(app.getIsRunning());
|
||||||
|
updated = true;
|
||||||
|
}
|
||||||
if (!existingApp.app.getAppName().equals(app.getAppName())) {
|
if (!existingApp.app.getAppName().equals(app.getAppName())) {
|
||||||
existingApp.app.setAppName(app.getAppName());
|
existingApp.app.setAppName(app.getAppName());
|
||||||
updated = true;
|
updated = true;
|
||||||
@@ -454,34 +415,6 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Next handle app removals
|
|
||||||
int i = 0;
|
|
||||||
while (i < appGridAdapter.getCount()) {
|
|
||||||
boolean foundExistingApp = false;
|
|
||||||
AppObject existingApp = (AppObject) appGridAdapter.getItem(i);
|
|
||||||
|
|
||||||
// Check if this app is in the latest list
|
|
||||||
for (NvApp app : appList) {
|
|
||||||
if (existingApp.app.getAppId() == app.getAppId()) {
|
|
||||||
foundExistingApp = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// This app was removed in the latest app list
|
|
||||||
if (!foundExistingApp) {
|
|
||||||
appGridAdapter.removeApp(existingApp);
|
|
||||||
updated = true;
|
|
||||||
|
|
||||||
// Check this same index again because the item at i+1 is now at i after
|
|
||||||
// the removal
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Move on to the next item
|
|
||||||
i++;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (updated) {
|
if (updated) {
|
||||||
appGridAdapter.notifyDataSetChanged();
|
appGridAdapter.notifyDataSetChanged();
|
||||||
}
|
}
|
||||||
@@ -489,6 +422,58 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void doStart(NvApp app) {
|
||||||
|
Intent intent = new Intent(this, Game.class);
|
||||||
|
intent.putExtra(Game.EXTRA_HOST,
|
||||||
|
computer.reachability == ComputerDetails.Reachability.LOCAL ?
|
||||||
|
computer.localIp.getHostAddress() : computer.remoteIp.getHostAddress());
|
||||||
|
intent.putExtra(Game.EXTRA_APP, app.getAppName());
|
||||||
|
intent.putExtra(Game.EXTRA_UNIQUEID, managerBinder.getUniqueId());
|
||||||
|
intent.putExtra(Game.EXTRA_STREAMING_REMOTE,
|
||||||
|
computer.reachability != ComputerDetails.Reachability.LOCAL);
|
||||||
|
startActivity(intent);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void doQuit(final NvApp app) {
|
||||||
|
Toast.makeText(AppView.this, getResources().getString(R.string.applist_quit_app)+" "+app.getAppName()+"...", Toast.LENGTH_SHORT).show();
|
||||||
|
new Thread(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
NvHTTP httpConn;
|
||||||
|
String message;
|
||||||
|
try {
|
||||||
|
httpConn = new NvHTTP(getAddress(),
|
||||||
|
managerBinder.getUniqueId(), null, PlatformBinding.getCryptoProvider(AppView.this));
|
||||||
|
if (httpConn.quitApp()) {
|
||||||
|
message = getResources().getString(R.string.applist_quit_success)+" "+app.getAppName();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
message = getResources().getString(R.string.applist_quit_fail)+" "+app.getAppName();
|
||||||
|
}
|
||||||
|
} catch (UnknownHostException e) {
|
||||||
|
message = getResources().getString(R.string.error_unknown_host);
|
||||||
|
} catch (FileNotFoundException e) {
|
||||||
|
message = getResources().getString(R.string.error_404);
|
||||||
|
} catch (Exception e) {
|
||||||
|
message = e.getMessage();
|
||||||
|
} finally {
|
||||||
|
// Trigger a poll immediately
|
||||||
|
if (poller != null) {
|
||||||
|
poller.pollNow();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final String toastMessage = message;
|
||||||
|
runOnUiThread(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
Toast.makeText(AppView.this, toastMessage, Toast.LENGTH_LONG).show();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}).start();
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int getAdapterFragmentLayoutId() {
|
public int getAdapterFragmentLayoutId() {
|
||||||
return PreferenceConfiguration.readPreferences(this).listMode ?
|
return PreferenceConfiguration.readPreferences(this).listMode ?
|
||||||
@@ -504,33 +489,31 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
public void onItemClick(AdapterView<?> arg0, View arg1, int pos,
|
public void onItemClick(AdapterView<?> arg0, View arg1, int pos,
|
||||||
long id) {
|
long id) {
|
||||||
AppObject app = (AppObject) appGridAdapter.getItem(pos);
|
AppObject app = (AppObject) appGridAdapter.getItem(pos);
|
||||||
|
if (app == null || app.app == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Only open the context menu if something is running, otherwise start it
|
// Only open the context menu if something is running, otherwise start it
|
||||||
if (lastRunningAppId != 0) {
|
if (getRunningAppId() != -1) {
|
||||||
openContextMenu(arg1);
|
openContextMenu(arg1);
|
||||||
} else {
|
} else {
|
||||||
ServerHelper.doStart(AppView.this, app.app, computer, managerBinder);
|
doStart(app.app);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
registerForContextMenu(listView);
|
registerForContextMenu(listView);
|
||||||
listView.requestFocus();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public class AppObject {
|
public class AppObject {
|
||||||
public final NvApp app;
|
public NvApp app;
|
||||||
public boolean isRunning;
|
|
||||||
|
public AppObject(NvApp app) {
|
||||||
public AppObject(NvApp app) {
|
this.app = app;
|
||||||
if (app == null) {
|
}
|
||||||
throw new IllegalArgumentException("app must not be null");
|
|
||||||
}
|
@Override
|
||||||
this.app = app;
|
public String toString() {
|
||||||
}
|
return app.getAppName();
|
||||||
|
}
|
||||||
@Override
|
}
|
||||||
public String toString() {
|
|
||||||
return app.getAppName();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -1,164 +0,0 @@
|
|||||||
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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
File diff suppressed because it is too large.
Load diff
@@ -1,73 +0,0 @@
|
|||||||
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) {
|
|
||||||
return !(url.toUpperCase().startsWith("https://github.com/moonlight-stream/moonlight-docs/wiki/".toUpperCase()) ||
|
|
||||||
url.toUpperCase().startsWith("http://github.com/moonlight-stream/moonlight-docs/wiki/".toUpperCase()));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,6 @@
|
|||||||
|
package com.limelight;
|
||||||
|
|
||||||
|
/* This is a dummy class to allow for a separate icon
|
||||||
|
* and launcher for TV.
|
||||||
|
*/
|
||||||
|
public class PcViewTv extends PcView {}
|
||||||
@@ -8,17 +8,17 @@ import com.limelight.nvstream.av.audio.AudioRenderer;
|
|||||||
import com.limelight.nvstream.http.LimelightCryptoProvider;
|
import com.limelight.nvstream.http.LimelightCryptoProvider;
|
||||||
|
|
||||||
public class PlatformBinding {
|
public class PlatformBinding {
|
||||||
public static String getDeviceName() {
|
public static String getDeviceName() {
|
||||||
String deviceName = android.os.Build.MODEL;
|
String deviceName = android.os.Build.MODEL;
|
||||||
deviceName = deviceName.replace(" ", "");
|
deviceName = deviceName.replace(" ", "");
|
||||||
return deviceName;
|
return deviceName;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static AudioRenderer getAudioRenderer() {
|
public static AudioRenderer getAudioRenderer() {
|
||||||
return new AndroidAudioRenderer();
|
return new AndroidAudioRenderer();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static LimelightCryptoProvider getCryptoProvider(Context c) {
|
public static LimelightCryptoProvider getCryptoProvider(Context c) {
|
||||||
return new AndroidCryptoProvider(c);
|
return new AndroidCryptoProvider(c);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,189 +1,97 @@
|
|||||||
package com.limelight.binding.audio;
|
package com.limelight.binding.audio;
|
||||||
|
|
||||||
import android.media.AudioAttributes;
|
|
||||||
import android.media.AudioFormat;
|
import android.media.AudioFormat;
|
||||||
import android.media.AudioManager;
|
import android.media.AudioManager;
|
||||||
import android.media.AudioTrack;
|
import android.media.AudioTrack;
|
||||||
import android.os.Build;
|
|
||||||
|
|
||||||
import com.limelight.LimeLog;
|
import com.limelight.LimeLog;
|
||||||
import com.limelight.nvstream.av.audio.AudioRenderer;
|
import com.limelight.nvstream.av.audio.AudioRenderer;
|
||||||
import com.limelight.nvstream.jni.MoonBridge;
|
|
||||||
|
|
||||||
public class AndroidAudioRenderer implements AudioRenderer {
|
public class AndroidAudioRenderer implements AudioRenderer {
|
||||||
|
|
||||||
private AudioTrack track;
|
public static final int FRAME_SIZE = 960;
|
||||||
|
|
||||||
|
private AudioTrack track;
|
||||||
|
|
||||||
private AudioTrack createAudioTrack(int channelConfig, int bufferSize, boolean lowLatency) {
|
@Override
|
||||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
|
public boolean streamInitialized(int channelCount, int sampleRate) {
|
||||||
return new AudioTrack(AudioManager.STREAM_MUSIC,
|
int channelConfig;
|
||||||
48000,
|
int bufferSize;
|
||||||
channelConfig,
|
|
||||||
AudioFormat.ENCODING_PCM_16BIT,
|
|
||||||
bufferSize,
|
|
||||||
AudioTrack.MODE_STREAM);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
AudioAttributes.Builder attributesBuilder = new AudioAttributes.Builder()
|
|
||||||
.setUsage(AudioAttributes.USAGE_GAME);
|
|
||||||
AudioFormat format = new AudioFormat.Builder()
|
|
||||||
.setEncoding(AudioFormat.ENCODING_PCM_16BIT)
|
|
||||||
.setSampleRate(48000)
|
|
||||||
.setChannelMask(channelConfig)
|
|
||||||
.build();
|
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
|
switch (channelCount)
|
||||||
// Use FLAG_LOW_LATENCY on L through N
|
{
|
||||||
if (lowLatency) {
|
case 1:
|
||||||
attributesBuilder.setFlags(AudioAttributes.FLAG_LOW_LATENCY);
|
channelConfig = AudioFormat.CHANNEL_OUT_MONO;
|
||||||
}
|
break;
|
||||||
}
|
case 2:
|
||||||
|
channelConfig = AudioFormat.CHANNEL_OUT_STEREO;
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
break;
|
||||||
AudioTrack.Builder trackBuilder = new AudioTrack.Builder()
|
default:
|
||||||
.setAudioFormat(format)
|
LimeLog.severe("Decoder returned unhandled channel count");
|
||||||
.setAudioAttributes(attributesBuilder.build())
|
return false;
|
||||||
.setTransferMode(AudioTrack.MODE_STREAM)
|
}
|
||||||
.setBufferSizeInBytes(bufferSize);
|
|
||||||
|
|
||||||
// Use PERFORMANCE_MODE_LOW_LATENCY on O and later
|
|
||||||
if (lowLatency) {
|
|
||||||
trackBuilder.setPerformanceMode(AudioTrack.PERFORMANCE_MODE_LOW_LATENCY);
|
|
||||||
}
|
|
||||||
|
|
||||||
return trackBuilder.build();
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
return new AudioTrack(attributesBuilder.build(),
|
|
||||||
format,
|
|
||||||
bufferSize,
|
|
||||||
AudioTrack.MODE_STREAM,
|
|
||||||
AudioManager.AUDIO_SESSION_ID_GENERATE);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int setup(int audioConfiguration) {
|
|
||||||
int channelConfig;
|
|
||||||
int bytesPerFrame;
|
|
||||||
|
|
||||||
switch (audioConfiguration)
|
|
||||||
{
|
|
||||||
case MoonBridge.AUDIO_CONFIGURATION_STEREO:
|
|
||||||
channelConfig = AudioFormat.CHANNEL_OUT_STEREO;
|
|
||||||
bytesPerFrame = 2 * 240 * 2;
|
|
||||||
break;
|
|
||||||
case MoonBridge.AUDIO_CONFIGURATION_51_SURROUND:
|
|
||||||
channelConfig = AudioFormat.CHANNEL_OUT_5POINT1;
|
|
||||||
bytesPerFrame = 6 * 240 * 2;
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
LimeLog.severe("Decoder returned unhandled channel count");
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// We're not supposed to request less than the minimum
|
// We're not supposed to request less than the minimum
|
||||||
// buffer size for our buffer, but it appears that we can
|
// buffer size for our buffer, but it appears that we can
|
||||||
// do this on many devices and it lowers audio latency.
|
// do this on many devices and it lowers audio latency.
|
||||||
// We'll try the small buffer size first and if it fails,
|
// We'll try the small buffer size first and if it fails,
|
||||||
// use the recommended larger buffer size.
|
// use the recommended larger buffer size.
|
||||||
|
try {
|
||||||
|
// Buffer two frames of audio if possible
|
||||||
|
bufferSize = FRAME_SIZE * 2;
|
||||||
|
|
||||||
for (int i = 0; i < 4; i++) {
|
track = new AudioTrack(AudioManager.STREAM_MUSIC,
|
||||||
boolean lowLatency;
|
sampleRate,
|
||||||
int bufferSize;
|
channelConfig,
|
||||||
|
AudioFormat.ENCODING_PCM_16BIT,
|
||||||
|
bufferSize,
|
||||||
|
AudioTrack.MODE_STREAM);
|
||||||
|
track.play();
|
||||||
|
} catch (Exception e) {
|
||||||
|
// Try to release the AudioTrack if we got far enough
|
||||||
|
try {
|
||||||
|
if (track != null) {
|
||||||
|
track.release();
|
||||||
|
}
|
||||||
|
} catch (Exception ignored) {}
|
||||||
|
|
||||||
// We will try:
|
// Now try the larger buffer size
|
||||||
// 1) Small buffer, low latency mode
|
bufferSize = Math.max(AudioTrack.getMinBufferSize(sampleRate,
|
||||||
// 2) Large buffer, low latency mode
|
|
||||||
// 3) Small buffer, standard mode
|
|
||||||
// 4) Large buffer, standard mode
|
|
||||||
|
|
||||||
switch (i) {
|
|
||||||
case 0:
|
|
||||||
case 1:
|
|
||||||
lowLatency = true;
|
|
||||||
break;
|
|
||||||
case 2:
|
|
||||||
case 3:
|
|
||||||
lowLatency = false;
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
// Unreachable
|
|
||||||
throw new IllegalStateException();
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (i) {
|
|
||||||
case 0:
|
|
||||||
case 2:
|
|
||||||
bufferSize = bytesPerFrame * 2;
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 1:
|
|
||||||
case 3:
|
|
||||||
// Try the larger buffer size
|
|
||||||
bufferSize = Math.max(AudioTrack.getMinBufferSize(48000,
|
|
||||||
channelConfig,
|
channelConfig,
|
||||||
AudioFormat.ENCODING_PCM_16BIT),
|
AudioFormat.ENCODING_PCM_16BIT),
|
||||||
bytesPerFrame * 2);
|
FRAME_SIZE * 2);
|
||||||
|
|
||||||
// Round to next frame
|
// Round to next frame
|
||||||
bufferSize = (((bufferSize + (bytesPerFrame - 1)) / bytesPerFrame) * bytesPerFrame);
|
bufferSize = (((bufferSize + (FRAME_SIZE - 1)) / FRAME_SIZE) * FRAME_SIZE);
|
||||||
break;
|
|
||||||
default:
|
|
||||||
// Unreachable
|
|
||||||
throw new IllegalStateException();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Skip low latency options if hardware sample rate isn't 48000Hz
|
track = new AudioTrack(AudioManager.STREAM_MUSIC,
|
||||||
if (AudioTrack.getNativeOutputSampleRate(AudioManager.STREAM_MUSIC) != 48000 && lowLatency) {
|
sampleRate,
|
||||||
continue;
|
channelConfig,
|
||||||
}
|
AudioFormat.ENCODING_PCM_16BIT,
|
||||||
|
bufferSize,
|
||||||
try {
|
AudioTrack.MODE_STREAM);
|
||||||
track = createAudioTrack(channelConfig, bufferSize, lowLatency);
|
track.play();
|
||||||
track.play();
|
|
||||||
|
|
||||||
// Successfully created working AudioTrack. We're done here.
|
|
||||||
LimeLog.info("Audio track configuration: "+bufferSize+" "+lowLatency);
|
|
||||||
break;
|
|
||||||
} catch (Exception e) {
|
|
||||||
// Try to release the AudioTrack if we got far enough
|
|
||||||
e.printStackTrace();
|
|
||||||
try {
|
|
||||||
if (track != null) {
|
|
||||||
track.release();
|
|
||||||
track = null;
|
|
||||||
}
|
|
||||||
} catch (Exception ignored) {}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
LimeLog.info("Audio track buffer size: "+bufferSize);
|
||||||
|
|
||||||
if (track == null) {
|
return true;
|
||||||
// Couldn't create any audio track for playback
|
}
|
||||||
return -2;
|
|
||||||
}
|
|
||||||
|
|
||||||
return 0;
|
@Override
|
||||||
}
|
public void playDecodedAudio(byte[] audioData, int offset, int length) {
|
||||||
|
track.write(audioData, offset, length);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void playDecodedAudio(byte[] audioData) {
|
public void streamClosing() {
|
||||||
track.write(audioData, 0, audioData.length);
|
if (track != null) {
|
||||||
}
|
track.release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void start() {}
|
public int getCapabilities() {
|
||||||
|
return 0;
|
||||||
@Override
|
}
|
||||||
public void stop() {
|
|
||||||
// Immediately drop all pending data
|
|
||||||
track.pause();
|
|
||||||
track.flush();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void cleanup() {
|
|
||||||
track.release();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -45,239 +45,239 @@ import com.limelight.nvstream.http.LimelightCryptoProvider;
|
|||||||
|
|
||||||
public class AndroidCryptoProvider implements LimelightCryptoProvider {
|
public class AndroidCryptoProvider implements LimelightCryptoProvider {
|
||||||
|
|
||||||
private final File certFile;
|
private File certFile;
|
||||||
private final File keyFile;
|
private File keyFile;
|
||||||
|
|
||||||
private X509Certificate cert;
|
private X509Certificate cert;
|
||||||
private RSAPrivateKey key;
|
private RSAPrivateKey key;
|
||||||
private byte[] pemCertBytes;
|
private byte[] pemCertBytes;
|
||||||
|
|
||||||
private static final Object globalCryptoLock = new Object();
|
private static final Object globalCryptoLock = new Object();
|
||||||
|
|
||||||
static {
|
static {
|
||||||
// Install the Bouncy Castle provider
|
// Install the Bouncy Castle provider
|
||||||
Security.addProvider(new BouncyCastleProvider());
|
Security.addProvider(new BouncyCastleProvider());
|
||||||
}
|
}
|
||||||
|
|
||||||
public AndroidCryptoProvider(Context c) {
|
public AndroidCryptoProvider(Context c) {
|
||||||
String dataPath = c.getFilesDir().getAbsolutePath();
|
String dataPath = c.getFilesDir().getAbsolutePath();
|
||||||
|
|
||||||
certFile = new File(dataPath + File.separator + "client.crt");
|
certFile = new File(dataPath + File.separator + "client.crt");
|
||||||
keyFile = new File(dataPath + File.separator + "client.key");
|
keyFile = new File(dataPath + File.separator + "client.key");
|
||||||
}
|
}
|
||||||
|
|
||||||
private byte[] loadFileToBytes(File f) {
|
private byte[] loadFileToBytes(File f) {
|
||||||
if (!f.exists()) {
|
if (!f.exists()) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
FileInputStream fin = new FileInputStream(f);
|
FileInputStream fin = new FileInputStream(f);
|
||||||
byte[] fileData = new byte[(int) f.length()];
|
byte[] fileData = new byte[(int) f.length()];
|
||||||
if (fin.read(fileData) != f.length()) {
|
if (fin.read(fileData) != f.length()) {
|
||||||
// Failed to read
|
// Failed to read
|
||||||
fileData = null;
|
fileData = null;
|
||||||
}
|
}
|
||||||
fin.close();
|
fin.close();
|
||||||
return fileData;
|
return fileData;
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private boolean loadCertKeyPair() {
|
||||||
|
byte[] certBytes = loadFileToBytes(certFile);
|
||||||
|
byte[] keyBytes = loadFileToBytes(keyFile);
|
||||||
|
|
||||||
|
// If either file was missing, we definitely can't succeed
|
||||||
|
if (certBytes == null || keyBytes == null) {
|
||||||
|
LimeLog.info("Missing cert or key; need to generate a new one");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
CertificateFactory certFactory = CertificateFactory.getInstance("X.509", "BC");
|
||||||
|
cert = (X509Certificate) certFactory.generateCertificate(new ByteArrayInputStream(certBytes));
|
||||||
|
pemCertBytes = certBytes;
|
||||||
|
KeyFactory keyFactory = KeyFactory.getInstance("RSA", "BC");
|
||||||
|
key = (RSAPrivateKey) keyFactory.generatePrivate(new PKCS8EncodedKeySpec(keyBytes));
|
||||||
|
} catch (CertificateException e) {
|
||||||
|
// May happen if the cert is corrupt
|
||||||
|
LimeLog.warning("Corrupted certificate");
|
||||||
|
return false;
|
||||||
|
} catch (NoSuchAlgorithmException e) {
|
||||||
|
// Should never happen
|
||||||
|
e.printStackTrace();
|
||||||
|
return false;
|
||||||
|
} catch (InvalidKeySpecException e) {
|
||||||
|
// May happen if the key is corrupt
|
||||||
|
LimeLog.warning("Corrupted key");
|
||||||
|
return false;
|
||||||
|
} catch (NoSuchProviderException e) {
|
||||||
|
// Should never happen
|
||||||
|
e.printStackTrace();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressLint("TrulyRandom")
|
||||||
|
private boolean generateCertKeyPair() {
|
||||||
|
byte[] snBytes = new byte[8];
|
||||||
|
new SecureRandom().nextBytes(snBytes);
|
||||||
|
|
||||||
|
KeyPair keyPair;
|
||||||
|
try {
|
||||||
|
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA", "BC");
|
||||||
|
keyPairGenerator.initialize(2048);
|
||||||
|
keyPair = keyPairGenerator.generateKeyPair();
|
||||||
|
} catch (NoSuchAlgorithmException e1) {
|
||||||
|
// Should never happen
|
||||||
|
e1.printStackTrace();
|
||||||
|
return false;
|
||||||
|
} catch (NoSuchProviderException e) {
|
||||||
|
// Should never happen
|
||||||
|
e.printStackTrace();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Date now = new Date();
|
||||||
|
|
||||||
|
// Expires in 20 years
|
||||||
|
Calendar calendar = Calendar.getInstance();
|
||||||
|
calendar.setTime(now);
|
||||||
|
calendar.add(Calendar.YEAR, 20);
|
||||||
|
Date expirationDate = calendar.getTime();
|
||||||
|
|
||||||
|
BigInteger serial = new BigInteger(snBytes).abs();
|
||||||
|
|
||||||
|
X500NameBuilder nameBuilder = new X500NameBuilder(BCStyle.INSTANCE);
|
||||||
|
nameBuilder.addRDN(BCStyle.CN, "NVIDIA GameStream Client");
|
||||||
|
X500Name name = nameBuilder.build();
|
||||||
|
|
||||||
|
X509v3CertificateBuilder certBuilder = new X509v3CertificateBuilder(name, serial, now, expirationDate, Locale.ENGLISH, name,
|
||||||
|
SubjectPublicKeyInfo.getInstance(keyPair.getPublic().getEncoded()));
|
||||||
|
|
||||||
|
try {
|
||||||
|
ContentSigner sigGen = new JcaContentSignerBuilder("SHA1withRSA").setProvider(BouncyCastleProvider.PROVIDER_NAME).build(keyPair.getPrivate());
|
||||||
|
cert = new JcaX509CertificateConverter().setProvider(BouncyCastleProvider.PROVIDER_NAME).getCertificate(certBuilder.build(sigGen));
|
||||||
|
key = (RSAPrivateKey) keyPair.getPrivate();
|
||||||
|
} catch (Exception e) {
|
||||||
|
// Nothing should go wrong here
|
||||||
|
e.printStackTrace();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
LimeLog.info("Generated a new key pair");
|
||||||
|
|
||||||
|
// Save the resulting pair
|
||||||
|
saveCertKeyPair();
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void saveCertKeyPair() {
|
||||||
|
try {
|
||||||
|
FileOutputStream certOut = new FileOutputStream(certFile);
|
||||||
|
FileOutputStream keyOut = new FileOutputStream(keyFile);
|
||||||
|
|
||||||
|
// Write the certificate in OpenSSL PEM format (important for the server)
|
||||||
|
StringWriter strWriter = new StringWriter();
|
||||||
|
JcaPEMWriter pemWriter = new JcaPEMWriter(strWriter);
|
||||||
|
pemWriter.writeObject(cert);
|
||||||
|
pemWriter.close();
|
||||||
|
|
||||||
|
// Line endings MUST be UNIX for the PC to accept the cert properly
|
||||||
|
OutputStreamWriter certWriter = new OutputStreamWriter(certOut);
|
||||||
|
String pemStr = strWriter.getBuffer().toString();
|
||||||
|
for (int i = 0; i < pemStr.length(); i++) {
|
||||||
|
char c = pemStr.charAt(i);
|
||||||
|
if (c != '\r')
|
||||||
|
certWriter.append(c);
|
||||||
|
}
|
||||||
|
certWriter.close();
|
||||||
|
|
||||||
|
// Write the private out in PKCS8 format
|
||||||
|
keyOut.write(key.getEncoded());
|
||||||
|
|
||||||
|
certOut.close();
|
||||||
|
keyOut.close();
|
||||||
|
|
||||||
|
LimeLog.info("Saved generated key pair to disk");
|
||||||
|
} catch (IOException e) {
|
||||||
|
// This isn't good because it means we'll have
|
||||||
|
// to re-pair next time
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public X509Certificate getClientCertificate() {
|
||||||
|
// Use a lock here to ensure only one guy will be generating or loading
|
||||||
|
// the certificate and key at a time
|
||||||
|
synchronized (globalCryptoLock) {
|
||||||
|
// Return a loaded cert if we have one
|
||||||
|
if (cert != null) {
|
||||||
|
return cert;
|
||||||
|
}
|
||||||
|
|
||||||
|
// No loaded cert yet, let's see if we have one on disk
|
||||||
|
if (loadCertKeyPair()) {
|
||||||
|
// Got one
|
||||||
|
return cert;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to generate a new key pair
|
||||||
|
if (!generateCertKeyPair()) {
|
||||||
|
// Failed
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load the generated pair
|
||||||
|
loadCertKeyPair();
|
||||||
|
return cert;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private boolean loadCertKeyPair() {
|
public RSAPrivateKey getClientPrivateKey() {
|
||||||
byte[] certBytes = loadFileToBytes(certFile);
|
// Use a lock here to ensure only one guy will be generating or loading
|
||||||
byte[] keyBytes = loadFileToBytes(keyFile);
|
// the certificate and key at a time
|
||||||
|
synchronized (globalCryptoLock) {
|
||||||
|
// Return a loaded key if we have one
|
||||||
|
if (key != null) {
|
||||||
|
return key;
|
||||||
|
}
|
||||||
|
|
||||||
|
// No loaded key yet, let's see if we have one on disk
|
||||||
|
if (loadCertKeyPair()) {
|
||||||
|
// Got one
|
||||||
|
return key;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to generate a new key pair
|
||||||
|
if (!generateCertKeyPair()) {
|
||||||
|
// Failed
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load the generated pair
|
||||||
|
loadCertKeyPair();
|
||||||
|
return key;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] getPemEncodedClientCertificate() {
|
||||||
|
synchronized (globalCryptoLock) {
|
||||||
|
// Call our helper function to do the cert loading/generation for us
|
||||||
|
getClientCertificate();
|
||||||
|
|
||||||
|
// Return a cached value if we have it
|
||||||
|
return pemCertBytes;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// If either file was missing, we definitely can't succeed
|
@Override
|
||||||
if (certBytes == null || keyBytes == null) {
|
public String encodeBase64String(byte[] data) {
|
||||||
LimeLog.info("Missing cert or key; need to generate a new one");
|
return Base64.encodeToString(data, Base64.NO_WRAP);
|
||||||
return false;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
CertificateFactory certFactory = CertificateFactory.getInstance("X.509", "BC");
|
|
||||||
cert = (X509Certificate) certFactory.generateCertificate(new ByteArrayInputStream(certBytes));
|
|
||||||
pemCertBytes = certBytes;
|
|
||||||
KeyFactory keyFactory = KeyFactory.getInstance("RSA", "BC");
|
|
||||||
key = (RSAPrivateKey) keyFactory.generatePrivate(new PKCS8EncodedKeySpec(keyBytes));
|
|
||||||
} catch (CertificateException e) {
|
|
||||||
// May happen if the cert is corrupt
|
|
||||||
LimeLog.warning("Corrupted certificate");
|
|
||||||
return false;
|
|
||||||
} catch (NoSuchAlgorithmException e) {
|
|
||||||
// Should never happen
|
|
||||||
e.printStackTrace();
|
|
||||||
return false;
|
|
||||||
} catch (InvalidKeySpecException e) {
|
|
||||||
// May happen if the key is corrupt
|
|
||||||
LimeLog.warning("Corrupted key");
|
|
||||||
return false;
|
|
||||||
} catch (NoSuchProviderException e) {
|
|
||||||
// Should never happen
|
|
||||||
e.printStackTrace();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressLint("TrulyRandom")
|
|
||||||
private boolean generateCertKeyPair() {
|
|
||||||
byte[] snBytes = new byte[8];
|
|
||||||
new SecureRandom().nextBytes(snBytes);
|
|
||||||
|
|
||||||
KeyPair keyPair;
|
|
||||||
try {
|
|
||||||
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA", "BC");
|
|
||||||
keyPairGenerator.initialize(2048);
|
|
||||||
keyPair = keyPairGenerator.generateKeyPair();
|
|
||||||
} catch (NoSuchAlgorithmException e1) {
|
|
||||||
// Should never happen
|
|
||||||
e1.printStackTrace();
|
|
||||||
return false;
|
|
||||||
} catch (NoSuchProviderException e) {
|
|
||||||
// Should never happen
|
|
||||||
e.printStackTrace();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
Date now = new Date();
|
|
||||||
|
|
||||||
// Expires in 20 years
|
|
||||||
Calendar calendar = Calendar.getInstance();
|
|
||||||
calendar.setTime(now);
|
|
||||||
calendar.add(Calendar.YEAR, 20);
|
|
||||||
Date expirationDate = calendar.getTime();
|
|
||||||
|
|
||||||
BigInteger serial = new BigInteger(snBytes).abs();
|
|
||||||
|
|
||||||
X500NameBuilder nameBuilder = new X500NameBuilder(BCStyle.INSTANCE);
|
|
||||||
nameBuilder.addRDN(BCStyle.CN, "NVIDIA GameStream Client");
|
|
||||||
X500Name name = nameBuilder.build();
|
|
||||||
|
|
||||||
X509v3CertificateBuilder certBuilder = new X509v3CertificateBuilder(name, serial, now, expirationDate, Locale.ENGLISH, name,
|
|
||||||
SubjectPublicKeyInfo.getInstance(keyPair.getPublic().getEncoded()));
|
|
||||||
|
|
||||||
try {
|
|
||||||
ContentSigner sigGen = new JcaContentSignerBuilder("SHA1withRSA").setProvider(BouncyCastleProvider.PROVIDER_NAME).build(keyPair.getPrivate());
|
|
||||||
cert = new JcaX509CertificateConverter().setProvider(BouncyCastleProvider.PROVIDER_NAME).getCertificate(certBuilder.build(sigGen));
|
|
||||||
key = (RSAPrivateKey) keyPair.getPrivate();
|
|
||||||
} catch (Exception e) {
|
|
||||||
// Nothing should go wrong here
|
|
||||||
e.printStackTrace();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
LimeLog.info("Generated a new key pair");
|
|
||||||
|
|
||||||
// Save the resulting pair
|
|
||||||
saveCertKeyPair();
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void saveCertKeyPair() {
|
|
||||||
try {
|
|
||||||
FileOutputStream certOut = new FileOutputStream(certFile);
|
|
||||||
FileOutputStream keyOut = new FileOutputStream(keyFile);
|
|
||||||
|
|
||||||
// Write the certificate in OpenSSL PEM format (important for the server)
|
|
||||||
StringWriter strWriter = new StringWriter();
|
|
||||||
JcaPEMWriter pemWriter = new JcaPEMWriter(strWriter);
|
|
||||||
pemWriter.writeObject(cert);
|
|
||||||
pemWriter.close();
|
|
||||||
|
|
||||||
// Line endings MUST be UNIX for the PC to accept the cert properly
|
|
||||||
OutputStreamWriter certWriter = new OutputStreamWriter(certOut);
|
|
||||||
String pemStr = strWriter.getBuffer().toString();
|
|
||||||
for (int i = 0; i < pemStr.length(); i++) {
|
|
||||||
char c = pemStr.charAt(i);
|
|
||||||
if (c != '\r')
|
|
||||||
certWriter.append(c);
|
|
||||||
}
|
|
||||||
certWriter.close();
|
|
||||||
|
|
||||||
// Write the private out in PKCS8 format
|
|
||||||
keyOut.write(key.getEncoded());
|
|
||||||
|
|
||||||
certOut.close();
|
|
||||||
keyOut.close();
|
|
||||||
|
|
||||||
LimeLog.info("Saved generated key pair to disk");
|
|
||||||
} catch (IOException e) {
|
|
||||||
// This isn't good because it means we'll have
|
|
||||||
// to re-pair next time
|
|
||||||
e.printStackTrace();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public X509Certificate getClientCertificate() {
|
|
||||||
// Use a lock here to ensure only one guy will be generating or loading
|
|
||||||
// the certificate and key at a time
|
|
||||||
synchronized (globalCryptoLock) {
|
|
||||||
// Return a loaded cert if we have one
|
|
||||||
if (cert != null) {
|
|
||||||
return cert;
|
|
||||||
}
|
|
||||||
|
|
||||||
// No loaded cert yet, let's see if we have one on disk
|
|
||||||
if (loadCertKeyPair()) {
|
|
||||||
// Got one
|
|
||||||
return cert;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Try to generate a new key pair
|
|
||||||
if (!generateCertKeyPair()) {
|
|
||||||
// Failed
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load the generated pair
|
|
||||||
loadCertKeyPair();
|
|
||||||
return cert;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public RSAPrivateKey getClientPrivateKey() {
|
|
||||||
// Use a lock here to ensure only one guy will be generating or loading
|
|
||||||
// the certificate and key at a time
|
|
||||||
synchronized (globalCryptoLock) {
|
|
||||||
// Return a loaded key if we have one
|
|
||||||
if (key != null) {
|
|
||||||
return key;
|
|
||||||
}
|
|
||||||
|
|
||||||
// No loaded key yet, let's see if we have one on disk
|
|
||||||
if (loadCertKeyPair()) {
|
|
||||||
// Got one
|
|
||||||
return key;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Try to generate a new key pair
|
|
||||||
if (!generateCertKeyPair()) {
|
|
||||||
// Failed
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load the generated pair
|
|
||||||
loadCertKeyPair();
|
|
||||||
return key;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public byte[] getPemEncodedClientCertificate() {
|
|
||||||
synchronized (globalCryptoLock) {
|
|
||||||
// Call our helper function to do the cert loading/generation for us
|
|
||||||
getClientCertificate();
|
|
||||||
|
|
||||||
// Return a cached value if we have it
|
|
||||||
return pemCertBytes;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String encodeBase64String(byte[] data) {
|
|
||||||
return Base64.encodeToString(data, Base64.NO_WRAP);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
File diff suppressed because it is too large.
Load diff
@@ -2,17 +2,20 @@ package com.limelight.binding.input;
|
|||||||
|
|
||||||
import android.view.KeyEvent;
|
import android.view.KeyEvent;
|
||||||
|
|
||||||
|
import com.limelight.nvstream.NvConnection;
|
||||||
|
import com.limelight.nvstream.input.KeycodeTranslator;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class to translate a Android key code into the codes GFE is expecting
|
* Class to translate a Android key code into the codes GFE is expecting
|
||||||
* @author Diego Waxemberg
|
* @author Diego Waxemberg
|
||||||
* @author Cameron Gutman
|
* @author Cameron Gutman
|
||||||
*/
|
*/
|
||||||
public class KeyboardTranslator {
|
public class KeyboardTranslator extends KeycodeTranslator {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* GFE's prefix for every key code
|
* GFE's prefix for every key code
|
||||||
*/
|
*/
|
||||||
private static final short KEY_PREFIX = (short) 0x80;
|
public static final short KEY_PREFIX = (short) 0x80;
|
||||||
|
|
||||||
public static final int VK_0 = 48;
|
public static final int VK_0 = 48;
|
||||||
public static final int VK_9 = 57;
|
public static final int VK_9 = 57;
|
||||||
@@ -20,8 +23,8 @@ public class KeyboardTranslator {
|
|||||||
public static final int VK_Z = 90;
|
public static final int VK_Z = 90;
|
||||||
public static final int VK_ALT = 18;
|
public static final int VK_ALT = 18;
|
||||||
public static final int VK_NUMPAD0 = 96;
|
public static final int VK_NUMPAD0 = 96;
|
||||||
public static final int VK_BACK_SLASH = 92;
|
public static final int VK_BACK_SLASH = 92;
|
||||||
public static final int VK_CAPS_LOCK = 20;
|
public static final int VK_CAPS_LOCK = 20;
|
||||||
public static final int VK_CLEAR = 12;
|
public static final int VK_CLEAR = 12;
|
||||||
public static final int VK_COMMA = 44;
|
public static final int VK_COMMA = 44;
|
||||||
public static final int VK_CONTROL = 17;
|
public static final int VK_CONTROL = 17;
|
||||||
@@ -56,12 +59,21 @@ public class KeyboardTranslator {
|
|||||||
public static final int VK_QUOTE = 222;
|
public static final int VK_QUOTE = 222;
|
||||||
public static final int VK_PAUSE = 19;
|
public static final int VK_PAUSE = 19;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs a new translator for the specified connection
|
||||||
|
* @param conn the connection to which the translated codes are sent
|
||||||
|
*/
|
||||||
|
public KeyboardTranslator(NvConnection conn) {
|
||||||
|
super(conn);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Translates the given keycode and returns the GFE keycode
|
* Translates the given keycode and returns the GFE keycode
|
||||||
* @param keycode the code to be translated
|
* @param keycode the code to be translated
|
||||||
* @return a GFE keycode for the given keycode
|
* @return a GFE keycode for the given keycode
|
||||||
*/
|
*/
|
||||||
public static short translate(int keycode) {
|
@Override
|
||||||
|
public short translate(int keycode) {
|
||||||
int translated;
|
int translated;
|
||||||
|
|
||||||
/* There seems to be no clean mapping between Android key codes
|
/* There seems to be no clean mapping between Android key codes
|
||||||
|
|||||||
@@ -1,238 +1,116 @@
|
|||||||
package com.limelight.binding.input;
|
package com.limelight.binding.input;
|
||||||
|
|
||||||
import android.view.View;
|
|
||||||
|
|
||||||
import com.limelight.nvstream.NvConnection;
|
import com.limelight.nvstream.NvConnection;
|
||||||
import com.limelight.nvstream.input.MouseButtonPacket;
|
import com.limelight.nvstream.input.MouseButtonPacket;
|
||||||
|
|
||||||
import java.util.Timer;
|
|
||||||
import java.util.TimerTask;
|
|
||||||
|
|
||||||
public class TouchContext {
|
public class TouchContext {
|
||||||
private int lastTouchX = 0;
|
private int lastTouchX = 0;
|
||||||
private int lastTouchY = 0;
|
private int lastTouchY = 0;
|
||||||
private int originalTouchX = 0;
|
private int originalTouchX = 0;
|
||||||
private int originalTouchY = 0;
|
private int originalTouchY = 0;
|
||||||
private long originalTouchTime = 0;
|
private long originalTouchTime = 0;
|
||||||
private boolean cancelled;
|
private boolean cancelled;
|
||||||
private boolean confirmedMove;
|
|
||||||
private boolean confirmedDrag;
|
private NvConnection conn;
|
||||||
private Timer dragTimer;
|
private int actionIndex;
|
||||||
private double distanceMoved;
|
|
||||||
private double xFactor, yFactor;
|
private double xFactor, yFactor;
|
||||||
|
|
||||||
private final NvConnection conn;
|
private static final int TAP_MOVEMENT_THRESHOLD = 10;
|
||||||
private final int actionIndex;
|
private static final int TAP_TIME_THRESHOLD = 250;
|
||||||
private final int referenceWidth;
|
|
||||||
private final int referenceHeight;
|
public TouchContext(NvConnection conn, int actionIndex, double xFactor, double yFactor)
|
||||||
private final View targetView;
|
{
|
||||||
|
this.conn = conn;
|
||||||
private static final int TAP_MOVEMENT_THRESHOLD = 20;
|
this.actionIndex = actionIndex;
|
||||||
private static final int TAP_DISTANCE_THRESHOLD = 25;
|
this.xFactor = xFactor;
|
||||||
private static final int TAP_TIME_THRESHOLD = 250;
|
this.yFactor = yFactor;
|
||||||
private static final int DRAG_TIME_THRESHOLD = 650;
|
}
|
||||||
|
|
||||||
public TouchContext(NvConnection conn, int actionIndex,
|
|
||||||
int referenceWidth, int referenceHeight, View view)
|
|
||||||
{
|
|
||||||
this.conn = conn;
|
|
||||||
this.actionIndex = actionIndex;
|
|
||||||
this.referenceWidth = referenceWidth;
|
|
||||||
this.referenceHeight = referenceHeight;
|
|
||||||
this.targetView = view;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getActionIndex()
|
public int getActionIndex()
|
||||||
{
|
{
|
||||||
return actionIndex;
|
return actionIndex;
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean isWithinTapBounds(int touchX, int touchY)
|
private boolean isTap()
|
||||||
{
|
{
|
||||||
int xDelta = Math.abs(touchX - originalTouchX);
|
int xDelta = Math.abs(lastTouchX - originalTouchX);
|
||||||
int yDelta = Math.abs(touchY - originalTouchY);
|
int yDelta = Math.abs(lastTouchY - originalTouchY);
|
||||||
return xDelta <= TAP_MOVEMENT_THRESHOLD &&
|
long timeDelta = System.currentTimeMillis() - originalTouchTime;
|
||||||
yDelta <= TAP_MOVEMENT_THRESHOLD;
|
|
||||||
}
|
return xDelta <= TAP_MOVEMENT_THRESHOLD &&
|
||||||
|
yDelta <= TAP_MOVEMENT_THRESHOLD &&
|
||||||
private boolean isTap()
|
timeDelta <= TAP_TIME_THRESHOLD;
|
||||||
{
|
}
|
||||||
long timeDelta = System.currentTimeMillis() - originalTouchTime;
|
|
||||||
|
private byte getMouseButtonIndex()
|
||||||
return isWithinTapBounds(lastTouchX, lastTouchY) && timeDelta <= TAP_TIME_THRESHOLD;
|
{
|
||||||
}
|
if (actionIndex == 1) {
|
||||||
|
return MouseButtonPacket.BUTTON_RIGHT;
|
||||||
private byte getMouseButtonIndex()
|
}
|
||||||
{
|
else {
|
||||||
if (actionIndex == 1) {
|
return MouseButtonPacket.BUTTON_LEFT;
|
||||||
return MouseButtonPacket.BUTTON_RIGHT;
|
}
|
||||||
}
|
}
|
||||||
else {
|
|
||||||
return MouseButtonPacket.BUTTON_LEFT;
|
public boolean touchDownEvent(int eventX, int eventY)
|
||||||
}
|
{
|
||||||
}
|
originalTouchX = lastTouchX = eventX;
|
||||||
|
originalTouchY = lastTouchY = eventY;
|
||||||
public boolean touchDownEvent(int eventX, int eventY)
|
originalTouchTime = System.currentTimeMillis();
|
||||||
{
|
cancelled = false;
|
||||||
// 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();
|
|
||||||
cancelled = confirmedDrag = confirmedMove = false;
|
|
||||||
distanceMoved = 0;
|
|
||||||
|
|
||||||
if (actionIndex == 0) {
|
|
||||||
// Start the timer for engaging a drag
|
|
||||||
startDragTimer();
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void touchUpEvent(int eventX, int eventY)
|
public void touchUpEvent(int eventX, int eventY)
|
||||||
{
|
{
|
||||||
if (cancelled) {
|
if (cancelled) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cancel the drag timer
|
if (isTap())
|
||||||
cancelDragTimer();
|
{
|
||||||
|
byte buttonIndex = getMouseButtonIndex();
|
||||||
byte buttonIndex = getMouseButtonIndex();
|
|
||||||
|
// Lower the mouse button
|
||||||
if (confirmedDrag) {
|
conn.sendMouseButtonDown(buttonIndex);
|
||||||
// Raise the button after a drag
|
|
||||||
conn.sendMouseButtonUp(buttonIndex);
|
// We need to sleep a bit here because some games
|
||||||
}
|
// do input detection by polling
|
||||||
else if (isTap())
|
try {
|
||||||
{
|
Thread.sleep(100);
|
||||||
// Lower the mouse button
|
} catch (InterruptedException ignored) {}
|
||||||
conn.sendMouseButtonDown(buttonIndex);
|
|
||||||
|
// Raise the mouse button
|
||||||
// We need to sleep a bit here because some games
|
conn.sendMouseButtonUp(buttonIndex);
|
||||||
// do input detection by polling
|
}
|
||||||
try {
|
}
|
||||||
Thread.sleep(100);
|
|
||||||
} catch (InterruptedException ignored) {}
|
public boolean touchMoveEvent(int eventX, int eventY)
|
||||||
|
|
||||||
// Raise the mouse button
|
|
||||||
conn.sendMouseButtonUp(buttonIndex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private synchronized void startDragTimer() {
|
|
||||||
dragTimer = new Timer(true);
|
|
||||||
dragTimer.schedule(new TimerTask() {
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
synchronized (TouchContext.this) {
|
|
||||||
// Check if someone already set move
|
|
||||||
if (confirmedMove) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if someone cancelled us
|
|
||||||
if (dragTimer == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Uncancellable now
|
|
||||||
dragTimer = null;
|
|
||||||
|
|
||||||
// We haven't been cancelled before the timer expired so begin dragging
|
|
||||||
confirmedDrag = true;
|
|
||||||
conn.sendMouseButtonDown(getMouseButtonIndex());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, DRAG_TIME_THRESHOLD);
|
|
||||||
}
|
|
||||||
|
|
||||||
private synchronized void cancelDragTimer() {
|
|
||||||
if (dragTimer != null) {
|
|
||||||
dragTimer.cancel();
|
|
||||||
dragTimer = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private synchronized void checkForConfirmedMove(int eventX, int eventY) {
|
|
||||||
// If we've already confirmed something, get out now
|
|
||||||
if (confirmedMove || confirmedDrag) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If it leaves the tap bounds before the drag time expires, it's a move.
|
|
||||||
if (!isWithinTapBounds(eventX, eventY)) {
|
|
||||||
confirmedMove = true;
|
|
||||||
cancelDragTimer();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if we've exceeded the maximum distance moved
|
|
||||||
distanceMoved += Math.sqrt(Math.pow(eventX - lastTouchX, 2) + Math.pow(eventY - lastTouchY, 2));
|
|
||||||
if (distanceMoved >= TAP_DISTANCE_THRESHOLD) {
|
|
||||||
confirmedMove = true;
|
|
||||||
cancelDragTimer();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean touchMoveEvent(int eventX, int eventY)
|
|
||||||
{
|
{
|
||||||
if (eventX != lastTouchX || eventY != lastTouchY)
|
if (eventX != lastTouchX || eventY != lastTouchY)
|
||||||
{
|
{
|
||||||
// We only send moves and drags for the primary touch point
|
// We only send moves for the primary touch point
|
||||||
if (actionIndex == 0) {
|
if (actionIndex == 0) {
|
||||||
checkForConfirmedMove(eventX, eventY);
|
|
||||||
|
|
||||||
int deltaX = eventX - lastTouchX;
|
int deltaX = eventX - lastTouchX;
|
||||||
int deltaY = eventY - lastTouchY;
|
int deltaY = eventY - lastTouchY;
|
||||||
|
|
||||||
// Scale the deltas based on the factors passed to our constructor
|
// Scale the deltas based on the factors passed to our constructor
|
||||||
deltaX = (int)Math.round((double)Math.abs(deltaX) * xFactor);
|
deltaX = (int)Math.round((double)deltaX * xFactor);
|
||||||
deltaY = (int)Math.round((double)Math.abs(deltaY) * yFactor);
|
deltaY = (int)Math.round((double)deltaY * yFactor);
|
||||||
|
|
||||||
// Fix up the signs
|
conn.sendMouseMove((short)deltaX, (short)deltaY);
|
||||||
if (eventX < lastTouchX) {
|
}
|
||||||
deltaX = -deltaX;
|
|
||||||
}
|
lastTouchX = eventX;
|
||||||
if (eventY < lastTouchY) {
|
lastTouchY = eventY;
|
||||||
deltaY = -deltaY;
|
}
|
||||||
}
|
|
||||||
|
return true;
|
||||||
// If the scaling factor ended up rounding deltas to zero, wait until they are
|
}
|
||||||
// non-zero to update lastTouch that way devices that report small touch events often
|
|
||||||
// will work correctly
|
|
||||||
if (deltaX != 0) {
|
|
||||||
lastTouchX = eventX;
|
|
||||||
}
|
|
||||||
if (deltaY != 0) {
|
|
||||||
lastTouchY = eventY;
|
|
||||||
}
|
|
||||||
|
|
||||||
conn.sendMouseMove((short)deltaX, (short)deltaY);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
lastTouchX = eventX;
|
|
||||||
lastTouchY = eventY;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void cancelTouch() {
|
public void cancelTouch() {
|
||||||
cancelled = true;
|
cancelled = true;
|
||||||
|
|
||||||
// Cancel the drag timer
|
|
||||||
cancelDragTimer();
|
|
||||||
|
|
||||||
// If it was a confirmed drag, we'll need to raise the button now
|
|
||||||
if (confirmedDrag) {
|
|
||||||
conn.sendMouseButtonUp(getMouseButtonIndex());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isCancelled() {
|
public boolean isCancelled() {
|
||||||
|
|||||||
-53
@@ -1,53 +0,0 @@
|
|||||||
package com.limelight.binding.input.capture;
|
|
||||||
|
|
||||||
import android.annotation.TargetApi;
|
|
||||||
import android.os.Build;
|
|
||||||
import android.view.InputDevice;
|
|
||||||
import android.view.MotionEvent;
|
|
||||||
import android.view.View;
|
|
||||||
|
|
||||||
@TargetApi(Build.VERSION_CODES.O)
|
|
||||||
public class AndroidNativePointerCaptureProvider extends InputCaptureProvider {
|
|
||||||
|
|
||||||
private View targetView;
|
|
||||||
|
|
||||||
public AndroidNativePointerCaptureProvider(View targetView) {
|
|
||||||
this.targetView = targetView;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static boolean isCaptureProviderSupported() {
|
|
||||||
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.O;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void enableCapture() {
|
|
||||||
super.enableCapture();
|
|
||||||
targetView.requestPointerCapture();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void disableCapture() {
|
|
||||||
super.disableCapture();
|
|
||||||
targetView.releasePointerCapture();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean isCapturingActive() {
|
|
||||||
return targetView.hasPointerCapture();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean eventHasRelativeMouseAxes(MotionEvent event) {
|
|
||||||
return event.getSource() == InputDevice.SOURCE_MOUSE_RELATIVE;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public float getRelativeAxisX(MotionEvent event) {
|
|
||||||
return event.getX();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public float getRelativeAxisY(MotionEvent event) {
|
|
||||||
return event.getY();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-61
@@ -1,61 +0,0 @@
|
|||||||
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 AndroidPointerIconCaptureProvider extends InputCaptureProvider {
|
|
||||||
private ViewGroup rootViewGroup;
|
|
||||||
private Context context;
|
|
||||||
|
|
||||||
public AndroidPointerIconCaptureProvider(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() {
|
|
||||||
super.enableCapture();
|
|
||||||
setPointerIconOnAllViews(PointerIcon.getSystemIcon(context, PointerIcon.TYPE_NULL));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void disableCapture() {
|
|
||||||
super.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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
package com.limelight.binding.input.capture;
|
|
||||||
|
|
||||||
import android.app.Activity;
|
|
||||||
|
|
||||||
import com.limelight.LimeLog;
|
|
||||||
import com.limelight.R;
|
|
||||||
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) {
|
|
||||||
if (AndroidNativePointerCaptureProvider.isCaptureProviderSupported()) {
|
|
||||||
LimeLog.info("Using Android O+ native mouse capture");
|
|
||||||
return new AndroidNativePointerCaptureProvider(activity.findViewById(R.id.surfaceView));
|
|
||||||
}
|
|
||||||
else 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 (AndroidPointerIconCaptureProvider.isCaptureProviderSupported()) {
|
|
||||||
// Android N's native capture can't capture over system UI elements
|
|
||||||
// so we want to only use it if there's no other option.
|
|
||||||
LimeLog.info("Using Android N+ pointer hiding");
|
|
||||||
return new AndroidPointerIconCaptureProvider(activity);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
LimeLog.info("Mouse capture not available");
|
|
||||||
return new NullCaptureProvider();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
package com.limelight.binding.input.capture;
|
|
||||||
|
|
||||||
import android.view.MotionEvent;
|
|
||||||
|
|
||||||
public abstract class InputCaptureProvider {
|
|
||||||
protected boolean isCapturing;
|
|
||||||
|
|
||||||
public void enableCapture() {
|
|
||||||
isCapturing = true;
|
|
||||||
}
|
|
||||||
public void disableCapture() {
|
|
||||||
isCapturing = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void destroy() {}
|
|
||||||
|
|
||||||
public boolean isCapturingEnabled() {
|
|
||||||
return isCapturing;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isCapturingActive() {
|
|
||||||
return isCapturing;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean eventHasRelativeMouseAxes(MotionEvent event) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
public float getRelativeAxisX(MotionEvent event) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
public float getRelativeAxisY(MotionEvent event) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
package com.limelight.binding.input.capture;
|
|
||||||
|
|
||||||
|
|
||||||
public class NullCaptureProvider extends InputCaptureProvider {}
|
|
||||||
@@ -1,91 +0,0 @@
|
|||||||
package com.limelight.binding.input.capture;
|
|
||||||
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.hardware.input.InputManager;
|
|
||||||
import android.view.MotionEvent;
|
|
||||||
|
|
||||||
import java.lang.reflect.Field;
|
|
||||||
import java.lang.reflect.InvocationTargetException;
|
|
||||||
import java.lang.reflect.Method;
|
|
||||||
|
|
||||||
// NVIDIA extended the Android input APIs with support for using an attached mouse in relative
|
|
||||||
// mode without having to grab the input device (which requires root). The data comes in the form
|
|
||||||
// of new AXIS_RELATIVE_X and AXIS_RELATIVE_Y constants in the mouse's MotionEvent objects and
|
|
||||||
// a new function, InputManager.setCursorVisibility(), that allows the cursor to be hidden.
|
|
||||||
//
|
|
||||||
// http://docs.nvidia.com/gameworks/index.html#technologies/mobile/game_controller_handling_mouse.htm
|
|
||||||
|
|
||||||
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);
|
|
||||||
|
|
||||||
Field fieldRelX = MotionEvent.class.getField("AXIS_RELATIVE_X");
|
|
||||||
Field fieldRelY = MotionEvent.class.getField("AXIS_RELATIVE_Y");
|
|
||||||
|
|
||||||
AXIS_RELATIVE_X = (Integer) fieldRelX.get(null);
|
|
||||||
AXIS_RELATIVE_Y = (Integer) fieldRelY.get(null);
|
|
||||||
|
|
||||||
nvExtensionSupported = true;
|
|
||||||
} catch (Exception e) {
|
|
||||||
nvExtensionSupported = 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;
|
|
||||||
} catch (InvocationTargetException e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
} catch (IllegalAccessException e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void enableCapture() {
|
|
||||||
super.enableCapture();
|
|
||||||
setCursorVisibility(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void disableCapture() {
|
|
||||||
super.disableCapture();
|
|
||||||
setCursorVisibility(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean eventHasRelativeMouseAxes(MotionEvent event) {
|
|
||||||
return event.getAxisValue(AXIS_RELATIVE_X) != 0 ||
|
|
||||||
event.getAxisValue(AXIS_RELATIVE_Y) != 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public float getRelativeAxisX(MotionEvent event) {
|
|
||||||
return event.getAxisValue(AXIS_RELATIVE_X);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public float getRelativeAxisY(MotionEvent event) {
|
|
||||||
return event.getAxisValue(AXIS_RELATIVE_Y);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
package com.limelight.binding.input.driver;
|
|
||||||
|
|
||||||
public abstract class AbstractController {
|
|
||||||
|
|
||||||
private final int deviceId;
|
|
||||||
|
|
||||||
private UsbDriverListener listener;
|
|
||||||
|
|
||||||
protected short buttonFlags;
|
|
||||||
protected float leftTrigger, rightTrigger;
|
|
||||||
protected float rightStickX, rightStickY;
|
|
||||||
protected float leftStickX, leftStickY;
|
|
||||||
|
|
||||||
public int getControllerId() {
|
|
||||||
return deviceId;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected void setButtonFlag(int buttonFlag, int data) {
|
|
||||||
if (data != 0) {
|
|
||||||
buttonFlags |= buttonFlag;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
buttonFlags &= ~buttonFlag;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected void reportInput() {
|
|
||||||
listener.reportControllerState(deviceId, buttonFlags, leftStickX, leftStickY,
|
|
||||||
rightStickX, rightStickY, leftTrigger, rightTrigger);
|
|
||||||
}
|
|
||||||
|
|
||||||
public abstract boolean start();
|
|
||||||
public abstract void stop();
|
|
||||||
|
|
||||||
public AbstractController(int deviceId, UsbDriverListener listener) {
|
|
||||||
this.deviceId = deviceId;
|
|
||||||
this.listener = listener;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected void notifyDeviceRemoved() {
|
|
||||||
listener.deviceRemoved(deviceId);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected void notifyDeviceAdded() {
|
|
||||||
listener.deviceAdded(deviceId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,149 +0,0 @@
|
|||||||
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 java.nio.ByteBuffer;
|
|
||||||
import java.nio.ByteOrder;
|
|
||||||
|
|
||||||
public abstract class AbstractXboxController extends AbstractController {
|
|
||||||
protected final UsbDevice device;
|
|
||||||
protected final UsbDeviceConnection connection;
|
|
||||||
|
|
||||||
private Thread inputThread;
|
|
||||||
private boolean stopped;
|
|
||||||
|
|
||||||
protected UsbEndpoint inEndpt, outEndpt;
|
|
||||||
|
|
||||||
public AbstractXboxController(UsbDevice device, UsbDeviceConnection connection, int deviceId, UsbDriverListener listener) {
|
|
||||||
super(deviceId, listener);
|
|
||||||
this.device = device;
|
|
||||||
this.connection = connection;
|
|
||||||
}
|
|
||||||
|
|
||||||
private Thread createInputThread() {
|
|
||||||
return new Thread() {
|
|
||||||
public void run() {
|
|
||||||
while (!isInterrupted() && !stopped) {
|
|
||||||
byte[] buffer = new byte[64];
|
|
||||||
|
|
||||||
int res;
|
|
||||||
|
|
||||||
//
|
|
||||||
// There's no way that I can tell to determine if a device has failed
|
|
||||||
// or if the timeout has simply expired. We'll check how long the transfer
|
|
||||||
// took to fail and assume the device failed if it happened before the timeout
|
|
||||||
// expired.
|
|
||||||
//
|
|
||||||
|
|
||||||
do {
|
|
||||||
// Read the next input state packet
|
|
||||||
long lastMillis = MediaCodecHelper.getMonotonicMillis();
|
|
||||||
res = connection.bulkTransfer(inEndpt, buffer, buffer.length, 3000);
|
|
||||||
|
|
||||||
// If we get a zero length response, treat it as an error
|
|
||||||
if (res == 0) {
|
|
||||||
res = -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (res == -1 && MediaCodecHelper.getMonotonicMillis() - lastMillis < 1000) {
|
|
||||||
LimeLog.warning("Detected device I/O error");
|
|
||||||
AbstractXboxController.this.stop();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
} while (res == -1 && !isInterrupted() && !stopped);
|
|
||||||
|
|
||||||
if (res == -1 || stopped) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (handleRead(ByteBuffer.wrap(buffer, 0, res).order(ByteOrder.LITTLE_ENDIAN))) {
|
|
||||||
// Report input if handleRead() returns true
|
|
||||||
reportInput();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean start() {
|
|
||||||
// Force claim all interfaces
|
|
||||||
for (int i = 0; i < device.getInterfaceCount(); i++) {
|
|
||||||
UsbInterface iface = device.getInterface(i);
|
|
||||||
|
|
||||||
if (!connection.claimInterface(iface, true)) {
|
|
||||||
LimeLog.warning("Failed to claim interfaces");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Find the endpoints
|
|
||||||
UsbInterface iface = device.getInterface(0);
|
|
||||||
for (int i = 0; i < iface.getEndpointCount(); i++) {
|
|
||||||
UsbEndpoint endpt = iface.getEndpoint(i);
|
|
||||||
if (endpt.getDirection() == UsbConstants.USB_DIR_IN) {
|
|
||||||
if (inEndpt != null) {
|
|
||||||
LimeLog.warning("Found duplicate IN endpoint");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
inEndpt = endpt;
|
|
||||||
}
|
|
||||||
else if (endpt.getDirection() == UsbConstants.USB_DIR_OUT) {
|
|
||||||
if (outEndpt != null) {
|
|
||||||
LimeLog.warning("Found duplicate OUT endpoint");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
outEndpt = endpt;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Make sure the required endpoints were present
|
|
||||||
if (inEndpt == null || outEndpt == null) {
|
|
||||||
LimeLog.warning("Missing required endpoint");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Run the init function
|
|
||||||
if (!doInit()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Report that we're added _before_ starting the input thread
|
|
||||||
notifyDeviceAdded();
|
|
||||||
|
|
||||||
// Start listening for controller input
|
|
||||||
inputThread = createInputThread();
|
|
||||||
inputThread.start();
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void stop() {
|
|
||||||
if (stopped) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
stopped = true;
|
|
||||||
|
|
||||||
// Stop the input thread
|
|
||||||
if (inputThread != null) {
|
|
||||||
inputThread.interrupt();
|
|
||||||
inputThread = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Close the USB connection
|
|
||||||
connection.close();
|
|
||||||
|
|
||||||
// Report the device removed
|
|
||||||
notifyDeviceRemoved();
|
|
||||||
}
|
|
||||||
|
|
||||||
protected abstract boolean handleRead(ByteBuffer buffer);
|
|
||||||
protected abstract boolean doInit();
|
|
||||||
}
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
package com.limelight.binding.input.driver;
|
|
||||||
|
|
||||||
public interface UsbDriverListener {
|
|
||||||
void reportControllerState(int controllerId, short buttonFlags,
|
|
||||||
float leftStickX, float leftStickY,
|
|
||||||
float rightStickX, float rightStickY,
|
|
||||||
float leftTrigger, float rightTrigger);
|
|
||||||
|
|
||||||
void deviceRemoved(int controllerId);
|
|
||||||
void deviceAdded(int controllerId);
|
|
||||||
}
|
|
||||||
@@ -1,213 +0,0 @@
|
|||||||
package com.limelight.binding.input.driver;
|
|
||||||
|
|
||||||
import android.app.PendingIntent;
|
|
||||||
import android.app.Service;
|
|
||||||
import android.content.BroadcastReceiver;
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.content.IntentFilter;
|
|
||||||
import android.hardware.usb.UsbDevice;
|
|
||||||
import android.hardware.usb.UsbDeviceConnection;
|
|
||||||
import android.hardware.usb.UsbManager;
|
|
||||||
import android.os.Binder;
|
|
||||||
import android.os.Build;
|
|
||||||
import android.os.IBinder;
|
|
||||||
import android.view.InputDevice;
|
|
||||||
|
|
||||||
import com.limelight.LimeLog;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
|
|
||||||
public class UsbDriverService extends Service implements UsbDriverListener {
|
|
||||||
|
|
||||||
private static final String ACTION_USB_PERMISSION =
|
|
||||||
"com.limelight.USB_PERMISSION";
|
|
||||||
|
|
||||||
private UsbManager usbManager;
|
|
||||||
|
|
||||||
private final UsbEventReceiver receiver = new UsbEventReceiver();
|
|
||||||
private final UsbDriverBinder binder = new UsbDriverBinder();
|
|
||||||
|
|
||||||
private final ArrayList<AbstractController> controllers = new ArrayList<>();
|
|
||||||
|
|
||||||
private UsbDriverListener listener;
|
|
||||||
private int nextDeviceId;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void reportControllerState(int controllerId, short buttonFlags, float leftStickX, float leftStickY, float rightStickX, float rightStickY, float leftTrigger, float rightTrigger) {
|
|
||||||
// Call through to the client's listener
|
|
||||||
if (listener != null) {
|
|
||||||
listener.reportControllerState(controllerId, buttonFlags, leftStickX, leftStickY, rightStickX, rightStickY, leftTrigger, rightTrigger);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void deviceRemoved(int controllerId) {
|
|
||||||
// Remove the the controller from our list (if not removed already)
|
|
||||||
for (AbstractController controller : controllers) {
|
|
||||||
if (controller.getControllerId() == controllerId) {
|
|
||||||
controllers.remove(controller);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Call through to the client's listener
|
|
||||||
if (listener != null) {
|
|
||||||
listener.deviceRemoved(controllerId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void deviceAdded(int controllerId) {
|
|
||||||
// Call through to the client's listener
|
|
||||||
if (listener != null) {
|
|
||||||
listener.deviceAdded(controllerId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public class UsbEventReceiver extends BroadcastReceiver {
|
|
||||||
@Override
|
|
||||||
public void onReceive(Context context, Intent intent) {
|
|
||||||
String action = intent.getAction();
|
|
||||||
|
|
||||||
// Initial attachment broadcast
|
|
||||||
if (action.equals(UsbManager.ACTION_USB_DEVICE_ATTACHED)) {
|
|
||||||
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 = 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)) {
|
|
||||||
handleUsbDeviceState(device);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public class UsbDriverBinder extends Binder {
|
|
||||||
public void setListener(UsbDriverListener listener) {
|
|
||||||
UsbDriverService.this.listener = listener;
|
|
||||||
|
|
||||||
// Report all controllerMap that already exist
|
|
||||||
if (listener != null) {
|
|
||||||
for (AbstractController controller : controllers) {
|
|
||||||
listener.deviceAdded(controller.getControllerId());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void handleUsbDeviceState(UsbDevice device) {
|
|
||||||
// Are we able to operate it?
|
|
||||||
if (shouldClaimDevice(device)) {
|
|
||||||
// Do we have permission yet?
|
|
||||||
if (!usbManager.hasPermission(device)) {
|
|
||||||
// Let's ask for permission
|
|
||||||
usbManager.requestPermission(device, PendingIntent.getBroadcast(UsbDriverService.this, 0, new Intent(ACTION_USB_PERMISSION), 0));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Open the device
|
|
||||||
UsbDeviceConnection connection = usbManager.openDevice(device);
|
|
||||||
if (connection == null) {
|
|
||||||
LimeLog.warning("Unable to open USB device: "+device.getDeviceName());
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
AbstractController controller;
|
|
||||||
|
|
||||||
if (XboxOneController.canClaimDevice(device)) {
|
|
||||||
controller = new XboxOneController(device, connection, nextDeviceId++, this);
|
|
||||||
}
|
|
||||||
else if (Xbox360Controller.canClaimDevice(device)) {
|
|
||||||
controller = new Xbox360Controller(device, connection, nextDeviceId++, this);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
// Unreachable
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Start the controller
|
|
||||||
if (!controller.start()) {
|
|
||||||
connection.close();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add this controller to the list
|
|
||||||
controllers.add(controller);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean isRecognizedInputDevice(UsbDevice device) {
|
|
||||||
// On KitKat and later, we can determine if this VID and PID combo
|
|
||||||
// matches an existing input device and defer to the built-in controller
|
|
||||||
// support in that case. Prior to KitKat, we'll always return true to be safe.
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
|
|
||||||
for (int id : InputDevice.getDeviceIds()) {
|
|
||||||
InputDevice inputDev = InputDevice.getDevice(id);
|
|
||||||
|
|
||||||
if (inputDev.getVendorId() == device.getVendorId() &&
|
|
||||||
inputDev.getProductId() == device.getProductId()) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean shouldClaimDevice(UsbDevice device) {
|
|
||||||
// We always bind to XB1 controllers but only bind to XB360 controllers
|
|
||||||
// if we know the kernel isn't already driving this device.
|
|
||||||
return XboxOneController.canClaimDevice(device) ||
|
|
||||||
(!isRecognizedInputDevice(device) && Xbox360Controller.canClaimDevice(device));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onCreate() {
|
|
||||||
this.usbManager = (UsbManager) getSystemService(Context.USB_SERVICE);
|
|
||||||
|
|
||||||
// Register for USB attach broadcasts and permission completions
|
|
||||||
IntentFilter filter = new IntentFilter();
|
|
||||||
filter.addAction(UsbManager.ACTION_USB_DEVICE_ATTACHED);
|
|
||||||
filter.addAction(ACTION_USB_PERMISSION);
|
|
||||||
registerReceiver(receiver, filter);
|
|
||||||
|
|
||||||
// Enumerate existing devices
|
|
||||||
for (UsbDevice dev : usbManager.getDeviceList().values()) {
|
|
||||||
if (shouldClaimDevice(dev)) {
|
|
||||||
// Start the process of claiming this device
|
|
||||||
handleUsbDeviceState(dev);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onDestroy() {
|
|
||||||
// Stop the attachment receiver
|
|
||||||
unregisterReceiver(receiver);
|
|
||||||
|
|
||||||
// Remove listeners
|
|
||||||
listener = null;
|
|
||||||
|
|
||||||
// Stop all controllers
|
|
||||||
while (controllers.size() > 0) {
|
|
||||||
// Stop and remove the controller
|
|
||||||
controllers.remove(0).stop();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public IBinder onBind(Intent intent) {
|
|
||||||
return binder;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,140 +0,0 @@
|
|||||||
package com.limelight.binding.input.driver;
|
|
||||||
|
|
||||||
import android.hardware.usb.UsbConstants;
|
|
||||||
import android.hardware.usb.UsbDevice;
|
|
||||||
import android.hardware.usb.UsbDeviceConnection;
|
|
||||||
|
|
||||||
import com.limelight.LimeLog;
|
|
||||||
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
|
|
||||||
|
|
||||||
private static final int[] SUPPORTED_VENDORS = {
|
|
||||||
0x044f, // Thrustmaster
|
|
||||||
0x045e, // Microsoft
|
|
||||||
0x046d, // Logitech
|
|
||||||
0x056e, // Elecom
|
|
||||||
0x06a3, // Saitek
|
|
||||||
0x0738, // Mad Catz
|
|
||||||
0x07ff, // Mad Catz
|
|
||||||
0x0e6f, // Unknown
|
|
||||||
0x0f0d, // Hori
|
|
||||||
0x11c9, // Nacon
|
|
||||||
0x12ab, // Unknown
|
|
||||||
0x1430, // RedOctane
|
|
||||||
0x146b, // BigBen
|
|
||||||
0x1532, // Razer Sabertooth
|
|
||||||
0x15e4, // Numark
|
|
||||||
0x162e, // Joytech
|
|
||||||
0x1689, // Razer Onza
|
|
||||||
0x1bad, // Harmonix
|
|
||||||
0x24c6, // PowerA
|
|
||||||
};
|
|
||||||
|
|
||||||
public static boolean canClaimDevice(UsbDevice device) {
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Xbox360Controller(UsbDevice device, UsbDeviceConnection connection, int deviceId, UsbDriverListener listener) {
|
|
||||||
super(device, connection, deviceId, listener);
|
|
||||||
}
|
|
||||||
|
|
||||||
private int unsignByte(byte b) {
|
|
||||||
if (b < 0) {
|
|
||||||
return b + 256;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
return b;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected boolean handleRead(ByteBuffer buffer) {
|
|
||||||
if (buffer.limit() < 14) {
|
|
||||||
LimeLog.severe("Read too small: "+buffer.limit());
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Skip first short
|
|
||||||
buffer.position(buffer.position() + 2);
|
|
||||||
|
|
||||||
// DPAD
|
|
||||||
byte b = buffer.get();
|
|
||||||
setButtonFlag(ControllerPacket.LEFT_FLAG, b & 0x04);
|
|
||||||
setButtonFlag(ControllerPacket.RIGHT_FLAG, b & 0x08);
|
|
||||||
setButtonFlag(ControllerPacket.UP_FLAG, b & 0x01);
|
|
||||||
setButtonFlag(ControllerPacket.DOWN_FLAG, b & 0x02);
|
|
||||||
|
|
||||||
// Start/Select
|
|
||||||
setButtonFlag(ControllerPacket.PLAY_FLAG, b & 0x10);
|
|
||||||
setButtonFlag(ControllerPacket.BACK_FLAG, b & 0x20);
|
|
||||||
|
|
||||||
// LS/RS
|
|
||||||
setButtonFlag(ControllerPacket.LS_CLK_FLAG, b & 0x40);
|
|
||||||
setButtonFlag(ControllerPacket.RS_CLK_FLAG, b & 0x80);
|
|
||||||
|
|
||||||
// ABXY buttons
|
|
||||||
b = buffer.get();
|
|
||||||
setButtonFlag(ControllerPacket.A_FLAG, b & 0x10);
|
|
||||||
setButtonFlag(ControllerPacket.B_FLAG, b & 0x20);
|
|
||||||
setButtonFlag(ControllerPacket.X_FLAG, b & 0x40);
|
|
||||||
setButtonFlag(ControllerPacket.Y_FLAG, b & 0x80);
|
|
||||||
|
|
||||||
// LB/RB
|
|
||||||
setButtonFlag(ControllerPacket.LB_FLAG, b & 0x01);
|
|
||||||
setButtonFlag(ControllerPacket.RB_FLAG, b & 0x02);
|
|
||||||
|
|
||||||
// Xbox button
|
|
||||||
setButtonFlag(ControllerPacket.SPECIAL_BUTTON_FLAG, b & 0x04);
|
|
||||||
|
|
||||||
// Triggers
|
|
||||||
leftTrigger = unsignByte(buffer.get()) / 255.0f;
|
|
||||||
rightTrigger = unsignByte(buffer.get()) / 255.0f;
|
|
||||||
|
|
||||||
// Left stick
|
|
||||||
leftStickX = buffer.getShort() / 32767.0f;
|
|
||||||
leftStickY = ~buffer.getShort() / 32767.0f;
|
|
||||||
|
|
||||||
// Right stick
|
|
||||||
rightStickX = buffer.getShort() / 32767.0f;
|
|
||||||
rightStickY = ~buffer.getShort() / 32767.0f;
|
|
||||||
|
|
||||||
// Return true to send input
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean sendLedCommand(byte command) {
|
|
||||||
byte[] commandBuffer = {0x01, 0x03, command};
|
|
||||||
|
|
||||||
int res = connection.bulkTransfer(outEndpt, commandBuffer, commandBuffer.length, 3000);
|
|
||||||
if (res != commandBuffer.length) {
|
|
||||||
LimeLog.warning("LED set transfer failed: "+res);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected boolean doInit() {
|
|
||||||
// Turn the LED on corresponding to our device ID
|
|
||||||
sendLedCommand((byte)(2 + (getControllerId() % 4)));
|
|
||||||
|
|
||||||
// No need to fail init if the LED command fails
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,123 +0,0 @@
|
|||||||
package com.limelight.binding.input.driver;
|
|
||||||
|
|
||||||
import android.hardware.usb.UsbConstants;
|
|
||||||
import android.hardware.usb.UsbDevice;
|
|
||||||
import android.hardware.usb.UsbDeviceConnection;
|
|
||||||
|
|
||||||
import com.limelight.LimeLog;
|
|
||||||
import com.limelight.nvstream.input.ControllerPacket;
|
|
||||||
|
|
||||||
import java.nio.ByteBuffer;
|
|
||||||
|
|
||||||
public class XboxOneController extends AbstractXboxController {
|
|
||||||
|
|
||||||
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
|
|
||||||
0x1532, // Razer Wildcat
|
|
||||||
0x24c6, // PowerA
|
|
||||||
};
|
|
||||||
|
|
||||||
// FIXME: odata_serial
|
|
||||||
private static final byte[] XB1_INIT_DATA = {0x05, 0x20, 0x00, 0x01, 0x00};
|
|
||||||
|
|
||||||
public XboxOneController(UsbDevice device, UsbDeviceConnection connection, int deviceId, UsbDriverListener listener) {
|
|
||||||
super(device, connection, deviceId, listener);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void processButtons(ByteBuffer buffer) {
|
|
||||||
byte b = buffer.get();
|
|
||||||
|
|
||||||
setButtonFlag(ControllerPacket.PLAY_FLAG, b & 0x04);
|
|
||||||
setButtonFlag(ControllerPacket.BACK_FLAG, b & 0x08);
|
|
||||||
|
|
||||||
setButtonFlag(ControllerPacket.A_FLAG, b & 0x10);
|
|
||||||
setButtonFlag(ControllerPacket.B_FLAG, b & 0x20);
|
|
||||||
setButtonFlag(ControllerPacket.X_FLAG, b & 0x40);
|
|
||||||
setButtonFlag(ControllerPacket.Y_FLAG, b & 0x80);
|
|
||||||
|
|
||||||
b = buffer.get();
|
|
||||||
setButtonFlag(ControllerPacket.LEFT_FLAG, b & 0x04);
|
|
||||||
setButtonFlag(ControllerPacket.RIGHT_FLAG, b & 0x08);
|
|
||||||
setButtonFlag(ControllerPacket.UP_FLAG, b & 0x01);
|
|
||||||
setButtonFlag(ControllerPacket.DOWN_FLAG, b & 0x02);
|
|
||||||
|
|
||||||
setButtonFlag(ControllerPacket.LB_FLAG, b & 0x10);
|
|
||||||
setButtonFlag(ControllerPacket.RB_FLAG, b & 0x20);
|
|
||||||
|
|
||||||
setButtonFlag(ControllerPacket.LS_CLK_FLAG, b & 0x40);
|
|
||||||
setButtonFlag(ControllerPacket.RS_CLK_FLAG, b & 0x80);
|
|
||||||
|
|
||||||
leftTrigger = buffer.getShort() / 1023.0f;
|
|
||||||
rightTrigger = buffer.getShort() / 1023.0f;
|
|
||||||
|
|
||||||
leftStickX = buffer.getShort() / 32767.0f;
|
|
||||||
leftStickY = ~buffer.getShort() / 32767.0f;
|
|
||||||
|
|
||||||
rightStickX = buffer.getShort() / 32767.0f;
|
|
||||||
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())
|
|
||||||
{
|
|
||||||
case 0x20:
|
|
||||||
buffer.position(buffer.position()+3);
|
|
||||||
processButtons(buffer);
|
|
||||||
return true;
|
|
||||||
|
|
||||||
case 0x07:
|
|
||||||
// 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static boolean canClaimDevice(UsbDevice device) {
|
|
||||||
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
|
|
||||||
protected boolean doInit() {
|
|
||||||
// Send the initialization packet
|
|
||||||
int res = connection.bulkTransfer(outEndpt, XB1_INIT_DATA, XB1_INIT_DATA.length, 3000);
|
|
||||||
if (res != XB1_INIT_DATA.length) {
|
|
||||||
LimeLog.warning("Initialization transfer failed: "+res);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,327 +0,0 @@
|
|||||||
package com.limelight.binding.input.evdev;
|
|
||||||
|
|
||||||
import android.app.Activity;
|
|
||||||
import android.os.Build;
|
|
||||||
import android.os.Looper;
|
|
||||||
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;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.InputStream;
|
|
||||||
import java.io.OutputStream;
|
|
||||||
import java.net.ServerSocket;
|
|
||||||
import java.net.Socket;
|
|
||||||
|
|
||||||
public class EvdevCaptureProvider extends InputCaptureProvider {
|
|
||||||
|
|
||||||
private final EvdevListener listener;
|
|
||||||
private final String libraryPath;
|
|
||||||
|
|
||||||
private boolean shutdown = false;
|
|
||||||
private InputStream evdevIn;
|
|
||||||
private OutputStream evdevOut;
|
|
||||||
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;
|
|
||||||
|
|
||||||
private final Thread handlerThread = new Thread() {
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
int deltaX = 0;
|
|
||||||
int deltaY = 0;
|
|
||||||
byte deltaScroll = 0;
|
|
||||||
|
|
||||||
// Bind a local listening socket for evdevreader to connect to
|
|
||||||
try {
|
|
||||||
servSock = new ServerSocket(0, 1);
|
|
||||||
} catch (IOException e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
final String evdevReaderCmd = libraryPath+File.separatorChar+"libevdev_reader.so "+servSock.getLocalPort();
|
|
||||||
|
|
||||||
// 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);
|
|
||||||
|
|
||||||
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
|
|
||||||
LimeLog.info("Waiting for EvdevReader connection to port "+servSock.getLocalPort());
|
|
||||||
try {
|
|
||||||
evdevSock = servSock.accept();
|
|
||||||
evdevIn = evdevSock.getInputStream();
|
|
||||||
evdevOut = evdevSock.getOutputStream();
|
|
||||||
} catch (IOException e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
LimeLog.info("EvdevReader connected from port "+evdevSock.getPort());
|
|
||||||
|
|
||||||
while (!isInterrupted() && !shutdown) {
|
|
||||||
EvdevEvent event;
|
|
||||||
try {
|
|
||||||
event = EvdevReader.read(evdevIn);
|
|
||||||
} catch (IOException e) {
|
|
||||||
event = null;
|
|
||||||
}
|
|
||||||
if (event == null) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (event.type) {
|
|
||||||
case EvdevEvent.EV_SYN:
|
|
||||||
if (deltaX != 0 || deltaY != 0) {
|
|
||||||
listener.mouseMove(deltaX, deltaY);
|
|
||||||
deltaX = deltaY = 0;
|
|
||||||
}
|
|
||||||
if (deltaScroll != 0) {
|
|
||||||
listener.mouseScroll(deltaScroll);
|
|
||||||
deltaScroll = 0;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case EvdevEvent.EV_REL:
|
|
||||||
switch (event.code) {
|
|
||||||
case EvdevEvent.REL_X:
|
|
||||||
deltaX = event.value;
|
|
||||||
break;
|
|
||||||
case EvdevEvent.REL_Y:
|
|
||||||
deltaY = event.value;
|
|
||||||
break;
|
|
||||||
case EvdevEvent.REL_WHEEL:
|
|
||||||
deltaScroll = (byte) event.value;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case EvdevEvent.EV_KEY:
|
|
||||||
switch (event.code) {
|
|
||||||
case EvdevEvent.BTN_LEFT:
|
|
||||||
listener.mouseButtonEvent(EvdevListener.BUTTON_LEFT,
|
|
||||||
event.value != 0);
|
|
||||||
break;
|
|
||||||
case EvdevEvent.BTN_MIDDLE:
|
|
||||||
listener.mouseButtonEvent(EvdevListener.BUTTON_MIDDLE,
|
|
||||||
event.value != 0);
|
|
||||||
break;
|
|
||||||
case EvdevEvent.BTN_RIGHT:
|
|
||||||
listener.mouseButtonEvent(EvdevListener.BUTTON_RIGHT,
|
|
||||||
event.value != 0);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case EvdevEvent.BTN_SIDE:
|
|
||||||
case EvdevEvent.BTN_EXTRA:
|
|
||||||
case EvdevEvent.BTN_FORWARD:
|
|
||||||
case EvdevEvent.BTN_BACK:
|
|
||||||
case EvdevEvent.BTN_TASK:
|
|
||||||
// Other unhandled mouse buttons
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
|
||||||
// We got some unrecognized button. This means
|
|
||||||
// someone is trying to use the other device in this
|
|
||||||
// "combination" input device. We'll try to handle
|
|
||||||
// it via keyboard, but we're not going to disconnect
|
|
||||||
// if we can't
|
|
||||||
short keyCode = EvdevTranslator.translateEvdevKeyCode(event.code);
|
|
||||||
if (keyCode != 0) {
|
|
||||||
listener.keyboardEvent(event.value != 0, keyCode);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case EvdevEvent.EV_MSC:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
public EvdevCaptureProvider(Activity activity, EvdevListener listener) {
|
|
||||||
this.listener = listener;
|
|
||||||
this.activity = activity;
|
|
||||||
this.libraryPath = activity.getApplicationInfo().nativeLibraryDir;
|
|
||||||
}
|
|
||||||
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private void runInNetworkSafeContextSynchronously(Runnable runnable) {
|
|
||||||
// This function is used to avoid Android's strict NetworkOnMainThreadException.
|
|
||||||
// For our usage, it is highly unlikely to cause problems since we only do
|
|
||||||
// write operations and only to localhost sockets.
|
|
||||||
if (Looper.getMainLooper().getThread() == Thread.currentThread()) {
|
|
||||||
Thread t = new Thread(runnable);
|
|
||||||
t.start();
|
|
||||||
try {
|
|
||||||
t.join();
|
|
||||||
} catch (InterruptedException e) {
|
|
||||||
// The main thread should never be interrupted
|
|
||||||
e.printStackTrace();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
// Run the runnable directly
|
|
||||||
runnable.run();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void enableCapture() {
|
|
||||||
super.enableCapture();
|
|
||||||
if (!started) {
|
|
||||||
// Start the handler thread if it's our first time
|
|
||||||
// capturing
|
|
||||||
handlerThread.start();
|
|
||||||
started = true;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
// This may be called on the main thread
|
|
||||||
runInNetworkSafeContextSynchronously(new Runnable() {
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
// Send a request to regrab if we're already capturing
|
|
||||||
if (!shutdown && evdevOut != null) {
|
|
||||||
try {
|
|
||||||
evdevOut.write(REGRAB_REQUEST);
|
|
||||||
} catch (IOException e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void disableCapture() {
|
|
||||||
super.disableCapture();
|
|
||||||
// This may be called on the main thread
|
|
||||||
runInNetworkSafeContextSynchronously(new Runnable() {
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
if (started && !shutdown && evdevOut != null) {
|
|
||||||
try {
|
|
||||||
evdevOut.write(UNGRAB_REQUEST);
|
|
||||||
} catch (IOException e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@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.
|
|
||||||
//
|
|
||||||
// This may be called on the main thread.
|
|
||||||
|
|
||||||
if (!started) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
shutdown = true;
|
|
||||||
handlerThread.interrupt();
|
|
||||||
|
|
||||||
runInNetworkSafeContextSynchronously(new Runnable() {
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
if (servSock != null) {
|
|
||||||
try {
|
|
||||||
servSock.close();
|
|
||||||
} catch (IOException e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (evdevSock != null) {
|
|
||||||
try {
|
|
||||||
evdevSock.close();
|
|
||||||
} catch (IOException e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (evdevIn != null) {
|
|
||||||
try {
|
|
||||||
evdevIn.close();
|
|
||||||
} catch (IOException e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (evdevOut != null) {
|
|
||||||
try {
|
|
||||||
evdevOut.close();
|
|
||||||
} catch (IOException e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (su != null) {
|
|
||||||
su.destroy();
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
handlerThread.join();
|
|
||||||
} catch (InterruptedException ignored) {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,37 +1,41 @@
|
|||||||
package com.limelight.binding.input.evdev;
|
package com.limelight.binding.input.evdev;
|
||||||
|
|
||||||
public class EvdevEvent {
|
public class EvdevEvent {
|
||||||
public static final int EVDEV_MIN_EVENT_SIZE = 16;
|
public static final int EVDEV_MIN_EVENT_SIZE = 16;
|
||||||
public static final int EVDEV_MAX_EVENT_SIZE = 24;
|
public static final int EVDEV_MAX_EVENT_SIZE = 24;
|
||||||
|
|
||||||
/* Event types */
|
/* Event types */
|
||||||
public static final short EV_SYN = 0x00;
|
public static final short EV_SYN = 0x00;
|
||||||
public static final short EV_KEY = 0x01;
|
public static final short EV_KEY = 0x01;
|
||||||
public static final short EV_REL = 0x02;
|
public static final short EV_REL = 0x02;
|
||||||
public static final short EV_MSC = 0x04;
|
public static final short EV_MSC = 0x04;
|
||||||
|
|
||||||
/* Relative axes */
|
/* Relative axes */
|
||||||
public static final short REL_X = 0x00;
|
public static final short REL_X = 0x00;
|
||||||
public static final short REL_Y = 0x01;
|
public static final short REL_Y = 0x01;
|
||||||
public static final short REL_WHEEL = 0x08;
|
public static final short REL_WHEEL = 0x08;
|
||||||
|
|
||||||
/* Buttons */
|
/* Buttons */
|
||||||
public static final short BTN_LEFT = 0x110;
|
public static final short BTN_LEFT = 0x110;
|
||||||
public static final short BTN_RIGHT = 0x111;
|
public static final short BTN_RIGHT = 0x111;
|
||||||
public static final short BTN_MIDDLE = 0x112;
|
public static final short BTN_MIDDLE = 0x112;
|
||||||
public static final short BTN_SIDE = 0x113;
|
public static final short BTN_SIDE = 0x113;
|
||||||
public static final short BTN_EXTRA = 0x114;
|
public static final short BTN_EXTRA = 0x114;
|
||||||
public static final short BTN_FORWARD = 0x115;
|
public static final short BTN_FORWARD = 0x115;
|
||||||
public static final short BTN_BACK = 0x116;
|
public static final short BTN_BACK = 0x116;
|
||||||
public static final short BTN_TASK = 0x117;
|
public static final short BTN_TASK = 0x117;
|
||||||
|
public static final short BTN_GAMEPAD = 0x130;
|
||||||
public final short type;
|
|
||||||
public final short code;
|
/* Keys */
|
||||||
public final int value;
|
public static final short KEY_Q = 16;
|
||||||
|
|
||||||
public EvdevEvent(short type, short code, int value) {
|
public short type;
|
||||||
this.type = type;
|
public short code;
|
||||||
this.code = code;
|
public int value;
|
||||||
this.value = value;
|
|
||||||
}
|
public EvdevEvent(short type, short code, int value) {
|
||||||
|
this.type = type;
|
||||||
|
this.code = code;
|
||||||
|
this.value = value;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,167 @@
|
|||||||
|
package com.limelight.binding.input.evdev;
|
||||||
|
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
import java.nio.ByteOrder;
|
||||||
|
|
||||||
|
import com.limelight.LimeLog;
|
||||||
|
|
||||||
|
public class EvdevHandler {
|
||||||
|
|
||||||
|
private String absolutePath;
|
||||||
|
private EvdevListener listener;
|
||||||
|
private boolean shutdown = false;
|
||||||
|
private int fd = -1;
|
||||||
|
|
||||||
|
private Thread handlerThread = new Thread() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
// All the finally blocks here make this code look like a mess
|
||||||
|
// but it's important that we get this right to avoid causing
|
||||||
|
// system-wide input problems.
|
||||||
|
|
||||||
|
// Open the /dev/input/eventX file
|
||||||
|
fd = EvdevReader.open(absolutePath);
|
||||||
|
if (fd == -1) {
|
||||||
|
LimeLog.warning("Unable to open "+absolutePath);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Check if it's a mouse or keyboard, but not a gamepad
|
||||||
|
if ((!EvdevReader.isMouse(fd) && !EvdevReader.isAlphaKeyboard(fd)) ||
|
||||||
|
EvdevReader.isGamepad(fd)) {
|
||||||
|
// We only handle keyboards and mice
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Grab it for ourselves
|
||||||
|
if (!EvdevReader.grab(fd)) {
|
||||||
|
LimeLog.warning("Unable to grab "+absolutePath);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
LimeLog.info("Grabbed device for raw keyboard/mouse input: "+absolutePath);
|
||||||
|
|
||||||
|
ByteBuffer buffer = ByteBuffer.allocate(EvdevEvent.EVDEV_MAX_EVENT_SIZE).order(ByteOrder.nativeOrder());
|
||||||
|
|
||||||
|
try {
|
||||||
|
int deltaX = 0;
|
||||||
|
int deltaY = 0;
|
||||||
|
byte deltaScroll = 0;
|
||||||
|
|
||||||
|
while (!isInterrupted() && !shutdown) {
|
||||||
|
EvdevEvent event = EvdevReader.read(fd, buffer);
|
||||||
|
if (event == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (event.type)
|
||||||
|
{
|
||||||
|
case EvdevEvent.EV_SYN:
|
||||||
|
if (deltaX != 0 || deltaY != 0) {
|
||||||
|
listener.mouseMove(deltaX, deltaY);
|
||||||
|
deltaX = deltaY = 0;
|
||||||
|
}
|
||||||
|
if (deltaScroll != 0) {
|
||||||
|
listener.mouseScroll(deltaScroll);
|
||||||
|
deltaScroll = 0;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case EvdevEvent.EV_REL:
|
||||||
|
switch (event.code)
|
||||||
|
{
|
||||||
|
case EvdevEvent.REL_X:
|
||||||
|
deltaX = event.value;
|
||||||
|
break;
|
||||||
|
case EvdevEvent.REL_Y:
|
||||||
|
deltaY = event.value;
|
||||||
|
break;
|
||||||
|
case EvdevEvent.REL_WHEEL:
|
||||||
|
deltaScroll = (byte) event.value;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case EvdevEvent.EV_KEY:
|
||||||
|
switch (event.code)
|
||||||
|
{
|
||||||
|
case EvdevEvent.BTN_LEFT:
|
||||||
|
listener.mouseButtonEvent(EvdevListener.BUTTON_LEFT,
|
||||||
|
event.value != 0);
|
||||||
|
break;
|
||||||
|
case EvdevEvent.BTN_MIDDLE:
|
||||||
|
listener.mouseButtonEvent(EvdevListener.BUTTON_MIDDLE,
|
||||||
|
event.value != 0);
|
||||||
|
break;
|
||||||
|
case EvdevEvent.BTN_RIGHT:
|
||||||
|
listener.mouseButtonEvent(EvdevListener.BUTTON_RIGHT,
|
||||||
|
event.value != 0);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case EvdevEvent.BTN_SIDE:
|
||||||
|
case EvdevEvent.BTN_EXTRA:
|
||||||
|
case EvdevEvent.BTN_FORWARD:
|
||||||
|
case EvdevEvent.BTN_BACK:
|
||||||
|
case EvdevEvent.BTN_TASK:
|
||||||
|
// Other unhandled mouse buttons
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
// We got some unrecognized button. This means
|
||||||
|
// someone is trying to use the other device in this
|
||||||
|
// "combination" input device. We'll try to handle
|
||||||
|
// it via keyboard, but we're not going to disconnect
|
||||||
|
// if we can't
|
||||||
|
short keyCode = EvdevTranslator.translateEvdevKeyCode(event.code);
|
||||||
|
if (keyCode != 0) {
|
||||||
|
listener.keyboardEvent(event.value != 0, keyCode);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case EvdevEvent.EV_MSC:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
// Release our grab
|
||||||
|
EvdevReader.ungrab(fd);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
// Close the file
|
||||||
|
EvdevReader.close(fd);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
public EvdevHandler(String absolutePath, EvdevListener listener) {
|
||||||
|
this.absolutePath = absolutePath;
|
||||||
|
this.listener = listener;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void start() {
|
||||||
|
handlerThread.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void stop() {
|
||||||
|
// Close the fd. It doesn't matter if this races
|
||||||
|
// with the handler thread. We'll close this out from
|
||||||
|
// under the thread to wake it up
|
||||||
|
if (fd != -1) {
|
||||||
|
EvdevReader.close(fd);
|
||||||
|
}
|
||||||
|
|
||||||
|
shutdown = true;
|
||||||
|
handlerThread.interrupt();
|
||||||
|
|
||||||
|
try {
|
||||||
|
handlerThread.join();
|
||||||
|
} catch (InterruptedException ignored) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void notifyDeleted() {
|
||||||
|
stop();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,12 +1,12 @@
|
|||||||
package com.limelight.binding.input.evdev;
|
package com.limelight.binding.input.evdev;
|
||||||
|
|
||||||
public interface EvdevListener {
|
public interface EvdevListener {
|
||||||
int BUTTON_LEFT = 1;
|
public static final int BUTTON_LEFT = 1;
|
||||||
int BUTTON_MIDDLE = 2;
|
public static final int BUTTON_MIDDLE = 2;
|
||||||
int BUTTON_RIGHT = 3;
|
public static final int BUTTON_RIGHT = 3;
|
||||||
|
|
||||||
void mouseMove(int deltaX, int deltaY);
|
public void mouseMove(int deltaX, int deltaY);
|
||||||
void mouseButtonEvent(int buttonId, boolean down);
|
public void mouseButtonEvent(int buttonId, boolean down);
|
||||||
void mouseScroll(byte amount);
|
public void mouseScroll(byte amount);
|
||||||
void keyboardEvent(boolean buttonDown, short keyCode);
|
public void keyboardEvent(boolean buttonDown, short keyCode);
|
||||||
}
|
}
|
||||||
@@ -1,58 +1,105 @@
|
|||||||
package com.limelight.binding.input.evdev;
|
package com.limelight.binding.input.evdev;
|
||||||
|
|
||||||
import java.io.IOException;
|
import android.os.Build;
|
||||||
import java.io.InputStream;
|
|
||||||
import java.nio.ByteBuffer;
|
import java.nio.ByteBuffer;
|
||||||
import java.nio.ByteOrder;
|
import java.util.Locale;
|
||||||
|
|
||||||
import com.limelight.LimeLog;
|
import com.limelight.LimeLog;
|
||||||
|
|
||||||
public class EvdevReader {
|
public class EvdevReader {
|
||||||
private static void readAll(InputStream in, ByteBuffer bb) throws IOException {
|
static {
|
||||||
byte[] buf = bb.array();
|
System.loadLibrary("evdev_reader");
|
||||||
int ret;
|
}
|
||||||
int offset = 0;
|
|
||||||
|
|
||||||
while (offset < buf.length) {
|
public static void patchSeLinuxPolicies() {
|
||||||
ret = in.read(buf, offset, buf.length-offset);
|
//
|
||||||
if (ret <= 0) {
|
// FIXME: We REALLY shouldn't being changing permissions on the input devices like this.
|
||||||
throw new IOException("Read failed: "+ret);
|
// We should probably do something clever with a separate daemon and talk via a localhost
|
||||||
}
|
// socket. We don't return the SELinux policies back to default after we're done which I feel
|
||||||
|
// bad about, but we do chmod the input devices back so I don't think any additional attack surface
|
||||||
offset += ret;
|
// remains opened after streaming other than listing the /dev/input directory which you wouldn't
|
||||||
|
// normally be able to do with SELinux enforcing on Lollipop.
|
||||||
|
//
|
||||||
|
// We need to modify SELinux policies to allow us to capture input devices on Lollipop and possibly other
|
||||||
|
// more restrictive ROMs. Per Chainfire's SuperSU documentation, the supolicy binary is provided on
|
||||||
|
// 4.4 and later to do live SELinux policy changes.
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
|
||||||
|
EvdevShell shell = EvdevShell.getInstance();
|
||||||
|
shell.runCommand("supolicy --live \"allow untrusted_app input_device dir { getattr read search }\" " +
|
||||||
|
"\"allow untrusted_app input_device chr_file { open read write ioctl }\"");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Requires root to chmod /dev/input/eventX
|
||||||
|
public static void setPermissions(String[] files, int octalPermissions) {
|
||||||
|
EvdevShell shell = EvdevShell.getInstance();
|
||||||
|
|
||||||
// Takes a byte buffer to use to read the output into.
|
for (String file : files) {
|
||||||
// This buffer MUST be in native byte order and at least
|
shell.runCommand(String.format((Locale)null, "chmod %o %s", octalPermissions, file));
|
||||||
// EVDEV_MAX_EVENT_SIZE bytes long.
|
|
||||||
public static EvdevEvent read(InputStream input) throws IOException {
|
|
||||||
ByteBuffer bb;
|
|
||||||
int packetLength;
|
|
||||||
|
|
||||||
// Read the packet length
|
|
||||||
bb = ByteBuffer.allocate(4).order(ByteOrder.nativeOrder());
|
|
||||||
readAll(input, bb);
|
|
||||||
packetLength = bb.getInt();
|
|
||||||
|
|
||||||
if (packetLength < EvdevEvent.EVDEV_MIN_EVENT_SIZE) {
|
|
||||||
LimeLog.warning("Short read: "+packetLength);
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
// Read the rest of the packet
|
|
||||||
bb = ByteBuffer.allocate(packetLength).order(ByteOrder.nativeOrder());
|
// Returns the fd to be passed to other function or -1 on error
|
||||||
readAll(input, bb);
|
public static native int open(String fileName);
|
||||||
|
|
||||||
// Throw away the time stamp
|
// Prevent other apps (including Android itself) from using the device while "grabbed"
|
||||||
if (packetLength == EvdevEvent.EVDEV_MAX_EVENT_SIZE) {
|
public static native boolean grab(int fd);
|
||||||
bb.getLong();
|
public static native boolean ungrab(int fd);
|
||||||
bb.getLong();
|
|
||||||
} else {
|
// Used for checking device capabilities
|
||||||
bb.getInt();
|
public static native boolean hasRelAxis(int fd, short axis);
|
||||||
bb.getInt();
|
public static native boolean hasAbsAxis(int fd, short axis);
|
||||||
}
|
public static native boolean hasKey(int fd, short key);
|
||||||
|
|
||||||
return new EvdevEvent(bb.getShort(), bb.getShort(), bb.getInt());
|
public static boolean isMouse(int fd) {
|
||||||
}
|
// This is the same check that Android does in EventHub.cpp
|
||||||
|
return hasRelAxis(fd, EvdevEvent.REL_X) &&
|
||||||
|
hasRelAxis(fd, EvdevEvent.REL_Y) &&
|
||||||
|
hasKey(fd, EvdevEvent.BTN_LEFT);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean isAlphaKeyboard(int fd) {
|
||||||
|
// This is the same check that Android does in EventHub.cpp
|
||||||
|
return hasKey(fd, EvdevEvent.KEY_Q);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean isGamepad(int fd) {
|
||||||
|
return hasKey(fd, EvdevEvent.BTN_GAMEPAD);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns the bytes read or -1 on error
|
||||||
|
private static native int read(int fd, byte[] buffer);
|
||||||
|
|
||||||
|
// Takes a byte buffer to use to read the output into.
|
||||||
|
// This buffer MUST be in native byte order and at least
|
||||||
|
// EVDEV_MAX_EVENT_SIZE bytes long.
|
||||||
|
public static EvdevEvent read(int fd, ByteBuffer buffer) {
|
||||||
|
int bytesRead = read(fd, buffer.array());
|
||||||
|
if (bytesRead < 0) {
|
||||||
|
LimeLog.warning("Failed to read: "+bytesRead);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
else if (bytesRead < EvdevEvent.EVDEV_MIN_EVENT_SIZE) {
|
||||||
|
LimeLog.warning("Short read: "+bytesRead);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
buffer.limit(bytesRead);
|
||||||
|
buffer.rewind();
|
||||||
|
|
||||||
|
// Throw away the time stamp
|
||||||
|
if (bytesRead == EvdevEvent.EVDEV_MAX_EVENT_SIZE) {
|
||||||
|
buffer.getLong();
|
||||||
|
buffer.getLong();
|
||||||
|
} else {
|
||||||
|
buffer.getInt();
|
||||||
|
buffer.getInt();
|
||||||
|
}
|
||||||
|
|
||||||
|
return new EvdevEvent(buffer.getShort(), buffer.getShort(), buffer.getInt());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Closes the fd from open()
|
||||||
|
public static native int close(int fd);
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,116 @@
|
|||||||
|
package com.limelight.binding.input.evdev;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
import java.util.Scanner;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
public class EvdevShell {
|
||||||
|
private OutputStream stdin;
|
||||||
|
private InputStream stdout;
|
||||||
|
private Process shell;
|
||||||
|
private final String uuidString = UUID.randomUUID().toString();
|
||||||
|
|
||||||
|
private static final EvdevShell globalShell = new EvdevShell();
|
||||||
|
|
||||||
|
public static EvdevShell getInstance() {
|
||||||
|
return globalShell;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void startShell() {
|
||||||
|
ProcessBuilder builder = new ProcessBuilder("su");
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Redirect stderr to stdout
|
||||||
|
builder.redirectErrorStream(true);
|
||||||
|
shell = builder.start();
|
||||||
|
|
||||||
|
stdin = shell.getOutputStream();
|
||||||
|
stdout = shell.getInputStream();
|
||||||
|
} catch (IOException e) {
|
||||||
|
// This is unexpected
|
||||||
|
e.printStackTrace();
|
||||||
|
|
||||||
|
// Kill the shell if it spawned
|
||||||
|
if (stdin != null) {
|
||||||
|
try {
|
||||||
|
stdin.close();
|
||||||
|
} catch (IOException e1) {
|
||||||
|
e1.printStackTrace();
|
||||||
|
} finally {
|
||||||
|
stdin = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (stdout != null) {
|
||||||
|
try {
|
||||||
|
stdout.close();
|
||||||
|
} catch (IOException e1) {
|
||||||
|
e1.printStackTrace();
|
||||||
|
} finally {
|
||||||
|
stdout = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (shell != null) {
|
||||||
|
shell.destroy();
|
||||||
|
shell = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void runCommand(String command) {
|
||||||
|
if (shell == null) {
|
||||||
|
// Shell never started
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Write the command followed by an echo with our UUID
|
||||||
|
stdin.write((command+'\n').getBytes("UTF-8"));
|
||||||
|
stdin.write(("echo "+uuidString+'\n').getBytes("UTF-8"));
|
||||||
|
stdin.flush();
|
||||||
|
|
||||||
|
// This is the only command in flight so we can use a scanner
|
||||||
|
// without worrying about it eating too many characters
|
||||||
|
Scanner scanner = new Scanner(stdout);
|
||||||
|
while (scanner.hasNext()) {
|
||||||
|
if (scanner.next().contains(uuidString)) {
|
||||||
|
// Our command ran
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void stopShell() throws InterruptedException {
|
||||||
|
boolean exitWritten = false;
|
||||||
|
|
||||||
|
if (shell == null) {
|
||||||
|
// Shell never started
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
stdin.write("exit\n".getBytes("UTF-8"));
|
||||||
|
exitWritten = true;
|
||||||
|
} catch (IOException e) {
|
||||||
|
// We'll destroy the process without
|
||||||
|
// waiting for it to terminate since
|
||||||
|
// we don't know whether our exit command made it
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (exitWritten) {
|
||||||
|
try {
|
||||||
|
shell.waitFor();
|
||||||
|
} finally {
|
||||||
|
shell.destroy();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
shell.destroy();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,136 +4,136 @@ import android.view.KeyEvent;
|
|||||||
|
|
||||||
public class EvdevTranslator {
|
public class EvdevTranslator {
|
||||||
|
|
||||||
private static final short[] EVDEV_KEY_CODES = {
|
public static final short EVDEV_KEY_CODES[] = {
|
||||||
0, //KeyEvent.VK_RESERVED
|
0, //KeyEvent.VK_RESERVED
|
||||||
KeyEvent.KEYCODE_ESCAPE,
|
KeyEvent.KEYCODE_ESCAPE,
|
||||||
KeyEvent.KEYCODE_1,
|
KeyEvent.KEYCODE_1,
|
||||||
KeyEvent.KEYCODE_2,
|
KeyEvent.KEYCODE_2,
|
||||||
KeyEvent.KEYCODE_3,
|
KeyEvent.KEYCODE_3,
|
||||||
KeyEvent.KEYCODE_4,
|
KeyEvent.KEYCODE_4,
|
||||||
KeyEvent.KEYCODE_5,
|
KeyEvent.KEYCODE_5,
|
||||||
KeyEvent.KEYCODE_6,
|
KeyEvent.KEYCODE_6,
|
||||||
KeyEvent.KEYCODE_7,
|
KeyEvent.KEYCODE_7,
|
||||||
KeyEvent.KEYCODE_8,
|
KeyEvent.KEYCODE_8,
|
||||||
KeyEvent.KEYCODE_9,
|
KeyEvent.KEYCODE_9,
|
||||||
KeyEvent.KEYCODE_0,
|
KeyEvent.KEYCODE_0,
|
||||||
KeyEvent.KEYCODE_MINUS,
|
KeyEvent.KEYCODE_MINUS,
|
||||||
KeyEvent.KEYCODE_EQUALS,
|
KeyEvent.KEYCODE_EQUALS,
|
||||||
KeyEvent.KEYCODE_DEL,
|
KeyEvent.KEYCODE_DEL,
|
||||||
KeyEvent.KEYCODE_TAB,
|
KeyEvent.KEYCODE_TAB,
|
||||||
KeyEvent.KEYCODE_Q,
|
KeyEvent.KEYCODE_Q,
|
||||||
KeyEvent.KEYCODE_W,
|
KeyEvent.KEYCODE_W,
|
||||||
KeyEvent.KEYCODE_E,
|
KeyEvent.KEYCODE_E,
|
||||||
KeyEvent.KEYCODE_R,
|
KeyEvent.KEYCODE_R,
|
||||||
KeyEvent.KEYCODE_T,
|
KeyEvent.KEYCODE_T,
|
||||||
KeyEvent.KEYCODE_Y,
|
KeyEvent.KEYCODE_Y,
|
||||||
KeyEvent.KEYCODE_U,
|
KeyEvent.KEYCODE_U,
|
||||||
KeyEvent.KEYCODE_I,
|
KeyEvent.KEYCODE_I,
|
||||||
KeyEvent.KEYCODE_O,
|
KeyEvent.KEYCODE_O,
|
||||||
KeyEvent.KEYCODE_P,
|
KeyEvent.KEYCODE_P,
|
||||||
KeyEvent.KEYCODE_LEFT_BRACKET,
|
KeyEvent.KEYCODE_LEFT_BRACKET,
|
||||||
KeyEvent.KEYCODE_RIGHT_BRACKET,
|
KeyEvent.KEYCODE_RIGHT_BRACKET,
|
||||||
KeyEvent.KEYCODE_ENTER,
|
KeyEvent.KEYCODE_ENTER,
|
||||||
KeyEvent.KEYCODE_CTRL_LEFT,
|
KeyEvent.KEYCODE_CTRL_LEFT,
|
||||||
KeyEvent.KEYCODE_A,
|
KeyEvent.KEYCODE_A,
|
||||||
KeyEvent.KEYCODE_S,
|
KeyEvent.KEYCODE_S,
|
||||||
KeyEvent.KEYCODE_D,
|
KeyEvent.KEYCODE_D,
|
||||||
KeyEvent.KEYCODE_F,
|
KeyEvent.KEYCODE_F,
|
||||||
KeyEvent.KEYCODE_G,
|
KeyEvent.KEYCODE_G,
|
||||||
KeyEvent.KEYCODE_H,
|
KeyEvent.KEYCODE_H,
|
||||||
KeyEvent.KEYCODE_J,
|
KeyEvent.KEYCODE_J,
|
||||||
KeyEvent.KEYCODE_K,
|
KeyEvent.KEYCODE_K,
|
||||||
KeyEvent.KEYCODE_L,
|
KeyEvent.KEYCODE_L,
|
||||||
KeyEvent.KEYCODE_SEMICOLON,
|
KeyEvent.KEYCODE_SEMICOLON,
|
||||||
KeyEvent.KEYCODE_APOSTROPHE,
|
KeyEvent.KEYCODE_APOSTROPHE,
|
||||||
KeyEvent.KEYCODE_GRAVE,
|
KeyEvent.KEYCODE_GRAVE,
|
||||||
KeyEvent.KEYCODE_SHIFT_LEFT,
|
KeyEvent.KEYCODE_SHIFT_LEFT,
|
||||||
KeyEvent.KEYCODE_BACKSLASH,
|
KeyEvent.KEYCODE_BACKSLASH,
|
||||||
KeyEvent.KEYCODE_Z,
|
KeyEvent.KEYCODE_Z,
|
||||||
KeyEvent.KEYCODE_X,
|
KeyEvent.KEYCODE_X,
|
||||||
KeyEvent.KEYCODE_C,
|
KeyEvent.KEYCODE_C,
|
||||||
KeyEvent.KEYCODE_V,
|
KeyEvent.KEYCODE_V,
|
||||||
KeyEvent.KEYCODE_B,
|
KeyEvent.KEYCODE_B,
|
||||||
KeyEvent.KEYCODE_N,
|
KeyEvent.KEYCODE_N,
|
||||||
KeyEvent.KEYCODE_M,
|
KeyEvent.KEYCODE_M,
|
||||||
KeyEvent.KEYCODE_COMMA,
|
KeyEvent.KEYCODE_COMMA,
|
||||||
KeyEvent.KEYCODE_PERIOD,
|
KeyEvent.KEYCODE_PERIOD,
|
||||||
KeyEvent.KEYCODE_SLASH,
|
KeyEvent.KEYCODE_SLASH,
|
||||||
KeyEvent.KEYCODE_SHIFT_RIGHT,
|
KeyEvent.KEYCODE_SHIFT_RIGHT,
|
||||||
KeyEvent.KEYCODE_NUMPAD_MULTIPLY,
|
KeyEvent.KEYCODE_NUMPAD_MULTIPLY,
|
||||||
KeyEvent.KEYCODE_ALT_LEFT,
|
KeyEvent.KEYCODE_ALT_LEFT,
|
||||||
KeyEvent.KEYCODE_SPACE,
|
KeyEvent.KEYCODE_SPACE,
|
||||||
KeyEvent.KEYCODE_CAPS_LOCK,
|
KeyEvent.KEYCODE_CAPS_LOCK,
|
||||||
KeyEvent.KEYCODE_F1,
|
KeyEvent.KEYCODE_F1,
|
||||||
KeyEvent.KEYCODE_F2,
|
KeyEvent.KEYCODE_F2,
|
||||||
KeyEvent.KEYCODE_F3,
|
KeyEvent.KEYCODE_F3,
|
||||||
KeyEvent.KEYCODE_F4,
|
KeyEvent.KEYCODE_F4,
|
||||||
KeyEvent.KEYCODE_F5,
|
KeyEvent.KEYCODE_F5,
|
||||||
KeyEvent.KEYCODE_F6,
|
KeyEvent.KEYCODE_F6,
|
||||||
KeyEvent.KEYCODE_F7,
|
KeyEvent.KEYCODE_F7,
|
||||||
KeyEvent.KEYCODE_F8,
|
KeyEvent.KEYCODE_F8,
|
||||||
KeyEvent.KEYCODE_F9,
|
KeyEvent.KEYCODE_F9,
|
||||||
KeyEvent.KEYCODE_F10,
|
KeyEvent.KEYCODE_F10,
|
||||||
KeyEvent.KEYCODE_NUM_LOCK,
|
KeyEvent.KEYCODE_NUM_LOCK,
|
||||||
KeyEvent.KEYCODE_SCROLL_LOCK,
|
KeyEvent.KEYCODE_SCROLL_LOCK,
|
||||||
KeyEvent.KEYCODE_NUMPAD_7,
|
KeyEvent.KEYCODE_NUMPAD_7,
|
||||||
KeyEvent.KEYCODE_NUMPAD_8,
|
KeyEvent.KEYCODE_NUMPAD_8,
|
||||||
KeyEvent.KEYCODE_NUMPAD_9,
|
KeyEvent.KEYCODE_NUMPAD_9,
|
||||||
KeyEvent.KEYCODE_NUMPAD_SUBTRACT,
|
KeyEvent.KEYCODE_NUMPAD_SUBTRACT,
|
||||||
KeyEvent.KEYCODE_NUMPAD_4,
|
KeyEvent.KEYCODE_NUMPAD_4,
|
||||||
KeyEvent.KEYCODE_NUMPAD_5,
|
KeyEvent.KEYCODE_NUMPAD_5,
|
||||||
KeyEvent.KEYCODE_NUMPAD_6,
|
KeyEvent.KEYCODE_NUMPAD_6,
|
||||||
KeyEvent.KEYCODE_NUMPAD_ADD,
|
KeyEvent.KEYCODE_NUMPAD_ADD,
|
||||||
KeyEvent.KEYCODE_NUMPAD_1,
|
KeyEvent.KEYCODE_NUMPAD_1,
|
||||||
KeyEvent.KEYCODE_NUMPAD_2,
|
KeyEvent.KEYCODE_NUMPAD_2,
|
||||||
KeyEvent.KEYCODE_NUMPAD_3,
|
KeyEvent.KEYCODE_NUMPAD_3,
|
||||||
KeyEvent.KEYCODE_NUMPAD_0,
|
KeyEvent.KEYCODE_NUMPAD_0,
|
||||||
KeyEvent.KEYCODE_NUMPAD_DOT,
|
KeyEvent.KEYCODE_NUMPAD_DOT,
|
||||||
0,
|
0,
|
||||||
0, //KeyEvent.VK_ZENKAKUHANKAKU,
|
0, //KeyEvent.VK_ZENKAKUHANKAKU,
|
||||||
0, //KeyEvent.VK_102ND,
|
0, //KeyEvent.VK_102ND,
|
||||||
KeyEvent.KEYCODE_F11,
|
KeyEvent.KEYCODE_F11,
|
||||||
KeyEvent.KEYCODE_F12,
|
KeyEvent.KEYCODE_F12,
|
||||||
0, //KeyEvent.VK_RO,
|
0, //KeyEvent.VK_RO,
|
||||||
0, //KeyEvent.VK_KATAKANA,
|
0, //KeyEvent.VK_KATAKANA,
|
||||||
0, //KeyEvent.VK_HIRAGANA,
|
0, //KeyEvent.VK_HIRAGANA,
|
||||||
0, //KeyEvent.VK_HENKAN,
|
0, //KeyEvent.VK_HENKAN,
|
||||||
0, //KeyEvent.VK_KATAKANAHIRAGANA,
|
0, //KeyEvent.VK_KATAKANAHIRAGANA,
|
||||||
0, //KeyEvent.VK_MUHENKAN,
|
0, //KeyEvent.VK_MUHENKAN,
|
||||||
0, //KeyEvent.VK_KPJPCOMMA,
|
0, //KeyEvent.VK_KPJPCOMMA,
|
||||||
KeyEvent.KEYCODE_NUMPAD_ENTER,
|
KeyEvent.KEYCODE_NUMPAD_ENTER,
|
||||||
KeyEvent.KEYCODE_CTRL_RIGHT,
|
KeyEvent.KEYCODE_CTRL_RIGHT,
|
||||||
KeyEvent.KEYCODE_NUMPAD_DIVIDE,
|
KeyEvent.KEYCODE_NUMPAD_DIVIDE,
|
||||||
KeyEvent.KEYCODE_SYSRQ,
|
KeyEvent.KEYCODE_SYSRQ,
|
||||||
KeyEvent.KEYCODE_ALT_RIGHT,
|
KeyEvent.KEYCODE_ALT_RIGHT,
|
||||||
0, //KeyEvent.VK_LINEFEED,
|
0, //KeyEvent.VK_LINEFEED,
|
||||||
KeyEvent.KEYCODE_HOME,
|
KeyEvent.KEYCODE_HOME,
|
||||||
KeyEvent.KEYCODE_DPAD_UP,
|
KeyEvent.KEYCODE_DPAD_UP,
|
||||||
KeyEvent.KEYCODE_PAGE_UP,
|
KeyEvent.KEYCODE_PAGE_UP,
|
||||||
KeyEvent.KEYCODE_DPAD_LEFT,
|
KeyEvent.KEYCODE_DPAD_LEFT,
|
||||||
KeyEvent.KEYCODE_DPAD_RIGHT,
|
KeyEvent.KEYCODE_DPAD_RIGHT,
|
||||||
KeyEvent.KEYCODE_MOVE_END,
|
KeyEvent.KEYCODE_MOVE_END,
|
||||||
KeyEvent.KEYCODE_DPAD_DOWN,
|
KeyEvent.KEYCODE_DPAD_DOWN,
|
||||||
KeyEvent.KEYCODE_PAGE_DOWN,
|
KeyEvent.KEYCODE_PAGE_DOWN,
|
||||||
KeyEvent.KEYCODE_INSERT,
|
KeyEvent.KEYCODE_INSERT,
|
||||||
KeyEvent.KEYCODE_FORWARD_DEL,
|
KeyEvent.KEYCODE_FORWARD_DEL,
|
||||||
0, //KeyEvent.VK_MACRO,
|
0, //KeyEvent.VK_MACRO,
|
||||||
0, //KeyEvent.VK_MUTE,
|
0, //KeyEvent.VK_MUTE,
|
||||||
0, //KeyEvent.VK_VOLUMEDOWN,
|
0, //KeyEvent.VK_VOLUMEDOWN,
|
||||||
0, //KeyEvent.VK_VOLUMEUP,
|
0, //KeyEvent.VK_VOLUMEUP,
|
||||||
0, //KeyEvent.VK_POWER, /* SC System Power Down */
|
0, //KeyEvent.VK_POWER, /* SC System Power Down */
|
||||||
KeyEvent.KEYCODE_NUMPAD_EQUALS,
|
KeyEvent.KEYCODE_NUMPAD_EQUALS,
|
||||||
0, //KeyEvent.VK_KPPLUSMINUS,
|
0, //KeyEvent.VK_KPPLUSMINUS,
|
||||||
KeyEvent.KEYCODE_BREAK,
|
KeyEvent.KEYCODE_BREAK,
|
||||||
0, //KeyEvent.VK_SCALE, /* AL Compiz Scale (Expose) */
|
0, //KeyEvent.VK_SCALE, /* AL Compiz Scale (Expose) */
|
||||||
};
|
};
|
||||||
|
|
||||||
public static short translateEvdevKeyCode(short evdevKeyCode) {
|
public static short translateEvdevKeyCode(short evdevKeyCode) {
|
||||||
if (evdevKeyCode < EVDEV_KEY_CODES.length) {
|
if (evdevKeyCode < EVDEV_KEY_CODES.length) {
|
||||||
return EVDEV_KEY_CODES[evdevKeyCode];
|
return EVDEV_KEY_CODES[evdevKeyCode];
|
||||||
}
|
}
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,188 @@
|
|||||||
|
package com.limelight.binding.input.evdev;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import com.limelight.LimeLog;
|
||||||
|
|
||||||
|
import android.os.FileObserver;
|
||||||
|
|
||||||
|
@SuppressWarnings("ALL")
|
||||||
|
public class EvdevWatcher {
|
||||||
|
private static final String PATH = "/dev/input";
|
||||||
|
private static final String REQUIRED_FILE_PREFIX = "event";
|
||||||
|
|
||||||
|
private final HashMap<String, EvdevHandler> handlers = new HashMap<String, EvdevHandler>();
|
||||||
|
private boolean shutdown = false;
|
||||||
|
private boolean init = false;
|
||||||
|
private boolean ungrabbed = false;
|
||||||
|
private EvdevListener listener;
|
||||||
|
private Thread startThread;
|
||||||
|
|
||||||
|
private static boolean patchedSeLinuxPolicies = false;
|
||||||
|
|
||||||
|
private FileObserver observer = new FileObserver(PATH, FileObserver.CREATE | FileObserver.DELETE) {
|
||||||
|
@Override
|
||||||
|
public void onEvent(int event, String fileName) {
|
||||||
|
if (fileName == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!fileName.startsWith(REQUIRED_FILE_PREFIX)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
synchronized (handlers) {
|
||||||
|
if (shutdown) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((event & FileObserver.CREATE) != 0) {
|
||||||
|
LimeLog.info("Starting evdev handler for "+fileName);
|
||||||
|
|
||||||
|
if (!init) {
|
||||||
|
// If this a real new device, update permissions again so we can read it
|
||||||
|
EvdevReader.setPermissions(new String[]{PATH + "/" + fileName}, 0666);
|
||||||
|
}
|
||||||
|
|
||||||
|
EvdevHandler handler = new EvdevHandler(PATH + "/" + fileName, listener);
|
||||||
|
|
||||||
|
// If we're ungrabbed now, don't start the handler
|
||||||
|
if (!ungrabbed) {
|
||||||
|
handler.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
handlers.put(fileName, handler);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((event & FileObserver.DELETE) != 0) {
|
||||||
|
LimeLog.info("Halting evdev handler for "+fileName);
|
||||||
|
|
||||||
|
EvdevHandler handler = handlers.remove(fileName);
|
||||||
|
if (handler != null) {
|
||||||
|
handler.notifyDeleted();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
public EvdevWatcher(EvdevListener listener) {
|
||||||
|
this.listener = listener;
|
||||||
|
}
|
||||||
|
|
||||||
|
private File[] rundownWithPermissionsChange(int newPermissions) {
|
||||||
|
// Rundown existing files
|
||||||
|
File devInputDir = new File(PATH);
|
||||||
|
File[] files = devInputDir.listFiles();
|
||||||
|
if (files == null) {
|
||||||
|
return new File[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set desired permissions
|
||||||
|
String[] filePaths = new String[files.length];
|
||||||
|
for (int i = 0; i < files.length; i++) {
|
||||||
|
filePaths[i] = files[i].getAbsolutePath();
|
||||||
|
}
|
||||||
|
EvdevReader.setPermissions(filePaths, newPermissions);
|
||||||
|
|
||||||
|
return files;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ungrabAll() {
|
||||||
|
synchronized (handlers) {
|
||||||
|
// Note that we're ungrabbed for now
|
||||||
|
ungrabbed = true;
|
||||||
|
|
||||||
|
// Stop all handlers
|
||||||
|
for (EvdevHandler handler : handlers.values()) {
|
||||||
|
handler.stop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void regrabAll() {
|
||||||
|
synchronized (handlers) {
|
||||||
|
// We're regrabbing everything now
|
||||||
|
ungrabbed = false;
|
||||||
|
|
||||||
|
for (Map.Entry<String, EvdevHandler> entry : handlers.entrySet()) {
|
||||||
|
// We need to recreate each entry since we can't reuse a stopped one
|
||||||
|
entry.setValue(new EvdevHandler(PATH + "/" + entry.getKey(), listener));
|
||||||
|
entry.getValue().start();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void start() {
|
||||||
|
startThread = new Thread() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
// Initialize the root shell
|
||||||
|
EvdevShell.getInstance().startShell();
|
||||||
|
|
||||||
|
// Patch SELinux policies (if needed)
|
||||||
|
if (!patchedSeLinuxPolicies) {
|
||||||
|
EvdevReader.patchSeLinuxPolicies();
|
||||||
|
patchedSeLinuxPolicies = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// List all files and allow us access
|
||||||
|
File[] files = rundownWithPermissionsChange(0666);
|
||||||
|
|
||||||
|
init = true;
|
||||||
|
for (File f : files) {
|
||||||
|
observer.onEvent(FileObserver.CREATE, f.getName());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Done with initial onEvent calls
|
||||||
|
init = false;
|
||||||
|
|
||||||
|
// Start watching for new files
|
||||||
|
observer.startWatching();
|
||||||
|
|
||||||
|
synchronized (startThread) {
|
||||||
|
// Wait to be awoken again by shutdown()
|
||||||
|
try {
|
||||||
|
startThread.wait();
|
||||||
|
} catch (InterruptedException e) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Giveup eventX permissions
|
||||||
|
rundownWithPermissionsChange(0660);
|
||||||
|
|
||||||
|
// Kill the root shell
|
||||||
|
try {
|
||||||
|
EvdevShell.getInstance().stopShell();
|
||||||
|
} catch (InterruptedException e) {}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
startThread.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void shutdown() {
|
||||||
|
// Let start thread cleanup on it's own sweet time
|
||||||
|
synchronized (startThread) {
|
||||||
|
startThread.notify();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop the observer
|
||||||
|
observer.stopWatching();
|
||||||
|
|
||||||
|
synchronized (handlers) {
|
||||||
|
// Stop creating new handlers
|
||||||
|
shutdown = true;
|
||||||
|
|
||||||
|
// If we've already ungrabbed, there's nothing else to do
|
||||||
|
if (ungrabbed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop all handlers
|
||||||
|
for (EvdevHandler handler : handlers.values()) {
|
||||||
|
handler.stop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,349 +0,0 @@
|
|||||||
/**
|
|
||||||
* Created by Karim Mreisi.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package com.limelight.binding.input.virtual_controller;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.graphics.Canvas;
|
|
||||||
import android.graphics.Color;
|
|
||||||
import android.graphics.Paint;
|
|
||||||
import android.view.MotionEvent;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This is a analog stick on screen element. It is used to get 2-Axis user input.
|
|
||||||
*/
|
|
||||||
public class AnalogStick extends VirtualControllerElement {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* outer radius size in percent of the ui element
|
|
||||||
*/
|
|
||||||
public static final int SIZE_RADIUS_COMPLETE = 90;
|
|
||||||
/**
|
|
||||||
* analog stick size in percent of the ui element
|
|
||||||
*/
|
|
||||||
public static final int SIZE_RADIUS_ANALOG_STICK = 90;
|
|
||||||
/**
|
|
||||||
* dead zone size in percent of the ui element
|
|
||||||
*/
|
|
||||||
public static final int SIZE_RADIUS_DEADZONE = 90;
|
|
||||||
/**
|
|
||||||
* time frame for a double click
|
|
||||||
*/
|
|
||||||
public final static long timeoutDoubleClick = 350;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* touch down time until the deadzone is lifted to allow precise movements with the analog sticks
|
|
||||||
*/
|
|
||||||
public final static long timeoutDeadzone = 150;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Listener interface to update registered observers.
|
|
||||||
*/
|
|
||||||
public interface AnalogStickListener {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* onMovement event will be fired on real analog stick movement (outside of the deadzone).
|
|
||||||
*
|
|
||||||
* @param x horizontal position, value from -1.0 ... 0 .. 1.0
|
|
||||||
* @param y vertical position, value from -1.0 ... 0 .. 1.0
|
|
||||||
*/
|
|
||||||
void onMovement(float x, float y);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* onClick event will be fired on click on the analog stick
|
|
||||||
*/
|
|
||||||
void onClick();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* onDoubleClick event will be fired on a double click in a short time frame on the analog
|
|
||||||
* stick.
|
|
||||||
*/
|
|
||||||
void onDoubleClick();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* onRevoke event will be fired on unpress of the analog stick.
|
|
||||||
*/
|
|
||||||
void onRevoke();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Movement states of the analog sick.
|
|
||||||
*/
|
|
||||||
private enum STICK_STATE {
|
|
||||||
NO_MOVEMENT,
|
|
||||||
MOVED_IN_DEAD_ZONE,
|
|
||||||
MOVED_ACTIVE
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Click type states.
|
|
||||||
*/
|
|
||||||
private enum CLICK_STATE {
|
|
||||||
SINGLE,
|
|
||||||
DOUBLE
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* configuration if the analog stick should be displayed as circle or square
|
|
||||||
*/
|
|
||||||
private boolean circle_stick = true; // TODO: implement square sick for simulations
|
|
||||||
|
|
||||||
/**
|
|
||||||
* outer radius, this size will be automatically updated on resize
|
|
||||||
*/
|
|
||||||
private float radius_complete = 0;
|
|
||||||
/**
|
|
||||||
* analog stick radius, this size will be automatically updated on resize
|
|
||||||
*/
|
|
||||||
private float radius_analog_stick = 0;
|
|
||||||
/**
|
|
||||||
* dead zone radius, this size will be automatically updated on resize
|
|
||||||
*/
|
|
||||||
private float radius_dead_zone = 0;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* horizontal position in relation to the center of the element
|
|
||||||
*/
|
|
||||||
private float relative_x = 0;
|
|
||||||
/**
|
|
||||||
* vertical position in relation to the center of the element
|
|
||||||
*/
|
|
||||||
private float relative_y = 0;
|
|
||||||
|
|
||||||
|
|
||||||
private double movement_radius = 0;
|
|
||||||
private double movement_angle = 0;
|
|
||||||
|
|
||||||
private float position_stick_x = 0;
|
|
||||||
private float position_stick_y = 0;
|
|
||||||
|
|
||||||
private final Paint paint = new Paint();
|
|
||||||
|
|
||||||
private STICK_STATE stick_state = STICK_STATE.NO_MOVEMENT;
|
|
||||||
private CLICK_STATE click_state = CLICK_STATE.SINGLE;
|
|
||||||
|
|
||||||
private List<AnalogStickListener> listeners = new ArrayList<>();
|
|
||||||
private long timeLastClick = 0;
|
|
||||||
|
|
||||||
private static double getMovementRadius(float x, float y) {
|
|
||||||
return Math.sqrt(x * x + y * y);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static double getAngle(float way_x, float way_y) {
|
|
||||||
// prevent divisions by zero for corner cases
|
|
||||||
if (way_x == 0) {
|
|
||||||
return way_y < 0 ? Math.PI : 0;
|
|
||||||
} else if (way_y == 0) {
|
|
||||||
if (way_x > 0) {
|
|
||||||
return Math.PI * 3 / 2;
|
|
||||||
} else if (way_x < 0) {
|
|
||||||
return Math.PI * 1 / 2;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// return correct calculated angle for each quadrant
|
|
||||||
if (way_x > 0) {
|
|
||||||
if (way_y < 0) {
|
|
||||||
// first quadrant
|
|
||||||
return 3 * Math.PI / 2 + Math.atan((double) (-way_y / way_x));
|
|
||||||
} else {
|
|
||||||
// second quadrant
|
|
||||||
return Math.PI + Math.atan((double) (way_x / way_y));
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (way_y > 0) {
|
|
||||||
// third quadrant
|
|
||||||
return Math.PI / 2 + Math.atan((double) (way_y / -way_x));
|
|
||||||
} else {
|
|
||||||
// fourth quadrant
|
|
||||||
return 0 + Math.atan((double) (-way_x / -way_y));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public AnalogStick(VirtualController controller, Context context) {
|
|
||||||
super(controller, context);
|
|
||||||
// reset stick position
|
|
||||||
position_stick_x = getWidth() / 2;
|
|
||||||
position_stick_y = getHeight() / 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void addAnalogStickListener(AnalogStickListener listener) {
|
|
||||||
listeners.add(listener);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void notifyOnMovement(float x, float y) {
|
|
||||||
_DBG("movement x: " + x + " movement y: " + y);
|
|
||||||
// notify listeners
|
|
||||||
for (AnalogStickListener listener : listeners) {
|
|
||||||
listener.onMovement(x, y);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void notifyOnClick() {
|
|
||||||
_DBG("click");
|
|
||||||
// notify listeners
|
|
||||||
for (AnalogStickListener listener : listeners) {
|
|
||||||
listener.onClick();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void notifyOnDoubleClick() {
|
|
||||||
_DBG("double click");
|
|
||||||
// notify listeners
|
|
||||||
for (AnalogStickListener listener : listeners) {
|
|
||||||
listener.onDoubleClick();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void notifyOnRevoke() {
|
|
||||||
_DBG("revoke");
|
|
||||||
// notify listeners
|
|
||||||
for (AnalogStickListener listener : listeners) {
|
|
||||||
listener.onRevoke();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
|
|
||||||
// calculate new radius sizes depending
|
|
||||||
radius_complete = getPercent(getCorrectWidth() / 2, 90);
|
|
||||||
radius_dead_zone = getPercent(getCorrectWidth() / 2, 30);
|
|
||||||
radius_analog_stick = getPercent(getCorrectWidth() / 2, 20);
|
|
||||||
|
|
||||||
super.onSizeChanged(w, h, oldw, oldh);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onElementDraw(Canvas canvas) {
|
|
||||||
// set transparent background
|
|
||||||
canvas.drawColor(Color.TRANSPARENT);
|
|
||||||
|
|
||||||
paint.setStyle(Paint.Style.STROKE);
|
|
||||||
paint.setStrokeWidth(getDefaultStrokeWidth());
|
|
||||||
|
|
||||||
// draw outer circle
|
|
||||||
if (!isPressed() || click_state == CLICK_STATE.SINGLE) {
|
|
||||||
paint.setColor(getDefaultColor());
|
|
||||||
} else {
|
|
||||||
paint.setColor(pressedColor);
|
|
||||||
}
|
|
||||||
canvas.drawCircle(getWidth() / 2, getHeight() / 2, radius_complete, paint);
|
|
||||||
|
|
||||||
paint.setColor(getDefaultColor());
|
|
||||||
// draw dead zone
|
|
||||||
canvas.drawCircle(getWidth() / 2, getHeight() / 2, radius_dead_zone, paint);
|
|
||||||
|
|
||||||
// draw stick depending on state
|
|
||||||
switch (stick_state) {
|
|
||||||
case NO_MOVEMENT: {
|
|
||||||
paint.setColor(getDefaultColor());
|
|
||||||
canvas.drawCircle(getWidth() / 2, getHeight() / 2, radius_analog_stick, paint);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case MOVED_IN_DEAD_ZONE:
|
|
||||||
case MOVED_ACTIVE: {
|
|
||||||
paint.setColor(pressedColor);
|
|
||||||
canvas.drawCircle(position_stick_x, position_stick_y, radius_analog_stick, paint);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void updatePosition() {
|
|
||||||
// get 100% way
|
|
||||||
float complete = radius_complete - radius_analog_stick;
|
|
||||||
|
|
||||||
// calculate relative way
|
|
||||||
float correlated_y = (float) (Math.sin(Math.PI / 2 - movement_angle) * (movement_radius));
|
|
||||||
float correlated_x = (float) (Math.cos(Math.PI / 2 - movement_angle) * (movement_radius));
|
|
||||||
|
|
||||||
// update positions
|
|
||||||
position_stick_x = getWidth() / 2 - correlated_x;
|
|
||||||
position_stick_y = getHeight() / 2 - correlated_y;
|
|
||||||
|
|
||||||
// Stay active even if we're back in the deadzone because we know the user is actively
|
|
||||||
// giving analog stick input and we don't want to snap back into the deadzone.
|
|
||||||
// We also release the deadzone if the user keeps the stick pressed for a bit to allow
|
|
||||||
// them to make precise movements.
|
|
||||||
stick_state = (stick_state == STICK_STATE.MOVED_ACTIVE ||
|
|
||||||
System.currentTimeMillis() - timeLastClick > timeoutDeadzone ||
|
|
||||||
movement_radius > radius_dead_zone) ?
|
|
||||||
STICK_STATE.MOVED_ACTIVE : STICK_STATE.MOVED_IN_DEAD_ZONE;
|
|
||||||
|
|
||||||
// trigger move event if state active
|
|
||||||
if (stick_state == STICK_STATE.MOVED_ACTIVE) {
|
|
||||||
notifyOnMovement(-correlated_x / complete, correlated_y / complete);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean onElementTouchEvent(MotionEvent event) {
|
|
||||||
// save last click state
|
|
||||||
CLICK_STATE lastClickState = click_state;
|
|
||||||
|
|
||||||
// get absolute way for each axis
|
|
||||||
relative_x = -(getWidth() / 2 - event.getX());
|
|
||||||
relative_y = -(getHeight() / 2 - event.getY());
|
|
||||||
|
|
||||||
// get radius and angel of movement from center
|
|
||||||
movement_radius = getMovementRadius(relative_x, relative_y);
|
|
||||||
movement_angle = getAngle(relative_x, relative_y);
|
|
||||||
|
|
||||||
// chop radius if out of outer circle and already pressed
|
|
||||||
if (movement_radius > (radius_complete - radius_analog_stick)) {
|
|
||||||
// not pressed already, so ignore event from outer circle
|
|
||||||
if (!isPressed()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
movement_radius = radius_complete - radius_analog_stick;
|
|
||||||
}
|
|
||||||
|
|
||||||
// handle event depending on action
|
|
||||||
switch (event.getActionMasked()) {
|
|
||||||
// down event (touch event)
|
|
||||||
case MotionEvent.ACTION_DOWN:
|
|
||||||
case MotionEvent.ACTION_POINTER_DOWN: {
|
|
||||||
// set to dead zoned, will be corrected in update position if necessary
|
|
||||||
stick_state = STICK_STATE.MOVED_IN_DEAD_ZONE;
|
|
||||||
// check for double click
|
|
||||||
if (lastClickState == CLICK_STATE.SINGLE &&
|
|
||||||
timeLastClick + timeoutDoubleClick > System.currentTimeMillis()) {
|
|
||||||
click_state = CLICK_STATE.DOUBLE;
|
|
||||||
notifyOnDoubleClick();
|
|
||||||
} else {
|
|
||||||
click_state = CLICK_STATE.SINGLE;
|
|
||||||
notifyOnClick();
|
|
||||||
}
|
|
||||||
// reset last click timestamp
|
|
||||||
timeLastClick = System.currentTimeMillis();
|
|
||||||
// set item pressed and update
|
|
||||||
setPressed(true);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
// up event (revoke touch)
|
|
||||||
case MotionEvent.ACTION_UP:
|
|
||||||
case MotionEvent.ACTION_POINTER_UP: {
|
|
||||||
setPressed(false);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isPressed()) {
|
|
||||||
// when is pressed calculate new positions (will trigger movement if necessary)
|
|
||||||
updatePosition();
|
|
||||||
} else {
|
|
||||||
stick_state = STICK_STATE.NO_MOVEMENT;
|
|
||||||
notifyOnRevoke();
|
|
||||||
// not longer pressed reset analog stick
|
|
||||||
notifyOnMovement(0, 0);
|
|
||||||
}
|
|
||||||
// refresh view
|
|
||||||
invalidate();
|
|
||||||
// accept the touch event
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,243 +0,0 @@
|
|||||||
/**
|
|
||||||
* Created by Karim Mreisi.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package com.limelight.binding.input.virtual_controller;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.graphics.Canvas;
|
|
||||||
import android.graphics.Color;
|
|
||||||
import android.graphics.Paint;
|
|
||||||
import android.graphics.drawable.Drawable;
|
|
||||||
import android.view.MotionEvent;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Timer;
|
|
||||||
import java.util.TimerTask;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This is a digital button on screen element. It is used to get click and double click user input.
|
|
||||||
*/
|
|
||||||
public class DigitalButton extends VirtualControllerElement {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Listener interface to update registered observers.
|
|
||||||
*/
|
|
||||||
public interface DigitalButtonListener {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* onClick event will be fired on button click.
|
|
||||||
*/
|
|
||||||
void onClick();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* onLongClick event will be fired on button long click.
|
|
||||||
*/
|
|
||||||
void onLongClick();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* onRelease event will be fired on button unpress.
|
|
||||||
*/
|
|
||||||
void onRelease();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
private class TimerLongClickTimerTask extends TimerTask {
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
onLongClickCallback();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private List<DigitalButtonListener> listeners = new ArrayList<>();
|
|
||||||
private String text = "";
|
|
||||||
private int icon = -1;
|
|
||||||
private long timerLongClickTimeout = 3000;
|
|
||||||
private Timer timerLongClick = null;
|
|
||||||
private TimerLongClickTimerTask longClickTimerTask = null;
|
|
||||||
|
|
||||||
private final Paint paint = new Paint();
|
|
||||||
|
|
||||||
private int layer;
|
|
||||||
private DigitalButton movingButton = null;
|
|
||||||
|
|
||||||
boolean inRange(float x, float y) {
|
|
||||||
return (this.getX() < x && this.getX() + this.getWidth() > x) &&
|
|
||||||
(this.getY() < y && this.getY() + this.getHeight() > y);
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean checkMovement(float x, float y, DigitalButton movingButton) {
|
|
||||||
// check if the movement happened in the same layer
|
|
||||||
if (movingButton.layer != this.layer) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// save current pressed state
|
|
||||||
boolean wasPressed = isPressed();
|
|
||||||
|
|
||||||
// check if the movement directly happened on the button
|
|
||||||
if ((this.movingButton == null || movingButton == this.movingButton)
|
|
||||||
&& this.inRange(x, y)) {
|
|
||||||
// set button pressed state depending on moving button pressed state
|
|
||||||
if (this.isPressed() != movingButton.isPressed()) {
|
|
||||||
this.setPressed(movingButton.isPressed());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// check if the movement is outside of the range and the movement button
|
|
||||||
// is the saved moving button
|
|
||||||
else if (movingButton == this.movingButton) {
|
|
||||||
this.setPressed(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
// check if a change occurred
|
|
||||||
if (wasPressed != isPressed()) {
|
|
||||||
if (isPressed()) {
|
|
||||||
// is pressed set moving button and emit click event
|
|
||||||
this.movingButton = movingButton;
|
|
||||||
onClickCallback();
|
|
||||||
} else {
|
|
||||||
// no longer pressed reset moving button and emit release event
|
|
||||||
this.movingButton = null;
|
|
||||||
onReleaseCallback();
|
|
||||||
}
|
|
||||||
|
|
||||||
invalidate();
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void checkMovementForAllButtons(float x, float y) {
|
|
||||||
for (VirtualControllerElement element : virtualController.getElements()) {
|
|
||||||
if (element != this && element instanceof DigitalButton) {
|
|
||||||
((DigitalButton) element).checkMovement(x, y, this);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public DigitalButton(VirtualController controller, int layer, Context context) {
|
|
||||||
super(controller, context);
|
|
||||||
this.layer = layer;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void addDigitalButtonListener(DigitalButtonListener listener) {
|
|
||||||
listeners.add(listener);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setText(String text) {
|
|
||||||
this.text = text;
|
|
||||||
invalidate();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setIcon(int id) {
|
|
||||||
this.icon = id;
|
|
||||||
invalidate();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onElementDraw(Canvas canvas) {
|
|
||||||
// set transparent background
|
|
||||||
canvas.drawColor(Color.TRANSPARENT);
|
|
||||||
|
|
||||||
paint.setTextSize(getPercent(getWidth(), 30));
|
|
||||||
paint.setTextAlign(Paint.Align.CENTER);
|
|
||||||
paint.setStrokeWidth(getDefaultStrokeWidth());
|
|
||||||
|
|
||||||
paint.setColor(isPressed() ? pressedColor : getDefaultColor());
|
|
||||||
paint.setStyle(Paint.Style.STROKE);
|
|
||||||
canvas.drawRect(paint.getStrokeWidth(), paint.getStrokeWidth(),
|
|
||||||
getWidth() - paint.getStrokeWidth(), getHeight() - paint.getStrokeWidth(), paint);
|
|
||||||
|
|
||||||
if (icon != -1) {
|
|
||||||
Drawable d = getResources().getDrawable(icon);
|
|
||||||
d.setBounds(5, 5, getWidth() - 5, getHeight() - 5);
|
|
||||||
d.draw(canvas);
|
|
||||||
} else {
|
|
||||||
paint.setStyle(Paint.Style.FILL_AND_STROKE);
|
|
||||||
paint.setStrokeWidth(getDefaultStrokeWidth()/2);
|
|
||||||
canvas.drawText(text, getPercent(getWidth(), 50), getPercent(getHeight(), 63), paint);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void onClickCallback() {
|
|
||||||
_DBG("clicked");
|
|
||||||
// notify listeners
|
|
||||||
for (DigitalButtonListener listener : listeners) {
|
|
||||||
listener.onClick();
|
|
||||||
}
|
|
||||||
|
|
||||||
timerLongClick = new Timer();
|
|
||||||
longClickTimerTask = new TimerLongClickTimerTask();
|
|
||||||
timerLongClick.schedule(longClickTimerTask, timerLongClickTimeout);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void onLongClickCallback() {
|
|
||||||
_DBG("long click");
|
|
||||||
// notify listeners
|
|
||||||
for (DigitalButtonListener listener : listeners) {
|
|
||||||
listener.onLongClick();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void onReleaseCallback() {
|
|
||||||
_DBG("released");
|
|
||||||
// notify listeners
|
|
||||||
for (DigitalButtonListener listener : listeners) {
|
|
||||||
listener.onRelease();
|
|
||||||
}
|
|
||||||
|
|
||||||
// We may be called for a release without a prior click
|
|
||||||
if (timerLongClick != null) {
|
|
||||||
timerLongClick.cancel();
|
|
||||||
}
|
|
||||||
if (longClickTimerTask != null) {
|
|
||||||
longClickTimerTask.cancel();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean onElementTouchEvent(MotionEvent event) {
|
|
||||||
// get masked (not specific to a pointer) action
|
|
||||||
float x = getX() + event.getX();
|
|
||||||
float y = getY() + event.getY();
|
|
||||||
int action = event.getActionMasked();
|
|
||||||
|
|
||||||
switch (action) {
|
|
||||||
case MotionEvent.ACTION_DOWN:
|
|
||||||
case MotionEvent.ACTION_POINTER_DOWN: {
|
|
||||||
movingButton = null;
|
|
||||||
setPressed(true);
|
|
||||||
onClickCallback();
|
|
||||||
|
|
||||||
invalidate();
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
case MotionEvent.ACTION_MOVE: {
|
|
||||||
checkMovementForAllButtons(x, y);
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
case MotionEvent.ACTION_CANCEL:
|
|
||||||
case MotionEvent.ACTION_UP:
|
|
||||||
case MotionEvent.ACTION_POINTER_UP: {
|
|
||||||
setPressed(false);
|
|
||||||
onReleaseCallback();
|
|
||||||
|
|
||||||
checkMovementForAllButtons(x, y);
|
|
||||||
|
|
||||||
invalidate();
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
default: {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,205 +0,0 @@
|
|||||||
/**
|
|
||||||
* Created by Karim Mreisi.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package com.limelight.binding.input.virtual_controller;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.graphics.Canvas;
|
|
||||||
import android.graphics.Color;
|
|
||||||
import android.graphics.Paint;
|
|
||||||
import android.view.MotionEvent;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
public class DigitalPad extends VirtualControllerElement {
|
|
||||||
public final static int DIGITAL_PAD_DIRECTION_NO_DIRECTION = 0;
|
|
||||||
int direction = DIGITAL_PAD_DIRECTION_NO_DIRECTION;
|
|
||||||
public final static int DIGITAL_PAD_DIRECTION_LEFT = 1;
|
|
||||||
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<>();
|
|
||||||
|
|
||||||
private static final int DPAD_MARGIN = 5;
|
|
||||||
|
|
||||||
private final Paint paint = new Paint();
|
|
||||||
|
|
||||||
public DigitalPad(VirtualController controller, Context context) {
|
|
||||||
super(controller, context);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void addDigitalPadListener(DigitalPadListener listener) {
|
|
||||||
listeners.add(listener);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onElementDraw(Canvas canvas) {
|
|
||||||
// set transparent background
|
|
||||||
canvas.drawColor(Color.TRANSPARENT);
|
|
||||||
|
|
||||||
paint.setTextSize(getPercent(getCorrectWidth(), 20));
|
|
||||||
paint.setTextAlign(Paint.Align.CENTER);
|
|
||||||
paint.setStrokeWidth(getDefaultStrokeWidth());
|
|
||||||
|
|
||||||
if (direction == DIGITAL_PAD_DIRECTION_NO_DIRECTION) {
|
|
||||||
// draw no direction rect
|
|
||||||
paint.setStyle(Paint.Style.STROKE);
|
|
||||||
paint.setColor(getDefaultColor());
|
|
||||||
canvas.drawRect(
|
|
||||||
getPercent(getWidth(), 36), getPercent(getHeight(), 36),
|
|
||||||
getPercent(getWidth(), 63), getPercent(getHeight(), 63),
|
|
||||||
paint
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// draw left rect
|
|
||||||
paint.setColor(
|
|
||||||
(direction & DIGITAL_PAD_DIRECTION_LEFT) > 0 ? pressedColor : getDefaultColor());
|
|
||||||
paint.setStyle(Paint.Style.STROKE);
|
|
||||||
canvas.drawRect(
|
|
||||||
paint.getStrokeWidth()+DPAD_MARGIN, getPercent(getHeight(), 33),
|
|
||||||
getPercent(getWidth(), 33), getPercent(getHeight(), 66),
|
|
||||||
paint
|
|
||||||
);
|
|
||||||
|
|
||||||
|
|
||||||
// draw up rect
|
|
||||||
paint.setColor(
|
|
||||||
(direction & DIGITAL_PAD_DIRECTION_UP) > 0 ? pressedColor : getDefaultColor());
|
|
||||||
paint.setStyle(Paint.Style.STROKE);
|
|
||||||
canvas.drawRect(
|
|
||||||
getPercent(getWidth(), 33), paint.getStrokeWidth()+DPAD_MARGIN,
|
|
||||||
getPercent(getWidth(), 66), getPercent(getHeight(), 33),
|
|
||||||
paint
|
|
||||||
);
|
|
||||||
|
|
||||||
// draw right rect
|
|
||||||
paint.setColor(
|
|
||||||
(direction & DIGITAL_PAD_DIRECTION_RIGHT) > 0 ? pressedColor : getDefaultColor());
|
|
||||||
paint.setStyle(Paint.Style.STROKE);
|
|
||||||
canvas.drawRect(
|
|
||||||
getPercent(getWidth(), 66), getPercent(getHeight(), 33),
|
|
||||||
getWidth() - (paint.getStrokeWidth()+DPAD_MARGIN), getPercent(getHeight(), 66),
|
|
||||||
paint
|
|
||||||
);
|
|
||||||
|
|
||||||
// draw down rect
|
|
||||||
paint.setColor(
|
|
||||||
(direction & DIGITAL_PAD_DIRECTION_DOWN) > 0 ? pressedColor : getDefaultColor());
|
|
||||||
paint.setStyle(Paint.Style.STROKE);
|
|
||||||
canvas.drawRect(
|
|
||||||
getPercent(getWidth(), 33), getPercent(getHeight(), 66),
|
|
||||||
getPercent(getWidth(), 66), getHeight() - (paint.getStrokeWidth()+DPAD_MARGIN),
|
|
||||||
paint
|
|
||||||
);
|
|
||||||
|
|
||||||
// draw left up line
|
|
||||||
paint.setColor((
|
|
||||||
(direction & DIGITAL_PAD_DIRECTION_LEFT) > 0 &&
|
|
||||||
(direction & DIGITAL_PAD_DIRECTION_UP) > 0
|
|
||||||
) ? pressedColor : getDefaultColor()
|
|
||||||
);
|
|
||||||
paint.setStyle(Paint.Style.STROKE);
|
|
||||||
canvas.drawLine(
|
|
||||||
paint.getStrokeWidth()+DPAD_MARGIN, getPercent(getHeight(), 33),
|
|
||||||
getPercent(getWidth(), 33), paint.getStrokeWidth()+DPAD_MARGIN,
|
|
||||||
paint
|
|
||||||
);
|
|
||||||
|
|
||||||
// draw up right line
|
|
||||||
paint.setColor((
|
|
||||||
(direction & DIGITAL_PAD_DIRECTION_UP) > 0 &&
|
|
||||||
(direction & DIGITAL_PAD_DIRECTION_RIGHT) > 0
|
|
||||||
) ? pressedColor : getDefaultColor()
|
|
||||||
);
|
|
||||||
paint.setStyle(Paint.Style.STROKE);
|
|
||||||
canvas.drawLine(
|
|
||||||
getPercent(getWidth(), 66), paint.getStrokeWidth()+DPAD_MARGIN,
|
|
||||||
getWidth() - (paint.getStrokeWidth()+DPAD_MARGIN), getPercent(getHeight(), 33),
|
|
||||||
paint
|
|
||||||
);
|
|
||||||
|
|
||||||
// draw right down line
|
|
||||||
paint.setColor((
|
|
||||||
(direction & DIGITAL_PAD_DIRECTION_RIGHT) > 0 &&
|
|
||||||
(direction & DIGITAL_PAD_DIRECTION_DOWN) > 0
|
|
||||||
) ? pressedColor : getDefaultColor()
|
|
||||||
);
|
|
||||||
paint.setStyle(Paint.Style.STROKE);
|
|
||||||
canvas.drawLine(
|
|
||||||
getWidth()-paint.getStrokeWidth(), getPercent(getHeight(), 66),
|
|
||||||
getPercent(getWidth(), 66), getHeight()-(paint.getStrokeWidth()+DPAD_MARGIN),
|
|
||||||
paint
|
|
||||||
);
|
|
||||||
|
|
||||||
// draw down left line
|
|
||||||
paint.setColor((
|
|
||||||
(direction & DIGITAL_PAD_DIRECTION_DOWN) > 0 &&
|
|
||||||
(direction & DIGITAL_PAD_DIRECTION_LEFT) > 0
|
|
||||||
) ? pressedColor : getDefaultColor()
|
|
||||||
);
|
|
||||||
paint.setStyle(Paint.Style.STROKE);
|
|
||||||
canvas.drawLine(
|
|
||||||
getPercent(getWidth(), 33), getHeight()-(paint.getStrokeWidth()+DPAD_MARGIN),
|
|
||||||
paint.getStrokeWidth()+DPAD_MARGIN, getPercent(getHeight(), 66),
|
|
||||||
paint
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void newDirectionCallback(int direction) {
|
|
||||||
_DBG("direction: " + direction);
|
|
||||||
|
|
||||||
// notify listeners
|
|
||||||
for (DigitalPadListener listener : listeners) {
|
|
||||||
listener.onDirectionChange(direction);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean onElementTouchEvent(MotionEvent event) {
|
|
||||||
// get masked (not specific to a pointer) action
|
|
||||||
switch (event.getActionMasked()) {
|
|
||||||
case MotionEvent.ACTION_DOWN:
|
|
||||||
case MotionEvent.ACTION_POINTER_DOWN:
|
|
||||||
case MotionEvent.ACTION_MOVE: {
|
|
||||||
direction = 0;
|
|
||||||
|
|
||||||
if (event.getX() < getPercent(getWidth(), 33)) {
|
|
||||||
direction |= DIGITAL_PAD_DIRECTION_LEFT;
|
|
||||||
}
|
|
||||||
if (event.getX() > getPercent(getWidth(), 66)) {
|
|
||||||
direction |= DIGITAL_PAD_DIRECTION_RIGHT;
|
|
||||||
}
|
|
||||||
if (event.getY() > getPercent(getHeight(), 66)) {
|
|
||||||
direction |= DIGITAL_PAD_DIRECTION_DOWN;
|
|
||||||
}
|
|
||||||
if (event.getY() < getPercent(getHeight(), 33)) {
|
|
||||||
direction |= DIGITAL_PAD_DIRECTION_UP;
|
|
||||||
}
|
|
||||||
newDirectionCallback(direction);
|
|
||||||
invalidate();
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
case MotionEvent.ACTION_CANCEL:
|
|
||||||
case MotionEvent.ACTION_UP:
|
|
||||||
case MotionEvent.ACTION_POINTER_UP: {
|
|
||||||
direction = 0;
|
|
||||||
newDirectionCallback(direction);
|
|
||||||
invalidate();
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
default: {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public interface DigitalPadListener {
|
|
||||||
void onDirectionChange(int direction);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
/**
|
|
||||||
* Created by Karim Mreisi.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package com.limelight.binding.input.virtual_controller;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
|
|
||||||
import com.limelight.nvstream.input.ControllerPacket;
|
|
||||||
|
|
||||||
public class LeftAnalogStick extends AnalogStick {
|
|
||||||
public LeftAnalogStick(final VirtualController controller, final Context context) {
|
|
||||||
super(controller, context);
|
|
||||||
|
|
||||||
addAnalogStickListener(new AnalogStick.AnalogStickListener() {
|
|
||||||
@Override
|
|
||||||
public void onMovement(float x, float y) {
|
|
||||||
VirtualController.ControllerInputContext inputContext =
|
|
||||||
controller.getControllerInputContext();
|
|
||||||
inputContext.leftStickX = (short) (x * 0x7FFE);
|
|
||||||
inputContext.leftStickY = (short) (y * 0x7FFE);
|
|
||||||
|
|
||||||
controller.sendControllerInputContext();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onClick() {
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onDoubleClick() {
|
|
||||||
VirtualController.ControllerInputContext inputContext =
|
|
||||||
controller.getControllerInputContext();
|
|
||||||
inputContext.inputMap |= ControllerPacket.LS_CLK_FLAG;
|
|
||||||
|
|
||||||
controller.sendControllerInputContext();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onRevoke() {
|
|
||||||
VirtualController.ControllerInputContext inputContext =
|
|
||||||
controller.getControllerInputContext();
|
|
||||||
inputContext.inputMap &= ~ControllerPacket.LS_CLK_FLAG;
|
|
||||||
|
|
||||||
controller.sendControllerInputContext();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
/**
|
|
||||||
* Created by Karim Mreisi.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package com.limelight.binding.input.virtual_controller;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
|
|
||||||
public class LeftTrigger extends DigitalButton {
|
|
||||||
public LeftTrigger(final VirtualController controller, final int layer, final Context context) {
|
|
||||||
super(controller, layer, context);
|
|
||||||
addDigitalButtonListener(new DigitalButton.DigitalButtonListener() {
|
|
||||||
@Override
|
|
||||||
public void onClick() {
|
|
||||||
VirtualController.ControllerInputContext inputContext =
|
|
||||||
controller.getControllerInputContext();
|
|
||||||
inputContext.leftTrigger = (byte) 0xFF;
|
|
||||||
|
|
||||||
controller.sendControllerInputContext();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onLongClick() {
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onRelease() {
|
|
||||||
VirtualController.ControllerInputContext inputContext =
|
|
||||||
controller.getControllerInputContext();
|
|
||||||
inputContext.leftTrigger = (byte) 0x00;
|
|
||||||
|
|
||||||
controller.sendControllerInputContext();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-49
@@ -1,49 +0,0 @@
|
|||||||
/**
|
|
||||||
* Created by Karim Mreisi.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package com.limelight.binding.input.virtual_controller;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
|
|
||||||
import com.limelight.nvstream.input.ControllerPacket;
|
|
||||||
|
|
||||||
public class RightAnalogStick extends AnalogStick {
|
|
||||||
public RightAnalogStick(final VirtualController controller, final Context context) {
|
|
||||||
super(controller, context);
|
|
||||||
|
|
||||||
addAnalogStickListener(new AnalogStick.AnalogStickListener() {
|
|
||||||
@Override
|
|
||||||
public void onMovement(float x, float y) {
|
|
||||||
VirtualController.ControllerInputContext inputContext =
|
|
||||||
controller.getControllerInputContext();
|
|
||||||
inputContext.rightStickX = (short) (x * 0x7FFE);
|
|
||||||
inputContext.rightStickY = (short) (y * 0x7FFE);
|
|
||||||
|
|
||||||
controller.sendControllerInputContext();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onClick() {
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onDoubleClick() {
|
|
||||||
VirtualController.ControllerInputContext inputContext =
|
|
||||||
controller.getControllerInputContext();
|
|
||||||
inputContext.inputMap |= ControllerPacket.RS_CLK_FLAG;
|
|
||||||
|
|
||||||
controller.sendControllerInputContext();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onRevoke() {
|
|
||||||
VirtualController.ControllerInputContext inputContext =
|
|
||||||
controller.getControllerInputContext();
|
|
||||||
inputContext.inputMap &= ~ControllerPacket.RS_CLK_FLAG;
|
|
||||||
|
|
||||||
controller.sendControllerInputContext();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
/**
|
|
||||||
* Created by Karim Mreisi.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package com.limelight.binding.input.virtual_controller;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
|
|
||||||
public class RightTrigger extends DigitalButton {
|
|
||||||
public RightTrigger(final VirtualController controller, final int layer, final Context context) {
|
|
||||||
super(controller, layer, context);
|
|
||||||
addDigitalButtonListener(new DigitalButton.DigitalButtonListener() {
|
|
||||||
@Override
|
|
||||||
public void onClick() {
|
|
||||||
VirtualController.ControllerInputContext inputContext =
|
|
||||||
controller.getControllerInputContext();
|
|
||||||
inputContext.rightTrigger = (byte) 0xFF;
|
|
||||||
|
|
||||||
controller.sendControllerInputContext();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onLongClick() {
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onRelease() {
|
|
||||||
VirtualController.ControllerInputContext inputContext =
|
|
||||||
controller.getControllerInputContext();
|
|
||||||
inputContext.rightTrigger = (byte) 0x00;
|
|
||||||
|
|
||||||
controller.sendControllerInputContext();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-162
@@ -1,162 +0,0 @@
|
|||||||
/**
|
|
||||||
* Created by Karim Mreisi.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package com.limelight.binding.input.virtual_controller;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.util.DisplayMetrics;
|
|
||||||
import android.view.View;
|
|
||||||
import android.widget.Button;
|
|
||||||
import android.widget.FrameLayout;
|
|
||||||
import android.widget.RelativeLayout;
|
|
||||||
import android.widget.Toast;
|
|
||||||
|
|
||||||
import com.limelight.R;
|
|
||||||
import com.limelight.nvstream.NvConnection;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
public class VirtualController {
|
|
||||||
public class ControllerInputContext {
|
|
||||||
public short inputMap = 0x0000;
|
|
||||||
public byte leftTrigger = 0x00;
|
|
||||||
public byte rightTrigger = 0x00;
|
|
||||||
public short rightStickX = 0x0000;
|
|
||||||
public short rightStickY = 0x0000;
|
|
||||||
public short leftStickX = 0x0000;
|
|
||||||
public short leftStickY = 0x0000;
|
|
||||||
}
|
|
||||||
|
|
||||||
public enum ControllerMode {
|
|
||||||
Active,
|
|
||||||
Configuration
|
|
||||||
}
|
|
||||||
|
|
||||||
private static final boolean _PRINT_DEBUG_INFORMATION = false;
|
|
||||||
|
|
||||||
private NvConnection connection = null;
|
|
||||||
private Context context = null;
|
|
||||||
|
|
||||||
private FrameLayout frame_layout = null;
|
|
||||||
private RelativeLayout relative_layout = null;
|
|
||||||
|
|
||||||
ControllerMode currentMode = ControllerMode.Active;
|
|
||||||
ControllerInputContext inputContext = new ControllerInputContext();
|
|
||||||
|
|
||||||
private RelativeLayout.LayoutParams layoutParamsButtonConfigure = null;
|
|
||||||
private Button buttonConfigure = null;
|
|
||||||
|
|
||||||
private List<VirtualControllerElement> elements = new ArrayList<>();
|
|
||||||
|
|
||||||
public VirtualController(final NvConnection conn, FrameLayout layout, final Context context) {
|
|
||||||
this.connection = conn;
|
|
||||||
this.frame_layout = layout;
|
|
||||||
this.context = context;
|
|
||||||
|
|
||||||
relative_layout = new RelativeLayout(context);
|
|
||||||
|
|
||||||
frame_layout.addView(relative_layout);
|
|
||||||
|
|
||||||
buttonConfigure = new Button(context);
|
|
||||||
buttonConfigure.setBackgroundResource(R.drawable.ic_settings);
|
|
||||||
buttonConfigure.setOnClickListener(new View.OnClickListener() {
|
|
||||||
@Override
|
|
||||||
public void onClick(View v) {
|
|
||||||
String message;
|
|
||||||
|
|
||||||
if (currentMode == ControllerMode.Configuration) {
|
|
||||||
currentMode = ControllerMode.Active;
|
|
||||||
message = "Exiting configuration mode";
|
|
||||||
} else {
|
|
||||||
currentMode = ControllerMode.Configuration;
|
|
||||||
message = "Entering configuration mode";
|
|
||||||
}
|
|
||||||
|
|
||||||
Toast.makeText(context, message, Toast.LENGTH_SHORT).show();
|
|
||||||
|
|
||||||
relative_layout.invalidate();
|
|
||||||
|
|
||||||
for (VirtualControllerElement element : elements) {
|
|
||||||
element.invalidate();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public void removeElements() {
|
|
||||||
for (VirtualControllerElement element : elements) {
|
|
||||||
relative_layout.removeView(element);
|
|
||||||
}
|
|
||||||
elements.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void addElement(VirtualControllerElement element, int x, int y, int width, int height) {
|
|
||||||
elements.add(element);
|
|
||||||
RelativeLayout.LayoutParams layoutParams = new RelativeLayout.LayoutParams(width, height);
|
|
||||||
layoutParams.setMargins(x, y, 0, 0);
|
|
||||||
|
|
||||||
relative_layout.addView(element, layoutParams);
|
|
||||||
}
|
|
||||||
|
|
||||||
public List<VirtualControllerElement> getElements() {
|
|
||||||
return elements;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static final void _DBG(String text) {
|
|
||||||
if (_PRINT_DEBUG_INFORMATION) {
|
|
||||||
System.out.println("VirtualController: " + text);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void refreshLayout() {
|
|
||||||
relative_layout.removeAllViews();
|
|
||||||
removeElements();
|
|
||||||
|
|
||||||
DisplayMetrics screen = context.getResources().getDisplayMetrics();
|
|
||||||
|
|
||||||
int buttonSize = (int)(screen.heightPixels*0.05f);
|
|
||||||
layoutParamsButtonConfigure = new RelativeLayout.LayoutParams(buttonSize, buttonSize);
|
|
||||||
relative_layout.addView(buttonConfigure, layoutParamsButtonConfigure);
|
|
||||||
|
|
||||||
VirtualControllerConfigurationLoader.createDefaultLayout(this, context);
|
|
||||||
}
|
|
||||||
|
|
||||||
public ControllerMode getControllerMode() {
|
|
||||||
return currentMode;
|
|
||||||
}
|
|
||||||
|
|
||||||
public ControllerInputContext getControllerInputContext() {
|
|
||||||
return inputContext;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void sendControllerInputContext() {
|
|
||||||
sendControllerInputPacket();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void sendControllerInputPacket() {
|
|
||||||
try {
|
|
||||||
_DBG("INPUT_MAP + " + inputContext.inputMap);
|
|
||||||
_DBG("LEFT_TRIGGER " + inputContext.leftTrigger);
|
|
||||||
_DBG("RIGHT_TRIGGER " + inputContext.rightTrigger);
|
|
||||||
_DBG("LEFT STICK X: " + inputContext.leftStickX + " Y: " + inputContext.leftStickY);
|
|
||||||
_DBG("RIGHT STICK X: " + inputContext.rightStickX + " Y: " + inputContext.rightStickY);
|
|
||||||
_DBG("RIGHT STICK X: " + inputContext.rightStickX + " Y: " + inputContext.rightStickY);
|
|
||||||
|
|
||||||
if (connection != null) {
|
|
||||||
connection.sendControllerInput(
|
|
||||||
inputContext.inputMap,
|
|
||||||
inputContext.leftTrigger,
|
|
||||||
inputContext.rightTrigger,
|
|
||||||
inputContext.leftStickX,
|
|
||||||
inputContext.leftStickY,
|
|
||||||
inputContext.rightStickX,
|
|
||||||
inputContext.rightStickY
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-285
@@ -1,285 +0,0 @@
|
|||||||
/**
|
|
||||||
* Created by Karim Mreisi.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package com.limelight.binding.input.virtual_controller;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.util.DisplayMetrics;
|
|
||||||
|
|
||||||
import com.limelight.nvstream.input.ControllerPacket;
|
|
||||||
|
|
||||||
public class VirtualControllerConfigurationLoader {
|
|
||||||
private static final String PROFILE_PATH = "profiles";
|
|
||||||
|
|
||||||
private static int getPercent(
|
|
||||||
int percent,
|
|
||||||
int total) {
|
|
||||||
return (int) (((float) total / (float) 100) * (float) percent);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static DigitalPad createDigitalPad(
|
|
||||||
final VirtualController controller,
|
|
||||||
final Context context) {
|
|
||||||
|
|
||||||
DigitalPad digitalPad = new DigitalPad(controller, context);
|
|
||||||
digitalPad.addDigitalPadListener(new DigitalPad.DigitalPadListener() {
|
|
||||||
@Override
|
|
||||||
public void onDirectionChange(int direction) {
|
|
||||||
VirtualController.ControllerInputContext inputContext =
|
|
||||||
controller.getControllerInputContext();
|
|
||||||
|
|
||||||
if (direction == DigitalPad.DIGITAL_PAD_DIRECTION_NO_DIRECTION) {
|
|
||||||
inputContext.inputMap &= ~ControllerPacket.LEFT_FLAG;
|
|
||||||
inputContext.inputMap &= ~ControllerPacket.RIGHT_FLAG;
|
|
||||||
inputContext.inputMap &= ~ControllerPacket.UP_FLAG;
|
|
||||||
inputContext.inputMap &= ~ControllerPacket.DOWN_FLAG;
|
|
||||||
|
|
||||||
controller.sendControllerInputContext();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if ((direction & DigitalPad.DIGITAL_PAD_DIRECTION_LEFT) > 0) {
|
|
||||||
inputContext.inputMap |= ControllerPacket.LEFT_FLAG;
|
|
||||||
}
|
|
||||||
if ((direction & DigitalPad.DIGITAL_PAD_DIRECTION_RIGHT) > 0) {
|
|
||||||
inputContext.inputMap |= ControllerPacket.RIGHT_FLAG;
|
|
||||||
}
|
|
||||||
if ((direction & DigitalPad.DIGITAL_PAD_DIRECTION_UP) > 0) {
|
|
||||||
inputContext.inputMap |= ControllerPacket.UP_FLAG;
|
|
||||||
}
|
|
||||||
if ((direction & DigitalPad.DIGITAL_PAD_DIRECTION_DOWN) > 0) {
|
|
||||||
inputContext.inputMap |= ControllerPacket.DOWN_FLAG;
|
|
||||||
}
|
|
||||||
controller.sendControllerInputContext();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return digitalPad;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static DigitalButton createDigitalButton(
|
|
||||||
final int keyShort,
|
|
||||||
final int keyLong,
|
|
||||||
final int layer,
|
|
||||||
final String text,
|
|
||||||
final int icon,
|
|
||||||
final VirtualController controller,
|
|
||||||
final Context context) {
|
|
||||||
DigitalButton button = new DigitalButton(controller, layer, context);
|
|
||||||
button.setText(text);
|
|
||||||
button.setIcon(icon);
|
|
||||||
|
|
||||||
button.addDigitalButtonListener(new DigitalButton.DigitalButtonListener() {
|
|
||||||
@Override
|
|
||||||
public void onClick() {
|
|
||||||
VirtualController.ControllerInputContext inputContext =
|
|
||||||
controller.getControllerInputContext();
|
|
||||||
inputContext.inputMap |= keyShort;
|
|
||||||
|
|
||||||
controller.sendControllerInputContext();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onLongClick() {
|
|
||||||
VirtualController.ControllerInputContext inputContext =
|
|
||||||
controller.getControllerInputContext();
|
|
||||||
inputContext.inputMap |= keyLong;
|
|
||||||
|
|
||||||
controller.sendControllerInputContext();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onRelease() {
|
|
||||||
VirtualController.ControllerInputContext inputContext =
|
|
||||||
controller.getControllerInputContext();
|
|
||||||
inputContext.inputMap &= ~keyShort;
|
|
||||||
inputContext.inputMap &= ~keyLong;
|
|
||||||
|
|
||||||
controller.sendControllerInputContext();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return button;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static DigitalButton createLeftTrigger(
|
|
||||||
final int layer,
|
|
||||||
final String text,
|
|
||||||
final int icon,
|
|
||||||
final VirtualController controller,
|
|
||||||
final Context context) {
|
|
||||||
LeftTrigger button = new LeftTrigger(controller, layer, context);
|
|
||||||
button.setText(text);
|
|
||||||
button.setIcon(icon);
|
|
||||||
return button;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static DigitalButton createRightTrigger(
|
|
||||||
final int layer,
|
|
||||||
final String text,
|
|
||||||
final int icon,
|
|
||||||
final VirtualController controller,
|
|
||||||
final Context context) {
|
|
||||||
RightTrigger button = new RightTrigger(controller, layer, context);
|
|
||||||
button.setText(text);
|
|
||||||
button.setIcon(icon);
|
|
||||||
return button;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static AnalogStick createLeftStick(
|
|
||||||
final VirtualController controller,
|
|
||||||
final Context context) {
|
|
||||||
return new LeftAnalogStick(controller, context);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static AnalogStick createRightStick(
|
|
||||||
final VirtualController controller,
|
|
||||||
final Context context) {
|
|
||||||
return new RightAnalogStick(controller, context);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static final int BUTTON_BASE_X = 65;
|
|
||||||
private static final int BUTTON_BASE_Y = 5;
|
|
||||||
private static final int BUTTON_WIDTH = getPercent(30, 33);
|
|
||||||
private static final int BUTTON_HEIGHT = getPercent(40, 33);
|
|
||||||
|
|
||||||
public static void createDefaultLayout(final VirtualController controller, final Context context) {
|
|
||||||
|
|
||||||
DisplayMetrics screen = context.getResources().getDisplayMetrics();
|
|
||||||
|
|
||||||
// NOTE: Some of these getPercent() expressions seem like they can be combined
|
|
||||||
// into a single call. Due to floating point rounding, this isn't actually possible.
|
|
||||||
|
|
||||||
controller.addElement(createDigitalPad(controller, context),
|
|
||||||
getPercent(5, screen.widthPixels),
|
|
||||||
getPercent(BUTTON_BASE_Y, screen.heightPixels),
|
|
||||||
getPercent(30, screen.widthPixels),
|
|
||||||
getPercent(40, screen.heightPixels)
|
|
||||||
);
|
|
||||||
|
|
||||||
controller.addElement(createDigitalButton(
|
|
||||||
ControllerPacket.A_FLAG, 0, 1, "A", -1, controller, context),
|
|
||||||
getPercent(BUTTON_BASE_X, screen.widthPixels)+getPercent(BUTTON_WIDTH, screen.widthPixels),
|
|
||||||
getPercent(BUTTON_BASE_Y, screen.heightPixels)+2*getPercent(BUTTON_HEIGHT, screen.heightPixels),
|
|
||||||
getPercent(BUTTON_WIDTH, screen.widthPixels),
|
|
||||||
getPercent(BUTTON_HEIGHT, screen.heightPixels)
|
|
||||||
);
|
|
||||||
|
|
||||||
controller.addElement(createDigitalButton(
|
|
||||||
ControllerPacket.B_FLAG, 0, 1, "B", -1, controller, context),
|
|
||||||
getPercent(BUTTON_BASE_X, screen.widthPixels)+2*getPercent(BUTTON_WIDTH, screen.widthPixels),
|
|
||||||
getPercent(BUTTON_BASE_Y, screen.heightPixels)+getPercent(BUTTON_HEIGHT, screen.heightPixels),
|
|
||||||
getPercent(BUTTON_WIDTH, screen.widthPixels),
|
|
||||||
getPercent(BUTTON_HEIGHT, screen.heightPixels)
|
|
||||||
);
|
|
||||||
|
|
||||||
controller.addElement(createDigitalButton(
|
|
||||||
ControllerPacket.X_FLAG, 0, 1, "X", -1, controller, context),
|
|
||||||
getPercent(BUTTON_BASE_X, screen.widthPixels),
|
|
||||||
getPercent(BUTTON_BASE_Y, screen.heightPixels)+getPercent(BUTTON_HEIGHT, screen.heightPixels),
|
|
||||||
getPercent(BUTTON_WIDTH, screen.widthPixels),
|
|
||||||
getPercent(BUTTON_HEIGHT, screen.heightPixels)
|
|
||||||
);
|
|
||||||
|
|
||||||
controller.addElement(createDigitalButton(
|
|
||||||
ControllerPacket.Y_FLAG, 0, 1, "Y", -1, controller, context),
|
|
||||||
getPercent(BUTTON_BASE_X, screen.widthPixels)+getPercent(BUTTON_WIDTH, screen.widthPixels),
|
|
||||||
getPercent(BUTTON_BASE_Y, screen.heightPixels),
|
|
||||||
getPercent(BUTTON_WIDTH, screen.widthPixels),
|
|
||||||
getPercent(BUTTON_HEIGHT, screen.heightPixels)
|
|
||||||
);
|
|
||||||
|
|
||||||
controller.addElement(createLeftTrigger(
|
|
||||||
0, "LT", -1, controller, context),
|
|
||||||
getPercent(BUTTON_BASE_X, screen.widthPixels),
|
|
||||||
getPercent(BUTTON_BASE_Y, screen.heightPixels),
|
|
||||||
getPercent(BUTTON_WIDTH, screen.widthPixels),
|
|
||||||
getPercent(BUTTON_HEIGHT, screen.heightPixels)
|
|
||||||
);
|
|
||||||
|
|
||||||
controller.addElement(createRightTrigger(
|
|
||||||
0, "RT", -1, controller, context),
|
|
||||||
getPercent(BUTTON_BASE_X, screen.widthPixels)+2*getPercent(BUTTON_WIDTH, screen.widthPixels),
|
|
||||||
getPercent(BUTTON_BASE_Y, screen.heightPixels),
|
|
||||||
getPercent(BUTTON_WIDTH, screen.widthPixels),
|
|
||||||
getPercent(BUTTON_HEIGHT, screen.heightPixels)
|
|
||||||
);
|
|
||||||
|
|
||||||
controller.addElement(createDigitalButton(
|
|
||||||
ControllerPacket.LB_FLAG, 0, 1, "LB", -1, controller, context),
|
|
||||||
getPercent(BUTTON_BASE_X, screen.widthPixels),
|
|
||||||
getPercent(BUTTON_BASE_Y, screen.heightPixels)+2*getPercent(BUTTON_HEIGHT, screen.heightPixels),
|
|
||||||
getPercent(BUTTON_WIDTH, screen.widthPixels),
|
|
||||||
getPercent(BUTTON_HEIGHT, screen.heightPixels)
|
|
||||||
);
|
|
||||||
|
|
||||||
controller.addElement(createDigitalButton(
|
|
||||||
ControllerPacket.RB_FLAG, 0, 1, "RB", -1, controller, context),
|
|
||||||
getPercent(BUTTON_BASE_X, screen.widthPixels)+2*getPercent(BUTTON_WIDTH, screen.widthPixels),
|
|
||||||
getPercent(BUTTON_BASE_Y, screen.heightPixels)+2*getPercent(BUTTON_HEIGHT, screen.heightPixels),
|
|
||||||
getPercent(BUTTON_WIDTH, screen.widthPixels),
|
|
||||||
getPercent(BUTTON_HEIGHT, screen.heightPixels)
|
|
||||||
);
|
|
||||||
|
|
||||||
controller.addElement(createLeftStick(controller, context),
|
|
||||||
getPercent(5, screen.widthPixels),
|
|
||||||
getPercent(50, screen.heightPixels),
|
|
||||||
getPercent(40, screen.widthPixels),
|
|
||||||
getPercent(50, screen.heightPixels)
|
|
||||||
);
|
|
||||||
|
|
||||||
controller.addElement(createRightStick(controller, context),
|
|
||||||
getPercent(55, screen.widthPixels),
|
|
||||||
getPercent(50, screen.heightPixels),
|
|
||||||
getPercent(40, screen.widthPixels),
|
|
||||||
getPercent(50, screen.heightPixels)
|
|
||||||
);
|
|
||||||
|
|
||||||
controller.addElement(createDigitalButton(
|
|
||||||
ControllerPacket.BACK_FLAG, 0, 2, "BACK", -1, controller, context),
|
|
||||||
getPercent(40, screen.widthPixels),
|
|
||||||
getPercent(90, screen.heightPixels),
|
|
||||||
getPercent(10, screen.widthPixels),
|
|
||||||
getPercent(10, screen.heightPixels)
|
|
||||||
);
|
|
||||||
|
|
||||||
controller.addElement(createDigitalButton(
|
|
||||||
ControllerPacket.PLAY_FLAG, 0, 3, "START", -1, controller, context),
|
|
||||||
getPercent(40, screen.widthPixels)+getPercent(10, screen.widthPixels),
|
|
||||||
getPercent(90, screen.heightPixels),
|
|
||||||
getPercent(10, screen.widthPixels),
|
|
||||||
getPercent(10, screen.heightPixels)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
NOT IMPLEMENTED YET,
|
|
||||||
this should later be used to store and load a profile for the virtual controller
|
|
||||||
public static void saveProfile(final String name,
|
|
||||||
final VirtualController controller,
|
|
||||||
final Context context) {
|
|
||||||
|
|
||||||
SharedPreferences preferences = context.getSharedPreferences(PROFILE_PATH + "/" +
|
|
||||||
name, Activity.MODE_PRIVATE);
|
|
||||||
|
|
||||||
JSONArray elementConfigurations = new JSONArray();
|
|
||||||
for (VirtualControllerElement element : controller.getElements()) {
|
|
||||||
JSONObject elementConfiguration = new JSONObject();
|
|
||||||
try {
|
|
||||||
elementConfiguration.put("TYPE", element.getClass().getName());
|
|
||||||
elementConfiguration.put("CONFIGURATION", element.getConfiguration());
|
|
||||||
elementConfigurations.put(elementConfiguration);
|
|
||||||
} catch (Exception e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
SharedPreferences.Editor editor= preferences.edit();
|
|
||||||
editor.putString("ELEMENTS", elementConfigurations.toString());
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void loadFromPreferences(final VirtualController controller) {
|
|
||||||
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
}
|
|
||||||
-290
@@ -1,290 +0,0 @@
|
|||||||
/**
|
|
||||||
* Created by Karim Mreisi.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package com.limelight.binding.input.virtual_controller;
|
|
||||||
|
|
||||||
import android.app.AlertDialog;
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.DialogInterface;
|
|
||||||
import android.graphics.Canvas;
|
|
||||||
import android.graphics.Paint;
|
|
||||||
import android.util.DisplayMetrics;
|
|
||||||
import android.view.MotionEvent;
|
|
||||||
import android.view.View;
|
|
||||||
import android.widget.RelativeLayout;
|
|
||||||
|
|
||||||
public abstract class VirtualControllerElement extends View {
|
|
||||||
protected static boolean _PRINT_DEBUG_INFORMATION = false;
|
|
||||||
|
|
||||||
protected VirtualController virtualController;
|
|
||||||
|
|
||||||
private final Paint paint = new Paint();
|
|
||||||
|
|
||||||
private int normalColor = 0xF0888888;
|
|
||||||
protected int pressedColor = 0xF00000FF;
|
|
||||||
private int configNormalColor = 0xF0FF0000;
|
|
||||||
private int configSelectedColor = 0xF000FF00;
|
|
||||||
|
|
||||||
protected int startSize_x;
|
|
||||||
protected int startSize_y;
|
|
||||||
|
|
||||||
float position_pressed_x = 0;
|
|
||||||
float position_pressed_y = 0;
|
|
||||||
|
|
||||||
private enum Mode {
|
|
||||||
Normal,
|
|
||||||
Resize,
|
|
||||||
Move
|
|
||||||
}
|
|
||||||
|
|
||||||
private Mode currentMode = Mode.Normal;
|
|
||||||
|
|
||||||
protected VirtualControllerElement(VirtualController controller, Context context) {
|
|
||||||
super(context);
|
|
||||||
|
|
||||||
this.virtualController = controller;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected void moveElement(int pressed_x, int pressed_y, int x, int y) {
|
|
||||||
int newPos_x = (int) getX() + x - pressed_x;
|
|
||||||
int newPos_y = (int) getY() + y - pressed_y;
|
|
||||||
|
|
||||||
RelativeLayout.LayoutParams layoutParams = (RelativeLayout.LayoutParams) getLayoutParams();
|
|
||||||
|
|
||||||
layoutParams.leftMargin = newPos_x > 0 ? newPos_x : 0;
|
|
||||||
layoutParams.topMargin = newPos_y > 0 ? newPos_y : 0;
|
|
||||||
layoutParams.rightMargin = 0;
|
|
||||||
layoutParams.bottomMargin = 0;
|
|
||||||
|
|
||||||
requestLayout();
|
|
||||||
}
|
|
||||||
|
|
||||||
protected void resizeElement(int pressed_x, int pressed_y, int width, int height) {
|
|
||||||
RelativeLayout.LayoutParams layoutParams = (RelativeLayout.LayoutParams) getLayoutParams();
|
|
||||||
|
|
||||||
int newHeight = height + (startSize_y - pressed_y);
|
|
||||||
int newWidth = width + (startSize_x - pressed_x);
|
|
||||||
|
|
||||||
layoutParams.height = newHeight > 20 ? newHeight : 20;
|
|
||||||
layoutParams.width = newWidth > 20 ? newWidth : 20;
|
|
||||||
|
|
||||||
requestLayout();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onDraw(Canvas canvas) {
|
|
||||||
onElementDraw(canvas);
|
|
||||||
|
|
||||||
if (currentMode != Mode.Normal) {
|
|
||||||
paint.setColor(configSelectedColor);
|
|
||||||
paint.setStrokeWidth(getDefaultStrokeWidth());
|
|
||||||
paint.setStyle(Paint.Style.STROKE);
|
|
||||||
|
|
||||||
canvas.drawRect(paint.getStrokeWidth(), paint.getStrokeWidth(),
|
|
||||||
getWidth()-paint.getStrokeWidth(), getHeight()-paint.getStrokeWidth(),
|
|
||||||
paint);
|
|
||||||
}
|
|
||||||
|
|
||||||
super.onDraw(canvas);
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
protected void actionShowNormalColorChooser() {
|
|
||||||
AmbilWarnaDialog colorDialog = new AmbilWarnaDialog(getContext(), normalColor, true, new AmbilWarnaDialog.OnAmbilWarnaListener() {
|
|
||||||
@Override
|
|
||||||
public void onCancel(AmbilWarnaDialog dialog)
|
|
||||||
{}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onOk(AmbilWarnaDialog dialog, int color) {
|
|
||||||
normalColor = color;
|
|
||||||
invalidate();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
colorDialog.show();
|
|
||||||
}
|
|
||||||
|
|
||||||
protected void actionShowPressedColorChooser() {
|
|
||||||
AmbilWarnaDialog colorDialog = new AmbilWarnaDialog(getContext(), normalColor, true, new AmbilWarnaDialog.OnAmbilWarnaListener() {
|
|
||||||
@Override
|
|
||||||
public void onCancel(AmbilWarnaDialog dialog) {
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onOk(AmbilWarnaDialog dialog, int color) {
|
|
||||||
pressedColor = color;
|
|
||||||
invalidate();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
colorDialog.show();
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
|
|
||||||
protected void actionEnableMove() {
|
|
||||||
currentMode = Mode.Move;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected void actionEnableResize() {
|
|
||||||
currentMode = Mode.Resize;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected void actionCancel() {
|
|
||||||
currentMode = Mode.Normal;
|
|
||||||
invalidate();
|
|
||||||
}
|
|
||||||
|
|
||||||
protected int getDefaultColor() {
|
|
||||||
return (virtualController.getControllerMode() == VirtualController.ControllerMode.Configuration) ?
|
|
||||||
configNormalColor : normalColor;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected int getDefaultStrokeWidth() {
|
|
||||||
DisplayMetrics screen = getResources().getDisplayMetrics();
|
|
||||||
return (int)(screen.heightPixels*0.004f);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected void showConfigurationDialog() {
|
|
||||||
try {
|
|
||||||
AlertDialog.Builder alertBuilder = new AlertDialog.Builder(getContext());
|
|
||||||
|
|
||||||
alertBuilder.setTitle("Configuration");
|
|
||||||
|
|
||||||
CharSequence functions[] = new CharSequence[]{
|
|
||||||
"Move",
|
|
||||||
"Resize",
|
|
||||||
/*election
|
|
||||||
"Set n
|
|
||||||
Disable color sormal color",
|
|
||||||
"Set pressed color",
|
|
||||||
*/
|
|
||||||
"Cancel"
|
|
||||||
};
|
|
||||||
|
|
||||||
alertBuilder.setItems(functions, new DialogInterface.OnClickListener() {
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onClick(DialogInterface dialog, int which) {
|
|
||||||
switch (which) {
|
|
||||||
case 0: { // move
|
|
||||||
actionEnableMove();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 1: { // resize
|
|
||||||
actionEnableResize();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
/*
|
|
||||||
case 2: { // set default color
|
|
||||||
actionShowNormalColorChooser();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 3: { // set pressed color
|
|
||||||
actionShowPressedColorChooser();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
default: { // cancel
|
|
||||||
actionCancel();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
AlertDialog alert = alertBuilder.create();
|
|
||||||
// show menu
|
|
||||||
alert.show();
|
|
||||||
} catch (Exception e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean onTouchEvent(MotionEvent event) {
|
|
||||||
if (virtualController.getControllerMode() == VirtualController.ControllerMode.Active) {
|
|
||||||
return onElementTouchEvent(event);
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (event.getActionMasked()) {
|
|
||||||
case MotionEvent.ACTION_DOWN:
|
|
||||||
case MotionEvent.ACTION_POINTER_DOWN: {
|
|
||||||
position_pressed_x = event.getX();
|
|
||||||
position_pressed_y = event.getY();
|
|
||||||
startSize_x = getWidth();
|
|
||||||
startSize_y = getHeight();
|
|
||||||
|
|
||||||
actionEnableMove();
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
case MotionEvent.ACTION_MOVE: {
|
|
||||||
switch (currentMode) {
|
|
||||||
case Move: {
|
|
||||||
moveElement(
|
|
||||||
(int) position_pressed_x,
|
|
||||||
(int) position_pressed_y,
|
|
||||||
(int) event.getX(),
|
|
||||||
(int) event.getY());
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case Resize: {
|
|
||||||
resizeElement(
|
|
||||||
(int) position_pressed_x,
|
|
||||||
(int) position_pressed_y,
|
|
||||||
(int) event.getX(),
|
|
||||||
(int) event.getY());
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case Normal: {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
case MotionEvent.ACTION_CANCEL:
|
|
||||||
case MotionEvent.ACTION_UP:
|
|
||||||
case MotionEvent.ACTION_POINTER_UP: {
|
|
||||||
actionCancel();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
default: {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
abstract protected void onElementDraw(Canvas canvas);
|
|
||||||
|
|
||||||
abstract public boolean onElementTouchEvent(MotionEvent event);
|
|
||||||
|
|
||||||
protected static final void _DBG(String text) {
|
|
||||||
if (_PRINT_DEBUG_INFORMATION) {
|
|
||||||
System.out.println(text);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setColors(int normalColor, int pressedColor) {
|
|
||||||
this.normalColor = normalColor;
|
|
||||||
this.pressedColor = pressedColor;
|
|
||||||
|
|
||||||
invalidate();
|
|
||||||
}
|
|
||||||
|
|
||||||
protected final float getPercent(float value, float percent) {
|
|
||||||
return value / 100 * percent;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected final int getCorrectWidth() {
|
|
||||||
return getWidth() > getHeight() ? getHeight() : getWidth();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
public JSONObject getConfiguration () {
|
|
||||||
JSONObject configuration = new JSONObject();
|
|
||||||
return configuration;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void loadConfiguration (JSONObject configuration) {
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,288 @@
|
|||||||
|
package com.limelight.binding.video;
|
||||||
|
|
||||||
|
import java.io.BufferedReader;
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileReader;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
|
||||||
|
import android.graphics.PixelFormat;
|
||||||
|
import android.os.Build;
|
||||||
|
import android.view.SurfaceHolder;
|
||||||
|
|
||||||
|
import com.limelight.LimeLog;
|
||||||
|
import com.limelight.nvstream.av.ByteBufferDescriptor;
|
||||||
|
import com.limelight.nvstream.av.DecodeUnit;
|
||||||
|
import com.limelight.nvstream.av.video.VideoDecoderRenderer;
|
||||||
|
import com.limelight.nvstream.av.video.VideoDepacketizer;
|
||||||
|
import com.limelight.nvstream.av.video.cpu.AvcDecoder;
|
||||||
|
|
||||||
|
@SuppressWarnings("EmptyCatchBlock")
|
||||||
|
public class AndroidCpuDecoderRenderer extends EnhancedDecoderRenderer {
|
||||||
|
|
||||||
|
private Thread rendererThread, decoderThread;
|
||||||
|
private int targetFps;
|
||||||
|
|
||||||
|
private static final int DECODER_BUFFER_SIZE = 92*1024;
|
||||||
|
private ByteBuffer decoderBuffer;
|
||||||
|
|
||||||
|
// Only sleep if the difference is above this value
|
||||||
|
private static final int WAIT_CEILING_MS = 5;
|
||||||
|
|
||||||
|
private static final int LOW_PERF = 1;
|
||||||
|
private static final int MED_PERF = 2;
|
||||||
|
private static final int HIGH_PERF = 3;
|
||||||
|
|
||||||
|
private int totalFrames;
|
||||||
|
private long totalTimeMs;
|
||||||
|
|
||||||
|
private int cpuCount = Runtime.getRuntime().availableProcessors();
|
||||||
|
|
||||||
|
@SuppressWarnings("unused")
|
||||||
|
private int findOptimalPerformanceLevel() {
|
||||||
|
StringBuilder cpuInfo = new StringBuilder();
|
||||||
|
BufferedReader br = null;
|
||||||
|
try {
|
||||||
|
br = new BufferedReader(new FileReader(new File("/proc/cpuinfo")));
|
||||||
|
for (;;) {
|
||||||
|
int ch = br.read();
|
||||||
|
if (ch == -1)
|
||||||
|
break;
|
||||||
|
cpuInfo.append((char)ch);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Here we're doing very simple heuristics based on CPU model
|
||||||
|
String cpuInfoStr = cpuInfo.toString();
|
||||||
|
|
||||||
|
// We order them from greatest to least for proper detection
|
||||||
|
// of devices with multiple sets of cores (like Exynos 5 Octa)
|
||||||
|
// TODO Make this better (only even kind of works on ARM)
|
||||||
|
if (Build.FINGERPRINT.contains("generic")) {
|
||||||
|
// Emulator
|
||||||
|
return LOW_PERF;
|
||||||
|
}
|
||||||
|
else if (cpuInfoStr.contains("0xc0f")) {
|
||||||
|
// Cortex-A15
|
||||||
|
return MED_PERF;
|
||||||
|
}
|
||||||
|
else if (cpuInfoStr.contains("0xc09")) {
|
||||||
|
// Cortex-A9
|
||||||
|
return LOW_PERF;
|
||||||
|
}
|
||||||
|
else if (cpuInfoStr.contains("0xc07")) {
|
||||||
|
// Cortex-A7
|
||||||
|
return LOW_PERF;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// Didn't have anything we're looking for
|
||||||
|
return MED_PERF;
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
} finally {
|
||||||
|
if (br != null) {
|
||||||
|
try {
|
||||||
|
br.close();
|
||||||
|
} catch (IOException e) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Couldn't read cpuinfo, so assume medium
|
||||||
|
return MED_PERF;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean setup(int width, int height, int redrawRate, Object renderTarget, int drFlags) {
|
||||||
|
this.targetFps = redrawRate;
|
||||||
|
|
||||||
|
int perfLevel = LOW_PERF; //findOptimalPerformanceLevel();
|
||||||
|
int threadCount;
|
||||||
|
|
||||||
|
int avcFlags = 0;
|
||||||
|
switch (perfLevel) {
|
||||||
|
case HIGH_PERF:
|
||||||
|
// Single threaded low latency decode is ideal but hard to acheive
|
||||||
|
avcFlags = AvcDecoder.LOW_LATENCY_DECODE;
|
||||||
|
threadCount = 1;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case LOW_PERF:
|
||||||
|
// Disable the loop filter for performance reasons
|
||||||
|
avcFlags = AvcDecoder.FAST_BILINEAR_FILTERING;
|
||||||
|
|
||||||
|
// Use plenty of threads to try to utilize the CPU as best we can
|
||||||
|
threadCount = cpuCount - 1;
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
case MED_PERF:
|
||||||
|
avcFlags = AvcDecoder.BILINEAR_FILTERING;
|
||||||
|
|
||||||
|
// Only use 2 threads to minimize frame processing latency
|
||||||
|
threadCount = 2;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the user wants quality, we'll remove the low IQ flags
|
||||||
|
if ((drFlags & VideoDecoderRenderer.FLAG_PREFER_QUALITY) != 0) {
|
||||||
|
// Make sure the loop filter is enabled
|
||||||
|
avcFlags &= ~AvcDecoder.DISABLE_LOOP_FILTER;
|
||||||
|
|
||||||
|
// Disable the non-compliant speed optimizations
|
||||||
|
avcFlags &= ~AvcDecoder.FAST_DECODE;
|
||||||
|
|
||||||
|
LimeLog.info("Using high quality decoding");
|
||||||
|
}
|
||||||
|
|
||||||
|
SurfaceHolder sh = (SurfaceHolder)renderTarget;
|
||||||
|
sh.setFormat(PixelFormat.RGBX_8888);
|
||||||
|
|
||||||
|
int err = AvcDecoder.init(width, height, avcFlags, threadCount);
|
||||||
|
if (err != 0) {
|
||||||
|
throw new IllegalStateException("AVC decoder initialization failure: "+err);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!AvcDecoder.setRenderTarget(sh.getSurface())) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
decoderBuffer = ByteBuffer.allocate(DECODER_BUFFER_SIZE + AvcDecoder.getInputPaddingSize());
|
||||||
|
|
||||||
|
LimeLog.info("Using software decoding (performance level: "+perfLevel+")");
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean start(final VideoDepacketizer depacketizer) {
|
||||||
|
decoderThread = new Thread() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
DecodeUnit du;
|
||||||
|
while (!isInterrupted()) {
|
||||||
|
try {
|
||||||
|
du = depacketizer.takeNextDecodeUnit();
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
submitDecodeUnit(du);
|
||||||
|
depacketizer.freeDecodeUnit(du);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
decoderThread.setName("Video - Decoder (CPU)");
|
||||||
|
decoderThread.setPriority(Thread.MAX_PRIORITY - 1);
|
||||||
|
decoderThread.start();
|
||||||
|
|
||||||
|
rendererThread = new Thread() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
long nextFrameTime = System.currentTimeMillis();
|
||||||
|
DecodeUnit du;
|
||||||
|
while (!isInterrupted())
|
||||||
|
{
|
||||||
|
long diff = nextFrameTime - System.currentTimeMillis();
|
||||||
|
|
||||||
|
if (diff > WAIT_CEILING_MS) {
|
||||||
|
try {
|
||||||
|
Thread.sleep(diff - WAIT_CEILING_MS);
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
nextFrameTime = computePresentationTimeMs(targetFps);
|
||||||
|
AvcDecoder.redraw();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
rendererThread.setName("Video - Renderer (CPU)");
|
||||||
|
rendererThread.setPriority(Thread.MAX_PRIORITY);
|
||||||
|
rendererThread.start();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private long computePresentationTimeMs(int frameRate) {
|
||||||
|
return System.currentTimeMillis() + (1000 / frameRate);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void stop() {
|
||||||
|
rendererThread.interrupt();
|
||||||
|
decoderThread.interrupt();
|
||||||
|
|
||||||
|
try {
|
||||||
|
rendererThread.join();
|
||||||
|
} catch (InterruptedException e) { }
|
||||||
|
try {
|
||||||
|
decoderThread.join();
|
||||||
|
} catch (InterruptedException e) { }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void release() {
|
||||||
|
AvcDecoder.destroy();
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean submitDecodeUnit(DecodeUnit decodeUnit) {
|
||||||
|
byte[] data;
|
||||||
|
|
||||||
|
// Use the reserved decoder buffer if this decode unit will fit
|
||||||
|
if (decodeUnit.getDataLength() <= DECODER_BUFFER_SIZE) {
|
||||||
|
decoderBuffer.clear();
|
||||||
|
|
||||||
|
for (ByteBufferDescriptor bbd : decodeUnit.getBufferList()) {
|
||||||
|
decoderBuffer.put(bbd.data, bbd.offset, bbd.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
data = decoderBuffer.array();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
data = new byte[decodeUnit.getDataLength()+AvcDecoder.getInputPaddingSize()];
|
||||||
|
|
||||||
|
int offset = 0;
|
||||||
|
for (ByteBufferDescriptor bbd : decodeUnit.getBufferList()) {
|
||||||
|
System.arraycopy(bbd.data, bbd.offset, data, offset, bbd.length);
|
||||||
|
offset += bbd.length;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean success = (AvcDecoder.decode(data, 0, decodeUnit.getDataLength()) == 0);
|
||||||
|
if (success) {
|
||||||
|
long timeAfterDecode = System.currentTimeMillis();
|
||||||
|
|
||||||
|
// Add delta time to the totals (excluding probable outliers)
|
||||||
|
long delta = timeAfterDecode - decodeUnit.getReceiveTimestamp();
|
||||||
|
if (delta >= 0 && delta < 1000) {
|
||||||
|
totalTimeMs += delta;
|
||||||
|
totalFrames++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return success;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getCapabilities() {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getAverageDecoderLatency() {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getAverageEndToEndLatency() {
|
||||||
|
if (totalFrames == 0) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return (int)(totalTimeMs / totalFrames);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getDecoderName() {
|
||||||
|
return "CPU decoding";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
package com.limelight.binding.video;
|
||||||
|
|
||||||
|
import com.limelight.nvstream.av.video.VideoDecoderRenderer;
|
||||||
|
import com.limelight.nvstream.av.video.VideoDepacketizer;
|
||||||
|
|
||||||
|
public class ConfigurableDecoderRenderer extends EnhancedDecoderRenderer {
|
||||||
|
|
||||||
|
private EnhancedDecoderRenderer decoderRenderer;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void release() {
|
||||||
|
if (decoderRenderer != null) {
|
||||||
|
decoderRenderer.release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean setup(int width, int height, int redrawRate, Object renderTarget, int drFlags) {
|
||||||
|
if (decoderRenderer == null) {
|
||||||
|
throw new IllegalStateException("ConfigurableDecoderRenderer not initialized");
|
||||||
|
}
|
||||||
|
return decoderRenderer.setup(width, height, redrawRate, renderTarget, drFlags);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void initializeWithFlags(int drFlags) {
|
||||||
|
if ((drFlags & VideoDecoderRenderer.FLAG_FORCE_HARDWARE_DECODING) != 0 ||
|
||||||
|
((drFlags & VideoDecoderRenderer.FLAG_FORCE_SOFTWARE_DECODING) == 0 &&
|
||||||
|
MediaCodecHelper.findProbableSafeDecoder() != null)) {
|
||||||
|
decoderRenderer = new MediaCodecDecoderRenderer();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
decoderRenderer = new AndroidCpuDecoderRenderer();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isHardwareAccelerated() {
|
||||||
|
if (decoderRenderer == null) {
|
||||||
|
throw new IllegalStateException("ConfigurableDecoderRenderer not initialized");
|
||||||
|
}
|
||||||
|
return (decoderRenderer instanceof MediaCodecDecoderRenderer);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean start(VideoDepacketizer depacketizer) {
|
||||||
|
return decoderRenderer.start(depacketizer);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void stop() {
|
||||||
|
decoderRenderer.stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getCapabilities() {
|
||||||
|
return decoderRenderer.getCapabilities();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getAverageDecoderLatency() {
|
||||||
|
if (decoderRenderer != null) {
|
||||||
|
return decoderRenderer.getAverageDecoderLatency();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getAverageEndToEndLatency() {
|
||||||
|
if (decoderRenderer != null) {
|
||||||
|
return decoderRenderer.getAverageEndToEndLatency();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getDecoderName() {
|
||||||
|
if (decoderRenderer != null) {
|
||||||
|
return decoderRenderer.getDecoderName();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
package com.limelight.binding.video;
|
|
||||||
|
|
||||||
public interface CrashListener {
|
|
||||||
void notifyCrash(Exception e);
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package com.limelight.binding.video;
|
||||||
|
|
||||||
|
import com.limelight.nvstream.av.video.VideoDecoderRenderer;
|
||||||
|
|
||||||
|
public abstract class EnhancedDecoderRenderer implements VideoDecoderRenderer {
|
||||||
|
public abstract String getDecoderName();
|
||||||
|
}
|
||||||
File diff suppressed because it is too large.
Load diff
@@ -10,9 +10,6 @@ import java.util.Locale;
|
|||||||
|
|
||||||
import android.annotation.SuppressLint;
|
import android.annotation.SuppressLint;
|
||||||
import android.annotation.TargetApi;
|
import android.annotation.TargetApi;
|
||||||
import android.app.ActivityManager;
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.pm.ConfigurationInfo;
|
|
||||||
import android.media.MediaCodecInfo;
|
import android.media.MediaCodecInfo;
|
||||||
import android.media.MediaCodecList;
|
import android.media.MediaCodecList;
|
||||||
import android.media.MediaCodecInfo.CodecCapabilities;
|
import android.media.MediaCodecInfo.CodecCapabilities;
|
||||||
@@ -23,157 +20,42 @@ import com.limelight.LimeLog;
|
|||||||
|
|
||||||
public class MediaCodecHelper {
|
public class MediaCodecHelper {
|
||||||
|
|
||||||
private static final List<String> preferredDecoders;
|
public static final List<String> preferredDecoders;
|
||||||
|
|
||||||
private static final List<String> blacklistedDecoderPrefixes;
|
|
||||||
private static final List<String> spsFixupBitstreamFixupDecoderPrefixes;
|
|
||||||
private static final List<String> whitelistedAdaptiveResolutionPrefixes;
|
|
||||||
private static final List<String> baselineProfileHackPrefixes;
|
|
||||||
private static final List<String> directSubmitPrefixes;
|
|
||||||
private static final List<String> constrainedHighProfilePrefixes;
|
|
||||||
private static final List<String> whitelistedHevcDecoders;
|
|
||||||
private static final List<String> refFrameInvalidationAvcPrefixes;
|
|
||||||
private static final List<String> refFrameInvalidationHevcPrefixes;
|
|
||||||
|
|
||||||
static {
|
|
||||||
directSubmitPrefixes = new LinkedList<>();
|
|
||||||
|
|
||||||
// These decoders have low enough input buffer latency that they
|
|
||||||
// can be directly invoked from the receive thread
|
|
||||||
directSubmitPrefixes.add("omx.qcom");
|
|
||||||
directSubmitPrefixes.add("omx.sec");
|
|
||||||
directSubmitPrefixes.add("omx.exynos");
|
|
||||||
directSubmitPrefixes.add("omx.intel");
|
|
||||||
directSubmitPrefixes.add("omx.brcm");
|
|
||||||
directSubmitPrefixes.add("omx.TI");
|
|
||||||
directSubmitPrefixes.add("omx.arc");
|
|
||||||
directSubmitPrefixes.add("omx.nvidia");
|
|
||||||
}
|
|
||||||
|
|
||||||
static {
|
|
||||||
refFrameInvalidationAvcPrefixes = new LinkedList<>();
|
|
||||||
refFrameInvalidationHevcPrefixes = new LinkedList<>();
|
|
||||||
|
|
||||||
// Qualcomm and NVIDIA may be added at runtime
|
|
||||||
}
|
|
||||||
|
|
||||||
|
public static final List<String> blacklistedDecoderPrefixes;
|
||||||
|
public static final List<String> spsFixupBitstreamFixupDecoderPrefixes;
|
||||||
|
public static final List<String> whitelistedAdaptiveResolutionPrefixes;
|
||||||
|
public static final List<String> baselineProfileHackPrefixes;
|
||||||
|
|
||||||
static {
|
static {
|
||||||
preferredDecoders = new LinkedList<>();
|
preferredDecoders = new LinkedList<String>();
|
||||||
}
|
}
|
||||||
|
|
||||||
static {
|
static {
|
||||||
blacklistedDecoderPrefixes = new LinkedList<>();
|
blacklistedDecoderPrefixes = new LinkedList<String>();
|
||||||
|
|
||||||
// Blacklist software decoders that don't support H264 high profile,
|
// Software decoders that don't support H264 high profile
|
||||||
// but exclude the official AOSP emulator from this restriction.
|
blacklistedDecoderPrefixes.add("omx.google");
|
||||||
if (!Build.HARDWARE.equals("ranchu") || !Build.BRAND.equals("google")) {
|
blacklistedDecoderPrefixes.add("AVCDecoder");
|
||||||
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");
|
|
||||||
|
|
||||||
// Force these decoders disabled because:
|
|
||||||
// 1) They are software decoders, so the performance is terrible
|
|
||||||
// 2) They crash with our HEVC stream anyway (at least prior to CSD batching)
|
|
||||||
blacklistedDecoderPrefixes.add("OMX.qcom.video.decoder.hevcswvdec");
|
|
||||||
blacklistedDecoderPrefixes.add("OMX.SEC.hevc.sw.dec");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static {
|
static {
|
||||||
// If a decoder qualifies for reference frame invalidation,
|
spsFixupBitstreamFixupDecoderPrefixes = new LinkedList<String>();
|
||||||
// these entries will be ignored for those decoders.
|
|
||||||
spsFixupBitstreamFixupDecoderPrefixes = new LinkedList<>();
|
|
||||||
spsFixupBitstreamFixupDecoderPrefixes.add("omx.nvidia");
|
spsFixupBitstreamFixupDecoderPrefixes.add("omx.nvidia");
|
||||||
spsFixupBitstreamFixupDecoderPrefixes.add("omx.qcom");
|
spsFixupBitstreamFixupDecoderPrefixes.add("omx.qcom");
|
||||||
|
spsFixupBitstreamFixupDecoderPrefixes.add("omx.mtk");
|
||||||
spsFixupBitstreamFixupDecoderPrefixes.add("omx.brcm");
|
spsFixupBitstreamFixupDecoderPrefixes.add("omx.brcm");
|
||||||
|
|
||||||
baselineProfileHackPrefixes = new LinkedList<>();
|
baselineProfileHackPrefixes = new LinkedList<String>();
|
||||||
baselineProfileHackPrefixes.add("omx.intel");
|
baselineProfileHackPrefixes.add("omx.intel");
|
||||||
|
|
||||||
whitelistedAdaptiveResolutionPrefixes = new LinkedList<>();
|
whitelistedAdaptiveResolutionPrefixes = new LinkedList<String>();
|
||||||
whitelistedAdaptiveResolutionPrefixes.add("omx.nvidia");
|
whitelistedAdaptiveResolutionPrefixes.add("omx.nvidia");
|
||||||
whitelistedAdaptiveResolutionPrefixes.add("omx.qcom");
|
whitelistedAdaptiveResolutionPrefixes.add("omx.qcom");
|
||||||
whitelistedAdaptiveResolutionPrefixes.add("omx.sec");
|
whitelistedAdaptiveResolutionPrefixes.add("omx.sec");
|
||||||
whitelistedAdaptiveResolutionPrefixes.add("omx.TI");
|
whitelistedAdaptiveResolutionPrefixes.add("omx.TI");
|
||||||
|
|
||||||
constrainedHighProfilePrefixes = new LinkedList<>();
|
|
||||||
constrainedHighProfilePrefixes.add("omx.intel");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static {
|
|
||||||
whitelistedHevcDecoders = new LinkedList<>();
|
|
||||||
|
|
||||||
// Allow software HEVC decoding in the official AOSP emulator
|
|
||||||
if (Build.HARDWARE.equals("ranchu") && Build.BRAND.equals("google")) {
|
|
||||||
whitelistedHevcDecoders.add("omx.google");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Exynos seems to be the only HEVC decoder that works reliably
|
|
||||||
whitelistedHevcDecoders.add("omx.exynos");
|
|
||||||
|
|
||||||
// TODO: This needs a similar fixup to the Tegra 3 otherwise it buffers 16 frames
|
|
||||||
//whitelistedHevcDecoders.add("omx.nvidia");
|
|
||||||
|
|
||||||
// Sony ATVs have broken MediaTek codecs (decoder hangs after rendering the first frame).
|
|
||||||
// I know the Fire TV 2 works, so I'll just whitelist Amazon devices which seem
|
|
||||||
// to actually be tested. Ugh...
|
|
||||||
if (Build.MANUFACTURER.equalsIgnoreCase("Amazon")) {
|
|
||||||
whitelistedHevcDecoders.add("omx.mtk");
|
|
||||||
}
|
|
||||||
|
|
||||||
// These theoretically have good HEVC decoding capabilities (potentially better than
|
|
||||||
// their AVC decoders), but haven't been tested enough
|
|
||||||
//whitelistedHevcDecoders.add("omx.amlogic");
|
|
||||||
//whitelistedHevcDecoders.add("omx.rk");
|
|
||||||
|
|
||||||
// Based on GPU attributes queried at runtime, the omx.qcom prefix will be added
|
|
||||||
// during initialization to avoid SoCs with broken HEVC decoders.
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void initializeWithContext(Context context) {
|
|
||||||
ActivityManager activityManager =
|
|
||||||
(ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
|
|
||||||
ConfigurationInfo configInfo = activityManager.getDeviceConfigurationInfo();
|
|
||||||
if (configInfo.reqGlEsVersion != ConfigurationInfo.GL_ES_VERSION_UNDEFINED) {
|
|
||||||
LimeLog.info("OpenGL ES version: "+configInfo.reqGlEsVersion);
|
|
||||||
|
|
||||||
// Tegra K1 and later can do reference frame invalidation properly
|
|
||||||
if (configInfo.reqGlEsVersion >= 0x30000) {
|
|
||||||
LimeLog.info("Added omx.nvidia to AVC reference frame invalidation support list");
|
|
||||||
refFrameInvalidationAvcPrefixes.add("omx.nvidia");
|
|
||||||
|
|
||||||
LimeLog.info("Added omx.qcom to AVC reference frame invalidation support list");
|
|
||||||
refFrameInvalidationAvcPrefixes.add("omx.qcom");
|
|
||||||
|
|
||||||
// Prior to M, we were tricking the decoder into using baseline profile, which
|
|
||||||
// won't support RFI properly.
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
|
||||||
LimeLog.info("Added omx.intel to AVC reference frame invalidation support list");
|
|
||||||
refFrameInvalidationAvcPrefixes.add("omx.intel");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Qualcomm's early HEVC decoders break hard on our HEVC stream. The best check to
|
|
||||||
// tell the good from the bad decoders are the generation of Adreno GPU included:
|
|
||||||
// 3xx - bad
|
|
||||||
// 4xx - good
|
|
||||||
//
|
|
||||||
// Unfortunately, it's not that easy to get that information here, so I'll use an
|
|
||||||
// approximation by checking the GLES level (<= 3.0 is bad).
|
|
||||||
if (configInfo.reqGlEsVersion > 0x30000) {
|
|
||||||
// FIXME: We prefer reference frame invalidation support (which is only doable on AVC on
|
|
||||||
// older Qualcomm chips) vs. enabling HEVC by default. The user can override using the settings
|
|
||||||
// to force HEVC on.
|
|
||||||
//LimeLog.info("Added omx.qcom to supported HEVC decoders based on GLES 3.1+ support");
|
|
||||||
//whitelistedHevcDecoders.add("omx.qcom");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static boolean isDecoderInList(List<String> decoderList, String decoderName) {
|
private static boolean isDecoderInList(List<String> decoderList, String decoderName) {
|
||||||
for (String badPrefix : decoderList) {
|
for (String badPrefix : decoderList) {
|
||||||
if (decoderName.length() >= badPrefix.length()) {
|
if (decoderName.length() >= badPrefix.length()) {
|
||||||
@@ -186,16 +68,12 @@ public class MediaCodecHelper {
|
|||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static long getMonotonicMillis() {
|
|
||||||
return System.nanoTime() / 1000000L;
|
|
||||||
}
|
|
||||||
|
|
||||||
@TargetApi(Build.VERSION_CODES.KITKAT)
|
@TargetApi(Build.VERSION_CODES.KITKAT)
|
||||||
public static boolean decoderSupportsAdaptivePlayback(String decoderName) {
|
public static boolean decoderSupportsAdaptivePlayback(String decoderName, MediaCodecInfo decoderInfo) {
|
||||||
/*
|
/*
|
||||||
FIXME: Intel's decoder on Nexus Player forces the high latency path if adaptive playback is enabled
|
FIXME: Intel's decoder on Nexus Player forces the high latency path if adaptive playback is enabled
|
||||||
so we'll keep it off for now, since we don't know whether other devices also do the same
|
so we'll keep it off for now, since we don't know whether other devices also do the same
|
||||||
|
|
||||||
if (isDecoderInList(whitelistedAdaptiveResolutionPrefixes, decoderName)) {
|
if (isDecoderInList(whitelistedAdaptiveResolutionPrefixes, decoderName)) {
|
||||||
LimeLog.info("Adaptive playback supported (whitelist)");
|
LimeLog.info("Adaptive playback supported (whitelist)");
|
||||||
@@ -219,70 +97,19 @@ public class MediaCodecHelper {
|
|||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static boolean decoderNeedsConstrainedHighProfile(String decoderName) {
|
|
||||||
return isDecoderInList(constrainedHighProfilePrefixes, decoderName);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static boolean decoderCanDirectSubmit(String decoderName) {
|
|
||||||
return isDecoderInList(directSubmitPrefixes, decoderName) && !isExynos4Device();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static boolean decoderNeedsSpsBitstreamRestrictions(String decoderName) {
|
public static boolean decoderNeedsSpsBitstreamRestrictions(String decoderName, MediaCodecInfo decoderInfo) {
|
||||||
return isDecoderInList(spsFixupBitstreamFixupDecoderPrefixes, decoderName);
|
return isDecoderInList(spsFixupBitstreamFixupDecoderPrefixes, decoderName);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static boolean decoderNeedsBaselineSpsHack(String decoderName) {
|
public static boolean decoderNeedsBaselineSpsHack(String decoderName, MediaCodecInfo decoderInfo) {
|
||||||
return isDecoderInList(baselineProfileHackPrefixes, decoderName);
|
return isDecoderInList(baselineProfileHackPrefixes, decoderName);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static boolean decoderSupportsRefFrameInvalidationAvc(String decoderName) {
|
|
||||||
return isDecoderInList(refFrameInvalidationAvcPrefixes, decoderName);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static boolean decoderSupportsRefFrameInvalidationHevc(String decoderName) {
|
|
||||||
return isDecoderInList(refFrameInvalidationHevcPrefixes, decoderName);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static boolean decoderIsWhitelistedForHevc(String decoderName) {
|
|
||||||
// TODO: Shield Tablet K1/LTE?
|
|
||||||
//
|
|
||||||
// NVIDIA does partial HEVC acceleration on the Shield Tablet. I don't know
|
|
||||||
// whether the performance is good enough to use for streaming, but they're
|
|
||||||
// using the same omx.nvidia.h265.decode name as the Shield TV which has a
|
|
||||||
// fully accelerated HEVC pipeline. AFAIK, the only K1 device with this
|
|
||||||
// partially accelerated HEVC decoder is the Shield Tablet, so I'll
|
|
||||||
// check for it here.
|
|
||||||
//
|
|
||||||
// TODO: Temporarily disabled with NVIDIA HEVC support
|
|
||||||
/*if (Build.DEVICE.equalsIgnoreCase("shieldtablet")) {
|
|
||||||
return false;
|
|
||||||
}*/
|
|
||||||
|
|
||||||
// Google didn't have official support for HEVC (or more importantly, a CTS test) until
|
|
||||||
// Lollipop. I've seen some MediaTek devices on 4.4 crash when attempting to use HEVC,
|
|
||||||
// so I'm restricting HEVC usage to Lollipop and higher.
|
|
||||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Software decoders are terrible and we never want to use them.
|
|
||||||
// We want to catch decoders like:
|
|
||||||
// OMX.qcom.video.decoder.hevcswvdec
|
|
||||||
// OMX.SEC.hevc.sw.dec
|
|
||||||
//
|
|
||||||
if (decoderName.contains("sw")) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return isDecoderInList(whitelistedHevcDecoders, decoderName);
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressWarnings("deprecation")
|
@SuppressWarnings("deprecation")
|
||||||
@SuppressLint("NewApi")
|
@SuppressLint("NewApi")
|
||||||
private static LinkedList<MediaCodecInfo> getMediaCodecList() {
|
private static LinkedList<MediaCodecInfo> getMediaCodecList() {
|
||||||
LinkedList<MediaCodecInfo> infoList = new LinkedList<>();
|
LinkedList<MediaCodecInfo> infoList = new LinkedList<MediaCodecInfo>();
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||||
MediaCodecList mcl = new MediaCodecList(MediaCodecList.REGULAR_CODECS);
|
MediaCodecList mcl = new MediaCodecList(MediaCodecList.REGULAR_CODECS);
|
||||||
@@ -319,7 +146,7 @@ public class MediaCodecHelper {
|
|||||||
return str;
|
return str;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static MediaCodecInfo findPreferredDecoder() {
|
public static MediaCodecInfo findPreferredDecoder() {
|
||||||
// This is a different algorithm than the other findXXXDecoder functions,
|
// This is a different algorithm than the other findXXXDecoder functions,
|
||||||
// because we want to evaluate the decoders in our list's order
|
// because we want to evaluate the decoders in our list's order
|
||||||
// rather than MediaCodecList's order
|
// rather than MediaCodecList's order
|
||||||
@@ -342,7 +169,7 @@ public class MediaCodecHelper {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static MediaCodecInfo findFirstDecoder(String mimeType) {
|
public static MediaCodecInfo findFirstDecoder() {
|
||||||
for (MediaCodecInfo codecInfo : getMediaCodecList()) {
|
for (MediaCodecInfo codecInfo : getMediaCodecList()) {
|
||||||
// Skip encoders
|
// Skip encoders
|
||||||
if (codecInfo.isEncoder()) {
|
if (codecInfo.isEncoder()) {
|
||||||
@@ -355,9 +182,9 @@ public class MediaCodecHelper {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find a decoder that supports the specified video format
|
// Find a decoder that supports H.264
|
||||||
for (String mime : codecInfo.getSupportedTypes()) {
|
for (String mime : codecInfo.getSupportedTypes()) {
|
||||||
if (mime.equalsIgnoreCase(mimeType)) {
|
if (mime.equalsIgnoreCase("video/avc")) {
|
||||||
LimeLog.info("First decoder choice is "+codecInfo.getName());
|
LimeLog.info("First decoder choice is "+codecInfo.getName());
|
||||||
return codecInfo;
|
return codecInfo;
|
||||||
}
|
}
|
||||||
@@ -367,7 +194,7 @@ public class MediaCodecHelper {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static MediaCodecInfo findProbableSafeDecoder(String mimeType, int requiredProfile) {
|
public static MediaCodecInfo findProbableSafeDecoder() {
|
||||||
// First look for a preferred decoder by name
|
// First look for a preferred decoder by name
|
||||||
MediaCodecInfo info = findPreferredDecoder();
|
MediaCodecInfo info = findPreferredDecoder();
|
||||||
if (info != null) {
|
if (info != null) {
|
||||||
@@ -377,12 +204,12 @@ public class MediaCodecHelper {
|
|||||||
// Now look for decoders we know are safe
|
// Now look for decoders we know are safe
|
||||||
try {
|
try {
|
||||||
// If this function completes, it will determine if the decoder is safe
|
// If this function completes, it will determine if the decoder is safe
|
||||||
return findKnownSafeDecoder(mimeType, requiredProfile);
|
return findKnownSafeDecoder();
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
// Some buggy devices seem to throw exceptions
|
// Some buggy devices seem to throw exceptions
|
||||||
// from getCapabilitiesForType() so we'll just assume
|
// from getCapabilitiesForType() so we'll just assume
|
||||||
// they're okay and go with the first one we find
|
// they're okay and go with the first one we find
|
||||||
return findFirstDecoder(mimeType);
|
return findFirstDecoder();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -390,7 +217,7 @@ public class MediaCodecHelper {
|
|||||||
// since some bad decoders can throw IllegalArgumentExceptions unexpectedly
|
// since some bad decoders can throw IllegalArgumentExceptions unexpectedly
|
||||||
// and we want to be sure all callers are handling this possibility
|
// and we want to be sure all callers are handling this possibility
|
||||||
@SuppressWarnings("RedundantThrows")
|
@SuppressWarnings("RedundantThrows")
|
||||||
private static MediaCodecInfo findKnownSafeDecoder(String mimeType, int requiredProfile) throws Exception {
|
public static MediaCodecInfo findKnownSafeDecoder() throws Exception {
|
||||||
for (MediaCodecInfo codecInfo : getMediaCodecList()) {
|
for (MediaCodecInfo codecInfo : getMediaCodecList()) {
|
||||||
// Skip encoders
|
// Skip encoders
|
||||||
if (codecInfo.isEncoder()) {
|
if (codecInfo.isEncoder()) {
|
||||||
@@ -403,26 +230,21 @@ public class MediaCodecHelper {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find a decoder that supports the requested video format
|
// Find a decoder that supports H.264 high profile
|
||||||
for (String mime : codecInfo.getSupportedTypes()) {
|
for (String mime : codecInfo.getSupportedTypes()) {
|
||||||
if (mime.equalsIgnoreCase(mimeType)) {
|
if (mime.equalsIgnoreCase("video/avc")) {
|
||||||
LimeLog.info("Examining decoder capabilities of "+codecInfo.getName());
|
LimeLog.info("Examining decoder capabilities of "+codecInfo.getName());
|
||||||
|
|
||||||
CodecCapabilities caps = codecInfo.getCapabilitiesForType(mime);
|
CodecCapabilities caps = codecInfo.getCapabilitiesForType(mime);
|
||||||
|
for (CodecProfileLevel profile : caps.profileLevels) {
|
||||||
if (requiredProfile != -1) {
|
if (profile.profile == CodecProfileLevel.AVCProfileHigh) {
|
||||||
for (CodecProfileLevel profile : caps.profileLevels) {
|
LimeLog.info("Decoder "+codecInfo.getName()+" supports high profile");
|
||||||
if (profile.profile == requiredProfile) {
|
LimeLog.info("Selected decoder: "+codecInfo.getName());
|
||||||
LimeLog.info("Decoder " + codecInfo.getName() + " supports required profile");
|
return codecInfo;
|
||||||
return codecInfo;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
LimeLog.info("Decoder " + codecInfo.getName() + " does NOT support required profile");
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
return codecInfo;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
LimeLog.info("Decoder "+codecInfo.getName()+" does NOT support high profile");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -440,7 +262,7 @@ public class MediaCodecHelper {
|
|||||||
break;
|
break;
|
||||||
cpuInfo.append((char)ch);
|
cpuInfo.append((char)ch);
|
||||||
}
|
}
|
||||||
|
|
||||||
return cpuInfo.toString();
|
return cpuInfo.toString();
|
||||||
} finally {
|
} finally {
|
||||||
br.close();
|
br.close();
|
||||||
|
|||||||
@@ -17,150 +17,153 @@ import android.database.sqlite.SQLiteDatabase;
|
|||||||
import android.database.sqlite.SQLiteException;
|
import android.database.sqlite.SQLiteException;
|
||||||
|
|
||||||
public class ComputerDatabaseManager {
|
public class ComputerDatabaseManager {
|
||||||
private static final String COMPUTER_DB_NAME = "computers.db";
|
private static final String COMPUTER_DB_NAME = "computers.db";
|
||||||
private static final String COMPUTER_TABLE_NAME = "Computers";
|
private static final String COMPUTER_TABLE_NAME = "Computers";
|
||||||
private static final String COMPUTER_NAME_COLUMN_NAME = "ComputerName";
|
private static final String COMPUTER_NAME_COLUMN_NAME = "ComputerName";
|
||||||
private static final String COMPUTER_UUID_COLUMN_NAME = "UUID";
|
private static final String COMPUTER_UUID_COLUMN_NAME = "UUID";
|
||||||
private static final String LOCAL_IP_COLUMN_NAME = "LocalIp";
|
private static final String LOCAL_IP_COLUMN_NAME = "LocalIp";
|
||||||
private static final String REMOTE_IP_COLUMN_NAME = "RemoteIp";
|
private static final String REMOTE_IP_COLUMN_NAME = "RemoteIp";
|
||||||
private static final String MAC_COLUMN_NAME = "Mac";
|
private static final String MAC_COLUMN_NAME = "Mac";
|
||||||
|
|
||||||
private static final String ADDRESS_PREFIX = "ADDRESS_PREFIX__";
|
private SQLiteDatabase computerDb;
|
||||||
|
|
||||||
private SQLiteDatabase computerDb;
|
public ComputerDatabaseManager(Context c) {
|
||||||
|
try {
|
||||||
public ComputerDatabaseManager(Context c) {
|
// Create or open an existing DB
|
||||||
try {
|
computerDb = c.openOrCreateDatabase(COMPUTER_DB_NAME, 0, null);
|
||||||
// Create or open an existing DB
|
} catch (SQLiteException e) {
|
||||||
computerDb = c.openOrCreateDatabase(COMPUTER_DB_NAME, 0, null);
|
// Delete the DB and try again
|
||||||
} catch (SQLiteException e) {
|
c.deleteDatabase(COMPUTER_DB_NAME);
|
||||||
// Delete the DB and try again
|
computerDb = c.openOrCreateDatabase(COMPUTER_DB_NAME, 0, null);
|
||||||
c.deleteDatabase(COMPUTER_DB_NAME);
|
}
|
||||||
computerDb = c.openOrCreateDatabase(COMPUTER_DB_NAME, 0, null);
|
initializeDb();
|
||||||
}
|
}
|
||||||
initializeDb();
|
|
||||||
}
|
public void close() {
|
||||||
|
computerDb.close();
|
||||||
public void close() {
|
}
|
||||||
computerDb.close();
|
|
||||||
}
|
private void initializeDb() {
|
||||||
|
// Create tables if they aren't already there
|
||||||
private void initializeDb() {
|
computerDb.execSQL(String.format((Locale)null, "CREATE TABLE IF NOT EXISTS %s(%s TEXT PRIMARY KEY," +
|
||||||
// Create tables if they aren't already there
|
" %s TEXT NOT NULL, %s TEXT NOT NULL, %s TEXT NOT NULL, %s TEXT NOT NULL)",
|
||||||
computerDb.execSQL(String.format((Locale)null, "CREATE TABLE IF NOT EXISTS %s(%s TEXT PRIMARY KEY," +
|
COMPUTER_TABLE_NAME,
|
||||||
" %s TEXT NOT NULL, %s TEXT NOT NULL, %s TEXT NOT NULL, %s TEXT NOT NULL)",
|
COMPUTER_NAME_COLUMN_NAME, COMPUTER_UUID_COLUMN_NAME, LOCAL_IP_COLUMN_NAME,
|
||||||
COMPUTER_TABLE_NAME,
|
REMOTE_IP_COLUMN_NAME, MAC_COLUMN_NAME));
|
||||||
COMPUTER_NAME_COLUMN_NAME, COMPUTER_UUID_COLUMN_NAME, LOCAL_IP_COLUMN_NAME,
|
}
|
||||||
REMOTE_IP_COLUMN_NAME, MAC_COLUMN_NAME));
|
|
||||||
}
|
public void deleteComputer(String name) {
|
||||||
|
computerDb.delete(COMPUTER_TABLE_NAME, COMPUTER_NAME_COLUMN_NAME+"='"+name+"'", null);
|
||||||
public void deleteComputer(String name) {
|
}
|
||||||
computerDb.delete(COMPUTER_TABLE_NAME, COMPUTER_NAME_COLUMN_NAME+"=?", new String[]{name});
|
|
||||||
}
|
public boolean updateComputer(ComputerDetails details) {
|
||||||
|
ContentValues values = new ContentValues();
|
||||||
public boolean updateComputer(ComputerDetails details) {
|
values.put(COMPUTER_NAME_COLUMN_NAME, details.name);
|
||||||
ContentValues values = new ContentValues();
|
values.put(COMPUTER_UUID_COLUMN_NAME, details.uuid.toString());
|
||||||
values.put(COMPUTER_NAME_COLUMN_NAME, details.name);
|
values.put(LOCAL_IP_COLUMN_NAME, details.localIp.getAddress());
|
||||||
values.put(COMPUTER_UUID_COLUMN_NAME, details.uuid.toString());
|
values.put(REMOTE_IP_COLUMN_NAME, details.remoteIp.getAddress());
|
||||||
values.put(LOCAL_IP_COLUMN_NAME, ADDRESS_PREFIX+details.localAddress);
|
values.put(MAC_COLUMN_NAME, details.macAddress);
|
||||||
values.put(REMOTE_IP_COLUMN_NAME, ADDRESS_PREFIX+details.remoteAddress);
|
return -1 != computerDb.insertWithOnConflict(COMPUTER_TABLE_NAME, null, values, SQLiteDatabase.CONFLICT_REPLACE);
|
||||||
values.put(MAC_COLUMN_NAME, details.macAddress);
|
}
|
||||||
return -1 != computerDb.insertWithOnConflict(COMPUTER_TABLE_NAME, null, values, SQLiteDatabase.CONFLICT_REPLACE);
|
|
||||||
}
|
public List<ComputerDetails> getAllComputers() {
|
||||||
|
Cursor c = computerDb.rawQuery("SELECT * FROM "+COMPUTER_TABLE_NAME, null);
|
||||||
private ComputerDetails getComputerFromCursor(Cursor c) {
|
LinkedList<ComputerDetails> computerList = new LinkedList<ComputerDetails>();
|
||||||
ComputerDetails details = new ComputerDetails();
|
while (c.moveToNext()) {
|
||||||
|
ComputerDetails details = new ComputerDetails();
|
||||||
details.name = c.getString(0);
|
|
||||||
|
details.name = c.getString(0);
|
||||||
String uuidStr = c.getString(1);
|
|
||||||
try {
|
String uuidStr = c.getString(1);
|
||||||
details.uuid = UUID.fromString(uuidStr);
|
try {
|
||||||
} catch (IllegalArgumentException e) {
|
details.uuid = UUID.fromString(uuidStr);
|
||||||
// We'll delete this entry
|
} catch (IllegalArgumentException e) {
|
||||||
LimeLog.severe("DB: Corrupted UUID for "+details.name);
|
// We'll delete this entry
|
||||||
}
|
LimeLog.severe("DB: Corrupted UUID for "+details.name);
|
||||||
|
}
|
||||||
// An earlier schema defined addresses as byte blobs. We'll
|
|
||||||
// gracefully migrate those to strings so we can store DNS names
|
try {
|
||||||
// too. To disambiguate, we'll need to prefix them with a string
|
details.localIp = InetAddress.getByAddress(c.getBlob(2));
|
||||||
// greater than the allowable IP address length.
|
} catch (UnknownHostException e) {
|
||||||
try {
|
// We'll delete this entry
|
||||||
details.localAddress = InetAddress.getByAddress(c.getBlob(2)).getHostAddress();
|
LimeLog.severe("DB: Corrupted local IP for "+details.name);
|
||||||
LimeLog.warning("DB: Legacy local address for "+details.name);
|
}
|
||||||
} catch (UnknownHostException e) {
|
|
||||||
// This is probably a hostname/address with the prefix string
|
try {
|
||||||
String stringData = c.getString(2);
|
details.remoteIp = InetAddress.getByAddress(c.getBlob(3));
|
||||||
if (stringData.startsWith(ADDRESS_PREFIX)) {
|
} catch (UnknownHostException e) {
|
||||||
details.localAddress = c.getString(2).substring(ADDRESS_PREFIX.length());
|
// We'll delete this entry
|
||||||
}
|
LimeLog.severe("DB: Corrupted remote IP for "+details.name);
|
||||||
else {
|
}
|
||||||
LimeLog.severe("DB: Corrupted local address for "+details.name);
|
|
||||||
}
|
details.macAddress = c.getString(4);
|
||||||
}
|
|
||||||
|
// This signifies we don't have dynamic state (like pair state)
|
||||||
try {
|
details.state = ComputerDetails.State.UNKNOWN;
|
||||||
details.remoteAddress = InetAddress.getByAddress(c.getBlob(3)).getHostAddress();
|
details.reachability = ComputerDetails.Reachability.UNKNOWN;
|
||||||
LimeLog.warning("DB: Legacy remote address for "+details.name);
|
|
||||||
} catch (UnknownHostException e) {
|
// If a field is corrupt or missing, skip the database entry
|
||||||
// This is probably a hostname/address with the prefix string
|
if (details.uuid == null || details.localIp == null || details.remoteIp == null ||
|
||||||
String stringData = c.getString(3);
|
details.macAddress == null) {
|
||||||
if (stringData.startsWith(ADDRESS_PREFIX)) {
|
continue;
|
||||||
details.remoteAddress = c.getString(3).substring(ADDRESS_PREFIX.length());
|
}
|
||||||
}
|
|
||||||
else {
|
|
||||||
LimeLog.severe("DB: Corrupted local address for "+details.name);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
details.macAddress = c.getString(4);
|
|
||||||
|
|
||||||
// This signifies we don't have dynamic state (like pair state)
|
|
||||||
details.state = ComputerDetails.State.UNKNOWN;
|
|
||||||
details.reachability = ComputerDetails.Reachability.UNKNOWN;
|
|
||||||
|
|
||||||
return details;
|
|
||||||
}
|
|
||||||
|
|
||||||
public List<ComputerDetails> getAllComputers() {
|
|
||||||
Cursor c = computerDb.rawQuery("SELECT * FROM "+COMPUTER_TABLE_NAME, null);
|
|
||||||
LinkedList<ComputerDetails> computerList = new LinkedList<>();
|
|
||||||
while (c.moveToNext()) {
|
|
||||||
ComputerDetails details = getComputerFromCursor(c);
|
|
||||||
|
|
||||||
// If a field is corrupt or missing, skip the database entry
|
|
||||||
if (details.uuid == null || details.localAddress == null || details.remoteAddress == null ||
|
|
||||||
details.macAddress == null) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
computerList.add(details);
|
computerList.add(details);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
c.close();
|
||||||
|
|
||||||
|
return computerList;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ComputerDetails getComputerByName(String name) {
|
||||||
|
Cursor c = computerDb.rawQuery("SELECT * FROM "+COMPUTER_TABLE_NAME+" WHERE "+COMPUTER_NAME_COLUMN_NAME+"='"+name+"'", null);
|
||||||
|
ComputerDetails details = new ComputerDetails();
|
||||||
|
if (!c.moveToFirst()) {
|
||||||
|
// No matching computer
|
||||||
|
c.close();
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
c.close();
|
details.name = c.getString(0);
|
||||||
|
|
||||||
|
String uuidStr = c.getString(1);
|
||||||
|
try {
|
||||||
|
details.uuid = UUID.fromString(uuidStr);
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
// We'll delete this entry
|
||||||
|
LimeLog.severe("DB: Corrupted UUID for "+details.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
details.localIp = InetAddress.getByAddress(c.getBlob(2));
|
||||||
|
} catch (UnknownHostException e) {
|
||||||
|
// We'll delete this entry
|
||||||
|
LimeLog.severe("DB: Corrupted local IP for "+details.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
details.remoteIp = InetAddress.getByAddress(c.getBlob(3));
|
||||||
|
} catch (UnknownHostException e) {
|
||||||
|
// We'll delete this entry
|
||||||
|
LimeLog.severe("DB: Corrupted remote IP for "+details.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
details.macAddress = c.getString(4);
|
||||||
|
|
||||||
|
c.close();
|
||||||
|
|
||||||
return computerList;
|
details.state = ComputerDetails.State.UNKNOWN;
|
||||||
}
|
details.reachability = ComputerDetails.Reachability.UNKNOWN;
|
||||||
|
|
||||||
public ComputerDetails getComputerByName(String name) {
|
// If a field is corrupt or missing, delete the database entry
|
||||||
Cursor c = computerDb.query(COMPUTER_TABLE_NAME, null, COMPUTER_NAME_COLUMN_NAME+"=?", new String[]{name}, null, null, null);
|
if (details.uuid == null || details.localIp == null || details.remoteIp == null ||
|
||||||
if (!c.moveToFirst()) {
|
details.macAddress == null) {
|
||||||
// No matching computer
|
deleteComputer(details.name);
|
||||||
c.close();
|
return null;
|
||||||
return null;
|
}
|
||||||
}
|
|
||||||
|
return details;
|
||||||
ComputerDetails details = getComputerFromCursor(c);
|
}
|
||||||
c.close();
|
|
||||||
|
|
||||||
// If a field is corrupt or missing, delete the database entry
|
|
||||||
if (details.uuid == null || details.localAddress == null || details.remoteAddress == null ||
|
|
||||||
details.macAddress == null) {
|
|
||||||
deleteComputer(details.name);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return details;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -3,5 +3,5 @@ package com.limelight.computers;
|
|||||||
import com.limelight.nvstream.http.ComputerDetails;
|
import com.limelight.nvstream.http.ComputerDetails;
|
||||||
|
|
||||||
public interface ComputerManagerListener {
|
public interface ComputerManagerListener {
|
||||||
void notifyComputerUpdated(ComputerDetails details);
|
public void notifyComputerUpdated(ComputerDetails details);
|
||||||
}
|
}
|
||||||
File diff suppressed because it is too large.
Load diff
@@ -12,75 +12,75 @@ import com.limelight.LimeLog;
|
|||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
|
|
||||||
public class IdentityManager {
|
public class IdentityManager {
|
||||||
private static final String UNIQUE_ID_FILE_NAME = "uniqueid";
|
private static final String UNIQUE_ID_FILE_NAME = "uniqueid";
|
||||||
private static final int UID_SIZE_IN_BYTES = 8;
|
private static final int UID_SIZE_IN_BYTES = 8;
|
||||||
|
|
||||||
private String uniqueId;
|
private String uniqueId;
|
||||||
|
|
||||||
public IdentityManager(Context c) {
|
public IdentityManager(Context c) {
|
||||||
uniqueId = loadUniqueId(c);
|
uniqueId = loadUniqueId(c);
|
||||||
if (uniqueId == null) {
|
if (uniqueId == null) {
|
||||||
uniqueId = generateNewUniqueId(c);
|
uniqueId = generateNewUniqueId(c);
|
||||||
}
|
}
|
||||||
|
|
||||||
LimeLog.info("UID is now: "+uniqueId);
|
LimeLog.info("UID is now: "+uniqueId);
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getUniqueId() {
|
public String getUniqueId() {
|
||||||
return uniqueId;
|
return uniqueId;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static String loadUniqueId(Context c) {
|
private static String loadUniqueId(Context c) {
|
||||||
// 2 Hex digits per byte
|
// 2 Hex digits per byte
|
||||||
char[] uid = new char[UID_SIZE_IN_BYTES * 2];
|
char[] uid = new char[UID_SIZE_IN_BYTES * 2];
|
||||||
InputStreamReader reader = null;
|
InputStreamReader reader = null;
|
||||||
LimeLog.info("Reading UID from disk");
|
LimeLog.info("Reading UID from disk");
|
||||||
try {
|
try {
|
||||||
reader = new InputStreamReader(c.openFileInput(UNIQUE_ID_FILE_NAME));
|
reader = new InputStreamReader(c.openFileInput(UNIQUE_ID_FILE_NAME));
|
||||||
if (reader.read(uid) != UID_SIZE_IN_BYTES * 2)
|
if (reader.read(uid) != UID_SIZE_IN_BYTES * 2)
|
||||||
{
|
{
|
||||||
LimeLog.severe("UID file data is truncated");
|
LimeLog.severe("UID file data is truncated");
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return new String(uid);
|
return new String(uid);
|
||||||
} catch (FileNotFoundException e) {
|
} catch (FileNotFoundException e) {
|
||||||
LimeLog.info("No UID file found");
|
LimeLog.info("No UID file found");
|
||||||
return null;
|
return null;
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
LimeLog.severe("Error while reading UID file");
|
LimeLog.severe("Error while reading UID file");
|
||||||
e.printStackTrace();
|
e.printStackTrace();
|
||||||
return null;
|
return null;
|
||||||
} finally {
|
} finally {
|
||||||
if (reader != null) {
|
if (reader != null) {
|
||||||
try {
|
try {
|
||||||
reader.close();
|
reader.close();
|
||||||
} catch (IOException ignored) {}
|
} catch (IOException ignored) {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static String generateNewUniqueId(Context c) {
|
private static String generateNewUniqueId(Context c) {
|
||||||
// Generate a new UID hex string
|
// Generate a new UID hex string
|
||||||
LimeLog.info("Generating new UID");
|
LimeLog.info("Generating new UID");
|
||||||
String uidStr = String.format((Locale)null, "%016x", new Random().nextLong());
|
String uidStr = String.format((Locale)null, "%016x", new Random().nextLong());
|
||||||
|
|
||||||
OutputStreamWriter writer = null;
|
OutputStreamWriter writer = null;
|
||||||
try {
|
try {
|
||||||
writer = new OutputStreamWriter(c.openFileOutput(UNIQUE_ID_FILE_NAME, 0));
|
writer = new OutputStreamWriter(c.openFileOutput(UNIQUE_ID_FILE_NAME, 0));
|
||||||
writer.write(uidStr);
|
writer.write(uidStr);
|
||||||
LimeLog.info("UID written to disk");
|
LimeLog.info("UID written to disk");
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
LimeLog.severe("Error while writing UID file");
|
LimeLog.severe("Error while writing UID file");
|
||||||
e.printStackTrace();
|
e.printStackTrace();
|
||||||
} finally {
|
} finally {
|
||||||
if (writer != null) {
|
if (writer != null) {
|
||||||
try {
|
try {
|
||||||
writer.close();
|
writer.close();
|
||||||
} catch (IOException ignored) {}
|
} catch (IOException ignored) {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// We can return a UID even if I/O fails
|
// We can return a UID even if I/O fails
|
||||||
return uidStr;
|
return uidStr;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -15,76 +15,76 @@ import android.os.Binder;
|
|||||||
import android.os.IBinder;
|
import android.os.IBinder;
|
||||||
|
|
||||||
public class DiscoveryService extends Service {
|
public class DiscoveryService extends Service {
|
||||||
|
|
||||||
|
private MdnsDiscoveryAgent discoveryAgent;
|
||||||
|
private MdnsDiscoveryListener boundListener;
|
||||||
|
private MulticastLock multicastLock;
|
||||||
|
|
||||||
|
public class DiscoveryBinder extends Binder {
|
||||||
|
public void setListener(MdnsDiscoveryListener listener) {
|
||||||
|
boundListener = listener;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void startDiscovery(int queryIntervalMs) {
|
||||||
|
multicastLock.acquire();
|
||||||
|
discoveryAgent.startDiscovery(queryIntervalMs);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void stopDiscovery() {
|
||||||
|
discoveryAgent.stopDiscovery();
|
||||||
|
multicastLock.release();
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<MdnsComputer> getComputerSet() {
|
||||||
|
return discoveryAgent.getComputerSet();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCreate() {
|
||||||
|
WifiManager wifiMgr = (WifiManager) getSystemService(Context.WIFI_SERVICE);
|
||||||
|
multicastLock = wifiMgr.createMulticastLock("Limelight mDNS");
|
||||||
|
multicastLock.setReferenceCounted(false);
|
||||||
|
|
||||||
|
discoveryAgent = new MdnsDiscoveryAgent(new MdnsDiscoveryListener() {
|
||||||
|
@Override
|
||||||
|
public void notifyComputerAdded(MdnsComputer computer) {
|
||||||
|
if (boundListener != null) {
|
||||||
|
boundListener.notifyComputerAdded(computer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private MdnsDiscoveryAgent discoveryAgent;
|
@Override
|
||||||
private MdnsDiscoveryListener boundListener;
|
public void notifyComputerRemoved(MdnsComputer computer) {
|
||||||
private MulticastLock multicastLock;
|
if (boundListener != null) {
|
||||||
|
boundListener.notifyComputerRemoved(computer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public class DiscoveryBinder extends Binder {
|
@Override
|
||||||
public void setListener(MdnsDiscoveryListener listener) {
|
public void notifyDiscoveryFailure(Exception e) {
|
||||||
boundListener = listener;
|
if (boundListener != null) {
|
||||||
}
|
boundListener.notifyDiscoveryFailure(e);
|
||||||
|
}
|
||||||
public void startDiscovery(int queryIntervalMs) {
|
}
|
||||||
multicastLock.acquire();
|
});
|
||||||
discoveryAgent.startDiscovery(queryIntervalMs);
|
}
|
||||||
}
|
|
||||||
|
private DiscoveryBinder binder = new DiscoveryBinder();
|
||||||
public void stopDiscovery() {
|
|
||||||
discoveryAgent.stopDiscovery();
|
@Override
|
||||||
multicastLock.release();
|
public IBinder onBind(Intent intent) {
|
||||||
}
|
return binder;
|
||||||
|
}
|
||||||
public List<MdnsComputer> getComputerSet() {
|
|
||||||
return discoveryAgent.getComputerSet();
|
@Override
|
||||||
}
|
public boolean onUnbind(Intent intent) {
|
||||||
}
|
// Stop any discovery session
|
||||||
|
discoveryAgent.stopDiscovery();
|
||||||
@Override
|
multicastLock.release();
|
||||||
public void onCreate() {
|
|
||||||
WifiManager wifiMgr = (WifiManager) getApplicationContext().getSystemService(Context.WIFI_SERVICE);
|
// Unbind the listener
|
||||||
multicastLock = wifiMgr.createMulticastLock("Limelight mDNS");
|
boundListener = null;
|
||||||
multicastLock.setReferenceCounted(false);
|
return false;
|
||||||
|
}
|
||||||
discoveryAgent = new MdnsDiscoveryAgent(new MdnsDiscoveryListener() {
|
|
||||||
@Override
|
|
||||||
public void notifyComputerAdded(MdnsComputer computer) {
|
|
||||||
if (boundListener != null) {
|
|
||||||
boundListener.notifyComputerAdded(computer);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void notifyComputerRemoved(MdnsComputer computer) {
|
|
||||||
if (boundListener != null) {
|
|
||||||
boundListener.notifyComputerRemoved(computer);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void notifyDiscoveryFailure(Exception e) {
|
|
||||||
if (boundListener != null) {
|
|
||||||
boundListener.notifyDiscoveryFailure(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private final DiscoveryBinder binder = new DiscoveryBinder();
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public IBinder onBind(Intent intent) {
|
|
||||||
return binder;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean onUnbind(Intent intent) {
|
|
||||||
// Stop any discovery session
|
|
||||||
discoveryAgent.stopDiscovery();
|
|
||||||
multicastLock.release();
|
|
||||||
|
|
||||||
// Unbind the listener
|
|
||||||
boundListener = null;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -1,92 +1,183 @@
|
|||||||
package com.limelight.grid;
|
package com.limelight.grid;
|
||||||
|
|
||||||
import android.app.Activity;
|
import android.content.Context;
|
||||||
|
import android.graphics.Bitmap;
|
||||||
import android.graphics.BitmapFactory;
|
import android.graphics.BitmapFactory;
|
||||||
|
import android.os.AsyncTask;
|
||||||
|
import android.view.View;
|
||||||
import android.widget.ImageView;
|
import android.widget.ImageView;
|
||||||
import android.widget.ProgressBar;
|
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
|
|
||||||
|
import com.koushikdutta.async.future.FutureCallback;
|
||||||
|
import com.koushikdutta.ion.ImageViewBitmapInfo;
|
||||||
|
import com.koushikdutta.ion.Ion;
|
||||||
import com.limelight.AppView;
|
import com.limelight.AppView;
|
||||||
import com.limelight.LimeLog;
|
import com.limelight.LimeLog;
|
||||||
import com.limelight.R;
|
import com.limelight.R;
|
||||||
import com.limelight.grid.assets.CachedAppAssetLoader;
|
import com.limelight.binding.PlatformBinding;
|
||||||
import com.limelight.grid.assets.DiskAssetLoader;
|
|
||||||
import com.limelight.grid.assets.MemoryAssetLoader;
|
|
||||||
import com.limelight.grid.assets.NetworkAssetLoader;
|
|
||||||
import com.limelight.nvstream.http.ComputerDetails;
|
import com.limelight.nvstream.http.ComputerDetails;
|
||||||
|
import com.limelight.nvstream.http.LimelightCryptoProvider;
|
||||||
|
import com.limelight.utils.CacheHelper;
|
||||||
|
|
||||||
|
import java.io.BufferedInputStream;
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileInputStream;
|
||||||
|
import java.io.FileNotFoundException;
|
||||||
|
import java.io.FileOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.net.InetAddress;
|
||||||
|
import java.net.Socket;
|
||||||
|
import java.security.KeyManagementException;
|
||||||
|
import java.security.NoSuchAlgorithmException;
|
||||||
|
import java.security.Principal;
|
||||||
|
import java.security.PrivateKey;
|
||||||
|
import java.security.SecureRandom;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.Comparator;
|
import java.util.Comparator;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.UUID;
|
||||||
|
import java.util.concurrent.Future;
|
||||||
|
|
||||||
|
import javax.net.ssl.HostnameVerifier;
|
||||||
|
import javax.net.ssl.KeyManager;
|
||||||
|
import javax.net.ssl.SSLContext;
|
||||||
|
import javax.net.ssl.SSLSession;
|
||||||
|
import javax.net.ssl.TrustManager;
|
||||||
|
import javax.net.ssl.X509KeyManager;
|
||||||
|
import javax.net.ssl.X509TrustManager;
|
||||||
|
import java.security.cert.X509Certificate;
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
|
||||||
public class AppGridAdapter extends GenericGridAdapter<AppView.AppObject> {
|
public class AppGridAdapter extends GenericGridAdapter<AppView.AppObject> {
|
||||||
private static final int ART_WIDTH_PX = 300;
|
|
||||||
private static final int SMALL_WIDTH_DP = 100;
|
|
||||||
private static final int LARGE_WIDTH_DP = 150;
|
|
||||||
|
|
||||||
private final CachedAppAssetLoader loader;
|
private ComputerDetails computer;
|
||||||
|
private String uniqueId;
|
||||||
|
private LimelightCryptoProvider cryptoProvider;
|
||||||
|
private SSLContext sslContext;
|
||||||
|
private final HashMap<ImageView, Future> pendingRequests = new HashMap<ImageView, Future>();
|
||||||
|
|
||||||
public AppGridAdapter(Activity activity, boolean listMode, boolean small, ComputerDetails computer, String uniqueId) {
|
public AppGridAdapter(Context context, boolean listMode, boolean small, ComputerDetails computer, String uniqueId) throws NoSuchAlgorithmException, KeyManagementException {
|
||||||
super(activity, listMode ? R.layout.simple_row : (small ? R.layout.app_grid_item_small : R.layout.app_grid_item));
|
super(context, listMode ? R.layout.simple_row : (small ? R.layout.app_grid_item_small : R.layout.app_grid_item), R.drawable.image_loading);
|
||||||
|
|
||||||
int dpi = activity.getResources().getDisplayMetrics().densityDpi;
|
this.computer = computer;
|
||||||
int dp;
|
this.uniqueId = uniqueId;
|
||||||
|
|
||||||
if (small) {
|
cryptoProvider = PlatformBinding.getCryptoProvider(context);
|
||||||
dp = SMALL_WIDTH_DP;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
dp = LARGE_WIDTH_DP;
|
|
||||||
}
|
|
||||||
|
|
||||||
double scalingDivisor = ART_WIDTH_PX / (dp * (dpi / 160.0));
|
sslContext = SSLContext.getInstance("SSL");
|
||||||
if (scalingDivisor < 1.0) {
|
sslContext.init(ourKeyman, trustAllCerts, new SecureRandom());
|
||||||
// We don't want to make them bigger before draw-time
|
|
||||||
scalingDivisor = 1.0;
|
|
||||||
}
|
|
||||||
LimeLog.info("Art scaling divisor: " + scalingDivisor);
|
|
||||||
|
|
||||||
BitmapFactory.Options options = new BitmapFactory.Options();
|
|
||||||
options.inSampleSize = (int) scalingDivisor;
|
|
||||||
|
|
||||||
this.loader = new CachedAppAssetLoader(computer, scalingDivisor,
|
|
||||||
new NetworkAssetLoader(context, uniqueId),
|
|
||||||
new MemoryAssetLoader(),
|
|
||||||
new DiskAssetLoader(context.getCacheDir()));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void cancelQueuedOperations() {
|
TrustManager[] trustAllCerts = new TrustManager[] {
|
||||||
loader.cancelForegroundLoads();
|
new X509TrustManager() {
|
||||||
loader.cancelBackgroundLoads();
|
public X509Certificate[] getAcceptedIssuers() {
|
||||||
loader.freeCacheMemory();
|
return new X509Certificate[0];
|
||||||
}
|
}
|
||||||
|
public void checkClientTrusted(X509Certificate[] certs, String authType) {}
|
||||||
|
public void checkServerTrusted(X509Certificate[] certs, String authType) {}
|
||||||
|
}};
|
||||||
|
|
||||||
|
KeyManager[] ourKeyman = new KeyManager[] {
|
||||||
|
new X509KeyManager() {
|
||||||
|
public String chooseClientAlias(String[] keyTypes,
|
||||||
|
Principal[] issuers, Socket socket) {
|
||||||
|
return "Limelight-RSA";
|
||||||
|
}
|
||||||
|
|
||||||
|
public String chooseServerAlias(String keyType, Principal[] issuers,
|
||||||
|
Socket socket) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public X509Certificate[] getCertificateChain(String alias) {
|
||||||
|
return new X509Certificate[] {cryptoProvider.getClientCertificate()};
|
||||||
|
}
|
||||||
|
|
||||||
|
public String[] getClientAliases(String keyType, Principal[] issuers) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public PrivateKey getPrivateKey(String alias) {
|
||||||
|
return cryptoProvider.getClientPrivateKey();
|
||||||
|
}
|
||||||
|
|
||||||
|
public String[] getServerAliases(String keyType, Principal[] issuers) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Ignore differences between given hostname and certificate hostname
|
||||||
|
HostnameVerifier hv = new HostnameVerifier() {
|
||||||
|
public boolean verify(String hostname, SSLSession session) { return true; }
|
||||||
|
};
|
||||||
|
|
||||||
private void sortList() {
|
private void sortList() {
|
||||||
Collections.sort(itemList, new Comparator<AppView.AppObject>() {
|
Collections.sort(itemList, new Comparator<AppView.AppObject>() {
|
||||||
@Override
|
@Override
|
||||||
public int compare(AppView.AppObject lhs, AppView.AppObject rhs) {
|
public int compare(AppView.AppObject lhs, AppView.AppObject rhs) {
|
||||||
return lhs.app.getAppName().toLowerCase().compareTo(rhs.app.getAppName().toLowerCase());
|
return lhs.app.getAppName().compareTo(rhs.app.getAppName());
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public void addApp(AppView.AppObject app) {
|
private InetAddress getCurrentAddress() {
|
||||||
// Queue a request to fetch this bitmap into cache
|
if (computer.reachability == ComputerDetails.Reachability.LOCAL) {
|
||||||
loader.queueCacheLoad(app.app);
|
return computer.localIp;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return computer.remoteIp;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Add the app to our sorted list
|
public void addApp(AppView.AppObject app) {
|
||||||
itemList.add(app);
|
itemList.add(app);
|
||||||
sortList();
|
sortList();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void removeApp(AppView.AppObject app) {
|
public void abortPendingRequests() {
|
||||||
itemList.remove(app);
|
HashMap<ImageView, Future> tempMap;
|
||||||
|
|
||||||
|
synchronized (pendingRequests) {
|
||||||
|
// Copy the pending requests under a lock
|
||||||
|
tempMap = new HashMap<ImageView, Future>(pendingRequests);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (Future f : tempMap.values()) {
|
||||||
|
if (!f.isCancelled() && !f.isDone()) {
|
||||||
|
f.cancel(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
synchronized (pendingRequests) {
|
||||||
|
// Remove cancelled requests
|
||||||
|
for (ImageView v : tempMap.keySet()) {
|
||||||
|
pendingRequests.remove(v);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Handle pruning of bitmap cache
|
||||||
|
private void populateBitmapCache(UUID uuid, int appId, Bitmap bitmap) {
|
||||||
|
try {
|
||||||
|
// PNG ignores quality setting
|
||||||
|
FileOutputStream out = CacheHelper.openCacheFileForOutput(context.getCacheDir(), "boxart", uuid.toString(), appId+".png");
|
||||||
|
bitmap.compress(Bitmap.CompressFormat.PNG, 0, out);
|
||||||
|
out.close();
|
||||||
|
} catch (IOException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean populateImageView(ImageView imgView, ProgressBar prgView, AppView.AppObject obj) {
|
public boolean populateImageView(final ImageView imgView, final AppView.AppObject obj) {
|
||||||
// Let the cached asset loader handle it
|
// Hide the image view while we're loading the image from disk cache
|
||||||
loader.populateImageView(obj.app, imgView, prgView);
|
imgView.setVisibility(View.INVISIBLE);
|
||||||
|
|
||||||
|
// Check the on-disk cache
|
||||||
|
new ImageCacheRequest(imgView, obj.app.getAppId()).execute();
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -101,13 +192,93 @@ public class AppGridAdapter extends GenericGridAdapter<AppView.AppObject> {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean populateOverlayView(ImageView overlayView, AppView.AppObject obj) {
|
public boolean populateOverlayView(ImageView overlayView, AppView.AppObject obj) {
|
||||||
if (obj.isRunning) {
|
if (obj.app.getIsRunning()) {
|
||||||
// Show the play button overlay
|
// Show the play button overlay
|
||||||
overlayView.setImageResource(R.drawable.ic_play);
|
overlayView.setImageResource(R.drawable.play);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// No overlay
|
// No overlay
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private class ImageCacheRequest extends AsyncTask<Void, Void, Bitmap> {
|
||||||
|
private ImageView view;
|
||||||
|
private int appId;
|
||||||
|
|
||||||
|
public ImageCacheRequest(ImageView view, int appId) {
|
||||||
|
this.view = view;
|
||||||
|
this.appId = appId;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Bitmap doInBackground(Void... v) {
|
||||||
|
InputStream in = null;
|
||||||
|
try {
|
||||||
|
in = CacheHelper.openCacheFileForInput(context.getCacheDir(), "boxart", computer.uuid.toString(), appId + ".png");
|
||||||
|
return BitmapFactory.decodeStream(in);
|
||||||
|
} catch (IOException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
} finally {
|
||||||
|
if (in != null) {
|
||||||
|
try {
|
||||||
|
in.close();
|
||||||
|
} catch (IOException e) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onPostExecute(Bitmap result) {
|
||||||
|
if (result != null) {
|
||||||
|
// Disk cache was read successfully
|
||||||
|
LimeLog.info("Image disk cache hit for ("+computer.uuid+", "+appId+")");
|
||||||
|
view.setImageBitmap(result);
|
||||||
|
view.setVisibility(View.VISIBLE);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
LimeLog.info("Image disk cache miss for ("+computer.uuid+", "+appId+")");
|
||||||
|
LimeLog.info("Requesting: "+"https://" + getCurrentAddress().getHostAddress() + ":47984/appasset?uniqueid=" + uniqueId + "&appid=" +
|
||||||
|
appId + "&AssetType=2&AssetIdx=0");
|
||||||
|
|
||||||
|
// Load the placeholder image
|
||||||
|
view.setImageResource(defaultImageRes);
|
||||||
|
view.setVisibility(View.VISIBLE);
|
||||||
|
|
||||||
|
// Set SSL contexts correctly to allow us to authenticate
|
||||||
|
Ion.getDefault(context).getHttpClient().getSSLSocketMiddleware().setTrustManagers(trustAllCerts);
|
||||||
|
Ion.getDefault(context).getHttpClient().getSSLSocketMiddleware().setSSLContext(sslContext);
|
||||||
|
|
||||||
|
// Kick off the deferred image load
|
||||||
|
synchronized (pendingRequests) {
|
||||||
|
Future<Bitmap> f = Ion.with(context)
|
||||||
|
.load("https://" + getCurrentAddress().getHostAddress() + ":47984/appasset?uniqueid=" + uniqueId + "&appid=" +
|
||||||
|
appId + "&AssetType=2&AssetIdx=0")
|
||||||
|
.asBitmap()
|
||||||
|
.setCallback(new FutureCallback<Bitmap>() {
|
||||||
|
@Override
|
||||||
|
public void onCompleted(Exception e, Bitmap result) {
|
||||||
|
synchronized (pendingRequests) {
|
||||||
|
pendingRequests.remove(view);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result != null) {
|
||||||
|
// Make the view visible now
|
||||||
|
view.setImageBitmap(result);
|
||||||
|
view.setVisibility(View.VISIBLE);
|
||||||
|
|
||||||
|
// Populate the disk cache if we got an image back
|
||||||
|
populateBitmapCache(computer.uuid, appId, result);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// Leave the loading icon as is (probably should change this eventually...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
pendingRequests.put(view, f);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
}
|
}
|
||||||
@@ -6,7 +6,6 @@ import android.view.View;
|
|||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
import android.widget.BaseAdapter;
|
import android.widget.BaseAdapter;
|
||||||
import android.widget.ImageView;
|
import android.widget.ImageView;
|
||||||
import android.widget.ProgressBar;
|
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
|
|
||||||
import com.limelight.R;
|
import com.limelight.R;
|
||||||
@@ -14,14 +13,16 @@ import com.limelight.R;
|
|||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
|
||||||
public abstract class GenericGridAdapter<T> extends BaseAdapter {
|
public abstract class GenericGridAdapter<T> extends BaseAdapter {
|
||||||
protected final Context context;
|
protected Context context;
|
||||||
protected final int layoutId;
|
protected int defaultImageRes;
|
||||||
protected final ArrayList<T> itemList = new ArrayList<>();
|
protected int layoutId;
|
||||||
protected final LayoutInflater inflater;
|
protected ArrayList<T> itemList = new ArrayList<T>();
|
||||||
|
protected LayoutInflater inflater;
|
||||||
|
|
||||||
public GenericGridAdapter(Context context, int layoutId) {
|
public GenericGridAdapter(Context context, int layoutId, int defaultImageRes) {
|
||||||
this.context = context;
|
this.context = context;
|
||||||
this.layoutId = layoutId;
|
this.layoutId = layoutId;
|
||||||
|
this.defaultImageRes = defaultImageRes;
|
||||||
|
|
||||||
this.inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
|
this.inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
|
||||||
}
|
}
|
||||||
@@ -45,7 +46,7 @@ public abstract class GenericGridAdapter<T> extends BaseAdapter {
|
|||||||
return i;
|
return i;
|
||||||
}
|
}
|
||||||
|
|
||||||
public abstract boolean populateImageView(ImageView imgView, ProgressBar prgView, T obj);
|
public abstract boolean populateImageView(ImageView imgView, T obj);
|
||||||
public abstract boolean populateTextView(TextView txtView, T obj);
|
public abstract boolean populateTextView(TextView txtView, T obj);
|
||||||
public abstract boolean populateOverlayView(ImageView overlayView, T obj);
|
public abstract boolean populateOverlayView(ImageView overlayView, T obj);
|
||||||
|
|
||||||
@@ -55,14 +56,13 @@ public abstract class GenericGridAdapter<T> extends BaseAdapter {
|
|||||||
convertView = inflater.inflate(layoutId, viewGroup, false);
|
convertView = inflater.inflate(layoutId, viewGroup, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
ImageView imgView = convertView.findViewById(R.id.grid_image);
|
ImageView imgView = (ImageView) convertView.findViewById(R.id.grid_image);
|
||||||
ImageView overlayView = convertView.findViewById(R.id.grid_overlay);
|
ImageView overlayView = (ImageView) convertView.findViewById(R.id.grid_overlay);
|
||||||
TextView txtView = convertView.findViewById(R.id.grid_text);
|
TextView txtView = (TextView) convertView.findViewById(R.id.grid_text);
|
||||||
ProgressBar prgView = convertView.findViewById(R.id.grid_spinner);
|
|
||||||
|
|
||||||
if (imgView != null) {
|
if (imgView != null) {
|
||||||
if (!populateImageView(imgView, prgView, itemList.get(i))) {
|
if (!populateImageView(imgView, itemList.get(i))) {
|
||||||
imgView.setImageBitmap(null);
|
imgView.setImageResource(defaultImageRes);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!populateTextView(txtView, itemList.get(i))) {
|
if (!populateTextView(txtView, itemList.get(i))) {
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
package com.limelight.grid;
|
package com.limelight.grid;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.view.View;
|
|
||||||
import android.widget.ImageView;
|
import android.widget.ImageView;
|
||||||
import android.widget.ProgressBar;
|
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
|
|
||||||
import com.limelight.PcView;
|
import com.limelight.PcView;
|
||||||
@@ -16,7 +14,7 @@ import java.util.Comparator;
|
|||||||
public class PcGridAdapter extends GenericGridAdapter<PcView.ComputerObject> {
|
public class PcGridAdapter extends GenericGridAdapter<PcView.ComputerObject> {
|
||||||
|
|
||||||
public PcGridAdapter(Context context, boolean listMode, boolean small) {
|
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));
|
super(context, listMode ? R.layout.simple_row : (small ? R.layout.pc_grid_item_small : R.layout.pc_grid_item), R.drawable.computer);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void addComputer(PcView.ComputerObject computer) {
|
public void addComputer(PcView.ComputerObject computer) {
|
||||||
@@ -28,7 +26,7 @@ public class PcGridAdapter extends GenericGridAdapter<PcView.ComputerObject> {
|
|||||||
Collections.sort(itemList, new Comparator<PcView.ComputerObject>() {
|
Collections.sort(itemList, new Comparator<PcView.ComputerObject>() {
|
||||||
@Override
|
@Override
|
||||||
public int compare(PcView.ComputerObject lhs, PcView.ComputerObject rhs) {
|
public int compare(PcView.ComputerObject lhs, PcView.ComputerObject rhs) {
|
||||||
return lhs.details.name.toLowerCase().compareTo(rhs.details.name.toLowerCase());
|
return lhs.details.name.compareTo(rhs.details.name);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -38,28 +36,21 @@ public class PcGridAdapter extends GenericGridAdapter<PcView.ComputerObject> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean populateImageView(ImageView imgView, ProgressBar prgView, PcView.ComputerObject obj) {
|
public boolean populateImageView(ImageView imgView, PcView.ComputerObject obj) {
|
||||||
if (obj.details.state == ComputerDetails.State.ONLINE) {
|
if (obj.details.reachability != ComputerDetails.Reachability.OFFLINE) {
|
||||||
imgView.setAlpha(1.0f);
|
imgView.setAlpha(1.0f);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
imgView.setAlpha(0.4f);
|
imgView.setAlpha(0.4f);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (obj.details.reachability == ComputerDetails.Reachability.UNKNOWN) {
|
// Return false to use the default drawable
|
||||||
prgView.setVisibility(View.VISIBLE);
|
return false;
|
||||||
}
|
|
||||||
else {
|
|
||||||
prgView.setVisibility(View.INVISIBLE);
|
|
||||||
}
|
|
||||||
|
|
||||||
imgView.setImageResource(R.drawable.ic_computer);
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean populateTextView(TextView txtView, PcView.ComputerObject obj) {
|
public boolean populateTextView(TextView txtView, PcView.ComputerObject obj) {
|
||||||
if (obj.details.state == ComputerDetails.State.ONLINE) {
|
if (obj.details.reachability != ComputerDetails.Reachability.OFFLINE) {
|
||||||
txtView.setAlpha(1.0f);
|
txtView.setAlpha(1.0f);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
@@ -72,11 +63,13 @@ public class PcGridAdapter extends GenericGridAdapter<PcView.ComputerObject> {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean populateOverlayView(ImageView overlayView, PcView.ComputerObject obj) {
|
public boolean populateOverlayView(ImageView overlayView, PcView.ComputerObject obj) {
|
||||||
if (obj.details.state == ComputerDetails.State.OFFLINE) {
|
if (obj.details.reachability == ComputerDetails.Reachability.UNKNOWN) {
|
||||||
overlayView.setImageResource(R.drawable.ic_pc_offline);
|
// Still refreshing this PC so display the overlay
|
||||||
overlayView.setAlpha(0.4f);
|
overlayView.setImageResource(R.drawable.image_loading);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// No overlay
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,357 +0,0 @@
|
|||||||
package com.limelight.grid.assets;
|
|
||||||
|
|
||||||
import android.content.res.Resources;
|
|
||||||
import android.graphics.Bitmap;
|
|
||||||
import android.graphics.drawable.BitmapDrawable;
|
|
||||||
import android.graphics.drawable.Drawable;
|
|
||||||
import android.os.AsyncTask;
|
|
||||||
import android.view.View;
|
|
||||||
import android.widget.ImageView;
|
|
||||||
import android.widget.ProgressBar;
|
|
||||||
|
|
||||||
import com.limelight.nvstream.http.ComputerDetails;
|
|
||||||
import com.limelight.nvstream.http.NvApp;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.InputStream;
|
|
||||||
import java.lang.ref.WeakReference;
|
|
||||||
import java.util.concurrent.LinkedBlockingQueue;
|
|
||||||
import java.util.concurrent.ThreadPoolExecutor;
|
|
||||||
import java.util.concurrent.TimeUnit;
|
|
||||||
|
|
||||||
public class CachedAppAssetLoader {
|
|
||||||
private static final int MAX_CONCURRENT_DISK_LOADS = 3;
|
|
||||||
private static final int MAX_CONCURRENT_NETWORK_LOADS = 3;
|
|
||||||
private static final int MAX_CONCURRENT_CACHE_LOADS = 1;
|
|
||||||
|
|
||||||
private static final int MAX_PENDING_CACHE_LOADS = 100;
|
|
||||||
private static final int MAX_PENDING_NETWORK_LOADS = 40;
|
|
||||||
private static final int MAX_PENDING_DISK_LOADS = 40;
|
|
||||||
|
|
||||||
private final ThreadPoolExecutor cacheExecutor = new ThreadPoolExecutor(
|
|
||||||
MAX_CONCURRENT_CACHE_LOADS, MAX_CONCURRENT_CACHE_LOADS,
|
|
||||||
Long.MAX_VALUE, TimeUnit.DAYS,
|
|
||||||
new LinkedBlockingQueue<Runnable>(MAX_PENDING_CACHE_LOADS),
|
|
||||||
new ThreadPoolExecutor.DiscardOldestPolicy());
|
|
||||||
|
|
||||||
private final ThreadPoolExecutor foregroundExecutor = new ThreadPoolExecutor(
|
|
||||||
MAX_CONCURRENT_DISK_LOADS, MAX_CONCURRENT_DISK_LOADS,
|
|
||||||
Long.MAX_VALUE, TimeUnit.DAYS,
|
|
||||||
new LinkedBlockingQueue<Runnable>(MAX_PENDING_DISK_LOADS),
|
|
||||||
new ThreadPoolExecutor.DiscardOldestPolicy());
|
|
||||||
|
|
||||||
private final ThreadPoolExecutor networkExecutor = new ThreadPoolExecutor(
|
|
||||||
MAX_CONCURRENT_NETWORK_LOADS, MAX_CONCURRENT_NETWORK_LOADS,
|
|
||||||
Long.MAX_VALUE, TimeUnit.DAYS,
|
|
||||||
new LinkedBlockingQueue<Runnable>(MAX_PENDING_NETWORK_LOADS),
|
|
||||||
new ThreadPoolExecutor.DiscardOldestPolicy());
|
|
||||||
|
|
||||||
private final ComputerDetails computer;
|
|
||||||
private final double scalingDivider;
|
|
||||||
private final NetworkAssetLoader networkLoader;
|
|
||||||
private final MemoryAssetLoader memoryLoader;
|
|
||||||
private final DiskAssetLoader diskLoader;
|
|
||||||
private final Bitmap placeholderBitmap;
|
|
||||||
|
|
||||||
public CachedAppAssetLoader(ComputerDetails computer, double scalingDivider,
|
|
||||||
NetworkAssetLoader networkLoader, MemoryAssetLoader memoryLoader,
|
|
||||||
DiskAssetLoader diskLoader) {
|
|
||||||
this.computer = computer;
|
|
||||||
this.scalingDivider = scalingDivider;
|
|
||||||
this.networkLoader = networkLoader;
|
|
||||||
this.memoryLoader = memoryLoader;
|
|
||||||
this.diskLoader = diskLoader;
|
|
||||||
this.placeholderBitmap = Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void cancelBackgroundLoads() {
|
|
||||||
Runnable r;
|
|
||||||
while ((r = cacheExecutor.getQueue().poll()) != null) {
|
|
||||||
cacheExecutor.remove(r);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void cancelForegroundLoads() {
|
|
||||||
Runnable r;
|
|
||||||
|
|
||||||
while ((r = foregroundExecutor.getQueue().poll()) != null) {
|
|
||||||
foregroundExecutor.remove(r);
|
|
||||||
}
|
|
||||||
|
|
||||||
while ((r = networkExecutor.getQueue().poll()) != null) {
|
|
||||||
networkExecutor.remove(r);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void freeCacheMemory() {
|
|
||||||
memoryLoader.clearCache();
|
|
||||||
}
|
|
||||||
|
|
||||||
private Bitmap doNetworkAssetLoad(LoaderTuple tuple, LoaderTask task) {
|
|
||||||
// Try 3 times
|
|
||||||
for (int i = 0; i < 3; i++) {
|
|
||||||
// Check again whether we've been cancelled or the image view is gone
|
|
||||||
if (task != null && (task.isCancelled() || task.imageViewRef.get() == null)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
InputStream in = networkLoader.getBitmapStream(tuple);
|
|
||||||
if (in != null) {
|
|
||||||
// Write the stream straight to disk
|
|
||||||
diskLoader.populateCacheWithStream(tuple, in);
|
|
||||||
|
|
||||||
// Close the network input stream
|
|
||||||
try {
|
|
||||||
in.close();
|
|
||||||
} catch (IOException ignored) {}
|
|
||||||
|
|
||||||
// If there's a task associated with this load, we should return the bitmap
|
|
||||||
if (task != null) {
|
|
||||||
// If the cached bitmap is valid, return it. Otherwise, we'll try the load again
|
|
||||||
Bitmap bmp = diskLoader.loadBitmapFromCache(tuple, (int) scalingDivider);
|
|
||||||
if (bmp != null) {
|
|
||||||
return bmp;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
// Otherwise it's a background load and we return nothing
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Wait 1 second with a bit of fuzz
|
|
||||||
try {
|
|
||||||
Thread.sleep((int) (1000 + (Math.random() * 500)));
|
|
||||||
} catch (InterruptedException e) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private class LoaderTask extends AsyncTask<LoaderTuple, Void, Bitmap> {
|
|
||||||
private final WeakReference<ImageView> imageViewRef;
|
|
||||||
private final WeakReference<ProgressBar> progressViewRef;
|
|
||||||
private final boolean diskOnly;
|
|
||||||
|
|
||||||
private LoaderTuple tuple;
|
|
||||||
|
|
||||||
public LoaderTask(ImageView imageView, ProgressBar prgView, boolean diskOnly) {
|
|
||||||
this.imageViewRef = new WeakReference<>(imageView);
|
|
||||||
this.progressViewRef = new WeakReference<>(prgView);
|
|
||||||
this.diskOnly = diskOnly;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected Bitmap doInBackground(LoaderTuple... params) {
|
|
||||||
tuple = params[0];
|
|
||||||
|
|
||||||
// Check whether it has been cancelled or the views are gone
|
|
||||||
if (isCancelled() || imageViewRef.get() == null || progressViewRef.get() == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
Bitmap bmp = diskLoader.loadBitmapFromCache(tuple, (int) scalingDivider);
|
|
||||||
if (bmp == null) {
|
|
||||||
if (!diskOnly) {
|
|
||||||
// Try to load the asset from the network
|
|
||||||
bmp = doNetworkAssetLoad(tuple, this);
|
|
||||||
} else {
|
|
||||||
// Report progress to display the placeholder and spin
|
|
||||||
// off the network-capable task
|
|
||||||
publishProgress();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Cache the bitmap
|
|
||||||
if (bmp != null) {
|
|
||||||
memoryLoader.populateCache(tuple, bmp);
|
|
||||||
}
|
|
||||||
|
|
||||||
return bmp;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onProgressUpdate(Void... nothing) {
|
|
||||||
// Do nothing if cancelled
|
|
||||||
if (isCancelled()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the current loader task for this view isn't us, do nothing
|
|
||||||
final ImageView imageView = imageViewRef.get();
|
|
||||||
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, prgView, false);
|
|
||||||
AsyncDrawable asyncDrawable = new AsyncDrawable(imageView.getResources(), placeholderBitmap, task);
|
|
||||||
imageView.setVisibility(View.VISIBLE);
|
|
||||||
imageView.setImageDrawable(asyncDrawable);
|
|
||||||
task.executeOnExecutor(networkExecutor, tuple);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onPostExecute(Bitmap bitmap) {
|
|
||||||
// Do nothing if cancelled
|
|
||||||
if (isCancelled()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
final ImageView imageView = imageViewRef.get();
|
|
||||||
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.setVisibility(View.VISIBLE);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static class AsyncDrawable extends BitmapDrawable {
|
|
||||||
private final WeakReference<LoaderTask> loaderTaskReference;
|
|
||||||
|
|
||||||
public AsyncDrawable(Resources res, Bitmap bitmap,
|
|
||||||
LoaderTask loaderTask) {
|
|
||||||
super(res, bitmap);
|
|
||||||
loaderTaskReference = new WeakReference<>(loaderTask);
|
|
||||||
}
|
|
||||||
|
|
||||||
public LoaderTask getLoaderTask() {
|
|
||||||
return loaderTaskReference.get();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static LoaderTask getLoaderTask(ImageView imageView) {
|
|
||||||
if (imageView == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
final Drawable drawable = imageView.getDrawable();
|
|
||||||
|
|
||||||
// If our drawable is in play, get the loader task
|
|
||||||
if (drawable instanceof AsyncDrawable) {
|
|
||||||
final AsyncDrawable asyncDrawable = (AsyncDrawable) drawable;
|
|
||||||
return asyncDrawable.getLoaderTask();
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static boolean cancelPendingLoad(LoaderTuple tuple, ImageView imageView) {
|
|
||||||
final LoaderTask loaderTask = getLoaderTask(imageView);
|
|
||||||
|
|
||||||
// Check if any task was pending for this image view
|
|
||||||
if (loaderTask != null && !loaderTask.isCancelled()) {
|
|
||||||
final LoaderTuple taskTuple = loaderTask.tuple;
|
|
||||||
|
|
||||||
// Cancel the task if it's not already loading the same data
|
|
||||||
if (taskTuple == null || !taskTuple.equals(tuple)) {
|
|
||||||
loaderTask.cancel(true);
|
|
||||||
} else {
|
|
||||||
// It's already loading what we want
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Allow the load to proceed
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void queueCacheLoad(NvApp app) {
|
|
||||||
final LoaderTuple tuple = new LoaderTuple(computer, app);
|
|
||||||
|
|
||||||
if (memoryLoader.loadBitmapFromCache(tuple) != null) {
|
|
||||||
// It's in memory which means it must also be on disk
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Queue a fetch in the cache executor
|
|
||||||
cacheExecutor.execute(new Runnable() {
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
// Check if the image is cached on disk
|
|
||||||
if (diskLoader.checkCacheExists(tuple)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Try to load the asset from the network and cache result on disk
|
|
||||||
doNetworkAssetLoad(tuple, null);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public 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, 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
|
|
||||||
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(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 {
|
|
||||||
public final ComputerDetails computer;
|
|
||||||
public final NvApp app;
|
|
||||||
|
|
||||||
public LoaderTuple(ComputerDetails computer, NvApp app) {
|
|
||||||
this.computer = computer;
|
|
||||||
this.app = app;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean equals(Object o) {
|
|
||||||
if (!(o instanceof LoaderTuple)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
LoaderTuple other = (LoaderTuple) o;
|
|
||||||
return computer.uuid.equals(other.computer.uuid) && app.getAppId() == other.app.getAppId();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String toString() {
|
|
||||||
return "("+computer.uuid+", "+app.getAppId()+")";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,82 +0,0 @@
|
|||||||
package com.limelight.grid.assets;
|
|
||||||
|
|
||||||
import android.graphics.Bitmap;
|
|
||||||
import android.graphics.BitmapFactory;
|
|
||||||
|
|
||||||
import com.limelight.LimeLog;
|
|
||||||
import com.limelight.utils.CacheHelper;
|
|
||||||
|
|
||||||
import java.io.File;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.InputStream;
|
|
||||||
import java.io.OutputStream;
|
|
||||||
|
|
||||||
public class DiskAssetLoader {
|
|
||||||
// 5 MB
|
|
||||||
private final long MAX_ASSET_SIZE = 5 * 1024 * 1024;
|
|
||||||
|
|
||||||
private final File cacheDir;
|
|
||||||
|
|
||||||
public DiskAssetLoader(File cacheDir) {
|
|
||||||
this.cacheDir = cacheDir;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean checkCacheExists(CachedAppAssetLoader.LoaderTuple tuple) {
|
|
||||||
return CacheHelper.cacheFileExists(cacheDir, "boxart", tuple.computer.uuid.toString(), tuple.app.getAppId() + ".png");
|
|
||||||
}
|
|
||||||
|
|
||||||
public Bitmap loadBitmapFromCache(CachedAppAssetLoader.LoaderTuple tuple, int sampleSize) {
|
|
||||||
InputStream in = null;
|
|
||||||
Bitmap bmp = null;
|
|
||||||
try {
|
|
||||||
// Make sure the cached asset doesn't exceed the maximum size
|
|
||||||
if (CacheHelper.getFileSize(cacheDir, "boxart", tuple.computer.uuid.toString(), tuple.app.getAppId() + ".png") > MAX_ASSET_SIZE) {
|
|
||||||
LimeLog.warning("Removing cached tuple exceeding size threshold: "+tuple);
|
|
||||||
CacheHelper.deleteCacheFile(cacheDir, "boxart", tuple.computer.uuid.toString(), tuple.app.getAppId() + ".png");
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
in = CacheHelper.openCacheFileForInput(cacheDir, "boxart", tuple.computer.uuid.toString(), tuple.app.getAppId() + ".png");
|
|
||||||
BitmapFactory.Options options = new BitmapFactory.Options();
|
|
||||||
options.inSampleSize = sampleSize;
|
|
||||||
options.inPreferredConfig = Bitmap.Config.RGB_565;
|
|
||||||
bmp = BitmapFactory.decodeStream(in, null, options);
|
|
||||||
} catch (IOException ignored) {
|
|
||||||
} finally {
|
|
||||||
if (in != null) {
|
|
||||||
try {
|
|
||||||
in.close();
|
|
||||||
} catch (IOException ignored) {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (bmp != null) {
|
|
||||||
LimeLog.info("Disk cache hit for tuple: "+tuple);
|
|
||||||
}
|
|
||||||
|
|
||||||
return bmp;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void populateCacheWithStream(CachedAppAssetLoader.LoaderTuple tuple, InputStream input) {
|
|
||||||
OutputStream out = null;
|
|
||||||
boolean success = false;
|
|
||||||
try {
|
|
||||||
out = CacheHelper.openCacheFileForOutput(cacheDir, "boxart", tuple.computer.uuid.toString(), tuple.app.getAppId() + ".png");
|
|
||||||
CacheHelper.writeInputStreamToOutputStream(input, out, MAX_ASSET_SIZE);
|
|
||||||
success = true;
|
|
||||||
} catch (IOException e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
} finally {
|
|
||||||
if (out != null) {
|
|
||||||
try {
|
|
||||||
out.close();
|
|
||||||
} catch (IOException ignored) {}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!success) {
|
|
||||||
LimeLog.warning("Unable to populate cache with tuple: "+tuple);
|
|
||||||
CacheHelper.deleteCacheFile(cacheDir, "boxart", tuple.computer.uuid.toString(), tuple.app.getAppId() + ".png");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
package com.limelight.grid.assets;
|
|
||||||
|
|
||||||
import android.graphics.Bitmap;
|
|
||||||
import android.util.LruCache;
|
|
||||||
|
|
||||||
import com.limelight.LimeLog;
|
|
||||||
|
|
||||||
public class MemoryAssetLoader {
|
|
||||||
private static final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
|
|
||||||
private static final LruCache<String, Bitmap> memoryCache = new LruCache<String, Bitmap>(maxMemory / 16) {
|
|
||||||
@Override
|
|
||||||
protected int sizeOf(String key, Bitmap bitmap) {
|
|
||||||
// Sizeof returns kilobytes
|
|
||||||
return bitmap.getByteCount() / 1024;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
private static String constructKey(CachedAppAssetLoader.LoaderTuple tuple) {
|
|
||||||
return tuple.computer.uuid.toString()+"-"+tuple.app.getAppId();
|
|
||||||
}
|
|
||||||
|
|
||||||
public Bitmap loadBitmapFromCache(CachedAppAssetLoader.LoaderTuple tuple) {
|
|
||||||
Bitmap bmp = memoryCache.get(constructKey(tuple));
|
|
||||||
if (bmp != null) {
|
|
||||||
LimeLog.info("Memory cache hit for tuple: "+tuple);
|
|
||||||
}
|
|
||||||
return bmp;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void populateCache(CachedAppAssetLoader.LoaderTuple tuple, Bitmap bitmap) {
|
|
||||||
memoryCache.put(constructKey(tuple), bitmap);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void clearCache() {
|
|
||||||
memoryCache.evictAll();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
package com.limelight.grid.assets;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
|
|
||||||
import com.limelight.LimeLog;
|
|
||||||
import com.limelight.binding.PlatformBinding;
|
|
||||||
import com.limelight.nvstream.http.NvHTTP;
|
|
||||||
import com.limelight.utils.ServerHelper;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.InputStream;
|
|
||||||
|
|
||||||
public class NetworkAssetLoader {
|
|
||||||
private final Context context;
|
|
||||||
private final String uniqueId;
|
|
||||||
|
|
||||||
public NetworkAssetLoader(Context context, String uniqueId) {
|
|
||||||
this.context = context;
|
|
||||||
this.uniqueId = uniqueId;
|
|
||||||
}
|
|
||||||
|
|
||||||
public InputStream getBitmapStream(CachedAppAssetLoader.LoaderTuple tuple) {
|
|
||||||
InputStream in = null;
|
|
||||||
try {
|
|
||||||
NvHTTP http = new NvHTTP(ServerHelper.getCurrentAddressFromComputer(tuple.computer), uniqueId, null, PlatformBinding.getCryptoProvider(context));
|
|
||||||
in = http.getBoxArt(tuple.app);
|
|
||||||
} catch (IOException ignored) {}
|
|
||||||
|
|
||||||
if (in != null) {
|
|
||||||
LimeLog.info("Network asset load complete: " + tuple);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
LimeLog.info("Network asset load failed: " + tuple);
|
|
||||||
}
|
|
||||||
|
|
||||||
return in;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
package com.limelight.nvstream.av.video.cpu;
|
||||||
|
|
||||||
|
public class AvcDecoder {
|
||||||
|
static {
|
||||||
|
// FFMPEG dependencies
|
||||||
|
System.loadLibrary("avutil-52");
|
||||||
|
System.loadLibrary("swresample-0");
|
||||||
|
System.loadLibrary("swscale-2");
|
||||||
|
System.loadLibrary("avcodec-55");
|
||||||
|
System.loadLibrary("avformat-55");
|
||||||
|
|
||||||
|
System.loadLibrary("nv_avc_dec");
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Disables the deblocking filter at the cost of image quality */
|
||||||
|
public static final int DISABLE_LOOP_FILTER = 0x1;
|
||||||
|
/** Uses the low latency decode flag (disables multithreading) */
|
||||||
|
public static final int LOW_LATENCY_DECODE = 0x2;
|
||||||
|
/** Threads process each slice, rather than each frame */
|
||||||
|
public static final int SLICE_THREADING = 0x4;
|
||||||
|
/** Uses nonstandard speedup tricks */
|
||||||
|
public static final int FAST_DECODE = 0x8;
|
||||||
|
/** Uses bilinear filtering instead of bicubic */
|
||||||
|
public static final int BILINEAR_FILTERING = 0x10;
|
||||||
|
/** Uses a faster bilinear filtering with lower image quality */
|
||||||
|
public static final int FAST_BILINEAR_FILTERING = 0x20;
|
||||||
|
/** Disables color conversion (output is NV21) */
|
||||||
|
public static final int NO_COLOR_CONVERSION = 0x40;
|
||||||
|
|
||||||
|
public static native int init(int width, int height, int perflvl, int threadcount);
|
||||||
|
public static native void destroy();
|
||||||
|
|
||||||
|
// Rendering API when NO_COLOR_CONVERSION == 0
|
||||||
|
public static native boolean setRenderTarget(Object androidSurface);
|
||||||
|
public static native boolean getRgbFrameInt(int[] rgbFrame, int bufferSize);
|
||||||
|
public static native boolean getRgbFrame(byte[] rgbFrame, int bufferSize);
|
||||||
|
public static native boolean redraw();
|
||||||
|
|
||||||
|
// Rendering API when NO_COLOR_CONVERSION == 1
|
||||||
|
public static native boolean getRawFrame(byte[] yuvFrame, int bufferSize);
|
||||||
|
|
||||||
|
public static native int getInputPaddingSize();
|
||||||
|
public static native int decode(byte[] indata, int inoff, int inlen);
|
||||||
|
}
|
||||||
@@ -1,5 +1,8 @@
|
|||||||
package com.limelight.preferences;
|
package com.limelight.preferences;
|
||||||
|
|
||||||
|
import java.net.InetAddress;
|
||||||
|
import java.net.UnknownHostException;
|
||||||
|
import java.util.Locale;
|
||||||
import java.util.concurrent.LinkedBlockingQueue;
|
import java.util.concurrent.LinkedBlockingQueue;
|
||||||
|
|
||||||
import com.limelight.computers.ComputerManagerService;
|
import com.limelight.computers.ComputerManagerService;
|
||||||
@@ -11,128 +14,138 @@ import com.limelight.utils.UiHelper;
|
|||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
import android.app.Service;
|
import android.app.Service;
|
||||||
import android.content.ComponentName;
|
import android.content.ComponentName;
|
||||||
import android.content.Context;
|
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.content.ServiceConnection;
|
import android.content.ServiceConnection;
|
||||||
|
import android.content.res.Configuration;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.os.IBinder;
|
import android.os.IBinder;
|
||||||
import android.view.KeyEvent;
|
import android.view.KeyEvent;
|
||||||
import android.view.inputmethod.EditorInfo;
|
import android.view.inputmethod.EditorInfo;
|
||||||
import android.view.inputmethod.InputMethodManager;
|
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
import android.widget.Toast;
|
import android.widget.Toast;
|
||||||
|
|
||||||
public class AddComputerManually extends Activity {
|
public class AddComputerManually extends Activity {
|
||||||
private TextView hostText;
|
private TextView hostText;
|
||||||
private ComputerManagerService.ComputerManagerBinder managerBinder;
|
private ComputerManagerService.ComputerManagerBinder managerBinder;
|
||||||
private final LinkedBlockingQueue<String> computersToAdd = new LinkedBlockingQueue<>();
|
private LinkedBlockingQueue<String> computersToAdd = new LinkedBlockingQueue<String>();
|
||||||
private Thread addThread;
|
private Thread addThread;
|
||||||
private final ServiceConnection serviceConnection = new ServiceConnection() {
|
private ServiceConnection serviceConnection = new ServiceConnection() {
|
||||||
public void onServiceConnected(ComponentName className, final IBinder binder) {
|
public void onServiceConnected(ComponentName className, final IBinder binder) {
|
||||||
managerBinder = ((ComputerManagerService.ComputerManagerBinder)binder);
|
managerBinder = ((ComputerManagerService.ComputerManagerBinder)binder);
|
||||||
startAddThread();
|
startAddThread();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void onServiceDisconnected(ComponentName className) {
|
public void onServiceDisconnected(ComponentName className) {
|
||||||
joinAddThread();
|
joinAddThread();
|
||||||
managerBinder = null;
|
managerBinder = null;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
private void doAddPc(String host) {
|
private void doAddPc(String host) {
|
||||||
String msg;
|
String msg;
|
||||||
boolean finish = false;
|
boolean finish = false;
|
||||||
|
|
||||||
SpinnerDialog dialog = SpinnerDialog.displayDialog(this, getResources().getString(R.string.title_add_pc),
|
SpinnerDialog dialog = SpinnerDialog.displayDialog(this, getResources().getString(R.string.title_add_pc),
|
||||||
getResources().getString(R.string.msg_add_pc), false);
|
getResources().getString(R.string.msg_add_pc), false);
|
||||||
|
|
||||||
if (!managerBinder.addComputerBlocking(host, true)){
|
try {
|
||||||
msg = getResources().getString(R.string.addpc_fail);
|
InetAddress addr = InetAddress.getByName(host);
|
||||||
}
|
|
||||||
else {
|
if (!managerBinder.addComputerBlocking(addr)){
|
||||||
msg = getResources().getString(R.string.addpc_success);
|
msg = getResources().getString(R.string.addpc_fail);
|
||||||
finish = true;
|
}
|
||||||
}
|
else {
|
||||||
|
msg = getResources().getString(R.string.addpc_success);
|
||||||
|
finish = true;
|
||||||
|
}
|
||||||
|
} catch (UnknownHostException e) {
|
||||||
|
msg = getResources().getString(R.string.addpc_unknown_host);
|
||||||
|
}
|
||||||
|
|
||||||
dialog.dismiss();
|
dialog.dismiss();
|
||||||
|
|
||||||
final boolean toastFinish = finish;
|
final boolean toastFinish = finish;
|
||||||
final String toastMsg = msg;
|
final String toastMsg = msg;
|
||||||
AddComputerManually.this.runOnUiThread(new Runnable() {
|
AddComputerManually.this.runOnUiThread(new Runnable() {
|
||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
Toast.makeText(AddComputerManually.this, toastMsg, Toast.LENGTH_LONG).show();
|
Toast.makeText(AddComputerManually.this, toastMsg, Toast.LENGTH_LONG).show();
|
||||||
|
|
||||||
|
if (toastFinish && !isFinishing()) {
|
||||||
|
// Close the activity
|
||||||
|
AddComputerManually.this.finish();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void startAddThread() {
|
||||||
|
addThread = new Thread() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
while (!isInterrupted()) {
|
||||||
|
String computer;
|
||||||
|
|
||||||
|
try {
|
||||||
|
computer = computersToAdd.take();
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
doAddPc(computer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
addThread.setName("UI - AddComputerManually");
|
||||||
|
addThread.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void joinAddThread() {
|
||||||
|
if (addThread != null) {
|
||||||
|
addThread.interrupt();
|
||||||
|
|
||||||
|
try {
|
||||||
|
addThread.join();
|
||||||
|
} catch (InterruptedException ignored) {}
|
||||||
|
|
||||||
|
addThread = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (toastFinish && !isFinishing()) {
|
@Override
|
||||||
// Close the activity
|
protected void onStop() {
|
||||||
AddComputerManually.this.finish();
|
super.onStop();
|
||||||
}
|
|
||||||
}
|
Dialog.closeDialogs();
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private void startAddThread() {
|
|
||||||
addThread = new Thread() {
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
while (!isInterrupted()) {
|
|
||||||
String computer;
|
|
||||||
|
|
||||||
try {
|
|
||||||
computer = computersToAdd.take();
|
|
||||||
} catch (InterruptedException e) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
doAddPc(computer);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
addThread.setName("UI - AddComputerManually");
|
|
||||||
addThread.start();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void joinAddThread() {
|
|
||||||
if (addThread != null) {
|
|
||||||
addThread.interrupt();
|
|
||||||
|
|
||||||
try {
|
|
||||||
addThread.join();
|
|
||||||
} catch (InterruptedException ignored) {}
|
|
||||||
|
|
||||||
addThread = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onStop() {
|
|
||||||
super.onStop();
|
|
||||||
|
|
||||||
Dialog.closeDialogs();
|
|
||||||
SpinnerDialog.closeDialogs(this);
|
SpinnerDialog.closeDialogs(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onDestroy() {
|
protected void onDestroy() {
|
||||||
super.onDestroy();
|
super.onDestroy();
|
||||||
|
|
||||||
if (managerBinder != null) {
|
if (managerBinder != null) {
|
||||||
joinAddThread();
|
joinAddThread();
|
||||||
unbindService(serviceConnection);
|
unbindService(serviceConnection);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onCreate(Bundle savedInstanceState) {
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
|
|
||||||
UiHelper.setLocale(this);
|
String locale = PreferenceConfiguration.readPreferences(this).language;
|
||||||
|
if (!locale.equals(PreferenceConfiguration.DEFAULT_LANGUAGE)) {
|
||||||
setContentView(R.layout.activity_add_computer_manually);
|
Configuration config = new Configuration(getResources().getConfiguration());
|
||||||
|
config.locale = new Locale(locale);
|
||||||
|
getResources().updateConfiguration(config, getResources().getDisplayMetrics());
|
||||||
|
}
|
||||||
|
|
||||||
|
setContentView(R.layout.activity_add_computer_manually);
|
||||||
|
|
||||||
UiHelper.notifyNewRootView(this);
|
UiHelper.notifyNewRootView(this);
|
||||||
|
|
||||||
this.hostText = findViewById(R.id.hostTextView);
|
this.hostText = (TextView) findViewById(R.id.hostTextView);
|
||||||
hostText.setImeOptions(EditorInfo.IME_ACTION_DONE);
|
hostText.setImeOptions(EditorInfo.IME_ACTION_DONE);
|
||||||
hostText.setOnEditorActionListener(new TextView.OnEditorActionListener() {
|
hostText.setOnEditorActionListener(new TextView.OnEditorActionListener() {
|
||||||
@Override
|
@Override
|
||||||
@@ -148,19 +161,13 @@ public class AddComputerManually extends Activity {
|
|||||||
|
|
||||||
computersToAdd.add(hostText.getText().toString().trim());
|
computersToAdd.add(hostText.getText().toString().trim());
|
||||||
}
|
}
|
||||||
else if (actionId == EditorInfo.IME_ACTION_PREVIOUS) {
|
|
||||||
// This is how the Fire TV dismisses the keyboard
|
|
||||||
InputMethodManager imm = (InputMethodManager)getSystemService(Context.INPUT_METHOD_SERVICE);
|
|
||||||
imm.hideSoftInputFromWindow(hostText.getWindowToken(), 0);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Bind to the ComputerManager service
|
// Bind to the ComputerManager service
|
||||||
bindService(new Intent(AddComputerManually.this,
|
bindService(new Intent(AddComputerManually.this,
|
||||||
ComputerManagerService.class), serviceConnection, Service.BIND_AUTO_CREATE);
|
ComputerManagerService.class), serviceConnection, Service.BIND_AUTO_CREATE);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2,12 +2,13 @@ package com.limelight.preferences;
|
|||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.SharedPreferences;
|
import android.content.SharedPreferences;
|
||||||
import android.content.pm.PackageManager;
|
|
||||||
import android.os.Build;
|
|
||||||
import android.preference.PreferenceManager;
|
import android.preference.PreferenceManager;
|
||||||
|
|
||||||
public class PreferenceConfiguration {
|
public class PreferenceConfiguration {
|
||||||
static final String RES_FPS_PREF_STRING = "list_resolution_fps";
|
static final String RESX_PREF_STRING = "resolution_x";
|
||||||
|
static final String RESY_PREF_STRING = "resolution_y";
|
||||||
|
private static final String FPS_PREF_STRING = "fps";
|
||||||
|
private static final String DECODER_PREF_STRING = "list_decoders";
|
||||||
static final String BITRATE_PREF_STRING = "seekbar_bitrate";
|
static final String BITRATE_PREF_STRING = "seekbar_bitrate";
|
||||||
private static final String STRETCH_PREF_STRING = "checkbox_stretch_video";
|
private static final String STRETCH_PREF_STRING = "checkbox_stretch_video";
|
||||||
private static final String SOPS_PREF_STRING = "checkbox_enable_sops";
|
private static final String SOPS_PREF_STRING = "checkbox_enable_sops";
|
||||||
@@ -17,22 +18,17 @@ public class PreferenceConfiguration {
|
|||||||
private static final String LANGUAGE_PREF_STRING = "list_languages";
|
private static final String LANGUAGE_PREF_STRING = "list_languages";
|
||||||
private static final String LIST_MODE_PREF_STRING = "checkbox_list_mode";
|
private static final String LIST_MODE_PREF_STRING = "checkbox_list_mode";
|
||||||
private static final String SMALL_ICONS_PREF_STRING = "checkbox_small_icon_mode";
|
private static final String SMALL_ICONS_PREF_STRING = "checkbox_small_icon_mode";
|
||||||
private static final String MULTI_CONTROLLER_PREF_STRING = "checkbox_multi_controller";
|
|
||||||
private static final String ENABLE_51_SURROUND_PREF_STRING = "checkbox_51_surround";
|
|
||||||
private static final String USB_DRIVER_PREF_SRING = "checkbox_usb_driver";
|
|
||||||
private static final String VIDEO_FORMAT_PREF_STRING = "video_format";
|
|
||||||
private static final String ONSCREEN_CONTROLLER_PREF_STRING = "checkbox_show_onscreen_controls";
|
|
||||||
private static final String BATTERY_SAVER_PREF_STRING = "checkbox_battery_saver";
|
|
||||||
|
|
||||||
private static final int BITRATE_DEFAULT_720_30 = 5;
|
private static final int BITRATE_DEFAULT_720_30 = 5;
|
||||||
private static final int BITRATE_DEFAULT_720_60 = 10;
|
private static final int BITRATE_DEFAULT_720_60 = 10;
|
||||||
private static final int BITRATE_DEFAULT_1080_30 = 10;
|
private static final int BITRATE_DEFAULT_1080_30 = 10;
|
||||||
private static final int BITRATE_DEFAULT_1080_60 = 20;
|
private static final int BITRATE_DEFAULT_1080_60 = 20;
|
||||||
private static final int BITRATE_DEFAULT_4K_30 = 40;
|
private static final int BITRATE_DEFAULT_MAX = 35;
|
||||||
private static final int BITRATE_DEFAULT_4K_60 = 80;
|
|
||||||
|
|
||||||
private static final String DEFAULT_RES_FPS = "720p60";
|
private static final int DEFAULT_RESX = 1280;
|
||||||
private static final int DEFAULT_BITRATE = BITRATE_DEFAULT_720_60;
|
private static final int DEFAULT_RESY = 720;
|
||||||
|
private static final String DEFAULT_FPS = "60";
|
||||||
|
private static final String DEFAULT_DECODER = "auto";
|
||||||
private static final boolean DEFAULT_STRETCH = false;
|
private static final boolean DEFAULT_STRETCH = false;
|
||||||
private static final boolean DEFAULT_SOPS = true;
|
private static final boolean DEFAULT_SOPS = true;
|
||||||
private static final boolean DEFAULT_DISABLE_TOASTS = false;
|
private static final boolean DEFAULT_DISABLE_TOASTS = false;
|
||||||
@@ -40,150 +36,85 @@ public class PreferenceConfiguration {
|
|||||||
private static final int DEFAULT_DEADZONE = 15;
|
private static final int DEFAULT_DEADZONE = 15;
|
||||||
public static final String DEFAULT_LANGUAGE = "default";
|
public static final String DEFAULT_LANGUAGE = "default";
|
||||||
private static final boolean DEFAULT_LIST_MODE = false;
|
private static final boolean DEFAULT_LIST_MODE = false;
|
||||||
private static final boolean DEFAULT_MULTI_CONTROLLER = true;
|
|
||||||
private static final boolean DEFAULT_ENABLE_51_SURROUND = false;
|
|
||||||
private static final boolean DEFAULT_USB_DRIVER = true;
|
|
||||||
private static final String DEFAULT_VIDEO_FORMAT = "auto";
|
|
||||||
private static final boolean ONSCREEN_CONTROLLER_DEFAULT = false;
|
|
||||||
private static final boolean DEFAULT_BATTERY_SAVER = false;
|
|
||||||
|
|
||||||
public static final int FORCE_H265_ON = -1;
|
public static final int FORCE_HARDWARE_DECODER = -1;
|
||||||
public static final int AUTOSELECT_H265 = 0;
|
public static final int AUTOSELECT_DECODER = 0;
|
||||||
public static final int FORCE_H265_OFF = 1;
|
public static final int FORCE_SOFTWARE_DECODER = 1;
|
||||||
|
|
||||||
public int width, height, fps;
|
public int width, height, fps;
|
||||||
public int bitrate;
|
public int bitrate;
|
||||||
public int videoFormat;
|
public int decoder;
|
||||||
public int deadzonePercentage;
|
public int deadzonePercentage;
|
||||||
public boolean stretchVideo, enableSops, playHostAudio, disableWarnings;
|
public boolean stretchVideo, enableSops, playHostAudio, disableWarnings;
|
||||||
public String language;
|
public String language;
|
||||||
public boolean listMode, smallIconMode, multiController, enable51Surround, usbDriver;
|
public boolean listMode, smallIconMode;
|
||||||
public boolean onscreenController;
|
|
||||||
public boolean batterySaver;
|
|
||||||
|
|
||||||
public static int getDefaultBitrate(String resFpsString) {
|
|
||||||
if (resFpsString.equals("720p30")) {
|
|
||||||
return BITRATE_DEFAULT_720_30;
|
|
||||||
}
|
|
||||||
else if (resFpsString.equals("720p60")) {
|
|
||||||
return BITRATE_DEFAULT_720_60;
|
|
||||||
}
|
|
||||||
else if (resFpsString.equals("1080p30")) {
|
|
||||||
return BITRATE_DEFAULT_1080_30;
|
|
||||||
}
|
|
||||||
else if (resFpsString.equals("1080p60")) {
|
|
||||||
return BITRATE_DEFAULT_1080_60;
|
|
||||||
}
|
|
||||||
else if (resFpsString.equals("4K30")) {
|
|
||||||
return BITRATE_DEFAULT_4K_30;
|
|
||||||
}
|
|
||||||
else if (resFpsString.equals("4K60")) {
|
|
||||||
return BITRATE_DEFAULT_4K_60;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
// Should never get here
|
|
||||||
return DEFAULT_BITRATE;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static boolean getDefaultSmallMode(Context context) {
|
public static boolean getDefaultSmallMode(Context context) {
|
||||||
PackageManager manager = context.getPackageManager();
|
|
||||||
if (manager != null) {
|
|
||||||
// TVs shouldn't use small mode by default
|
|
||||||
if (manager.hasSystemFeature(PackageManager.FEATURE_TELEVISION)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// API 21 uses LEANBACK instead of TELEVISION
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP_MR1) {
|
|
||||||
if (manager.hasSystemFeature(PackageManager.FEATURE_LEANBACK)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Use small mode on anything smaller than a 7" tablet
|
// Use small mode on anything smaller than a 7" tablet
|
||||||
return context.getResources().getConfiguration().smallestScreenWidthDp < 500;
|
return context.getResources().getConfiguration().smallestScreenWidthDp < 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int getFpsInt(SharedPreferences prefs) {
|
||||||
|
try {
|
||||||
|
return Integer.parseInt(prefs.getString(FPS_PREF_STRING, DEFAULT_FPS));
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
return Integer.parseInt(DEFAULT_FPS);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static int getDefaultBitrate(Context context) {
|
public static int getDefaultBitrate(Context context) {
|
||||||
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
|
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
|
||||||
return getDefaultBitrate(prefs.getString(RES_FPS_PREF_STRING, DEFAULT_RES_FPS));
|
|
||||||
|
long width = prefs.getInt(RESX_PREF_STRING, DEFAULT_RESX);
|
||||||
|
long height = prefs.getInt(RESY_PREF_STRING, DEFAULT_RESY);
|
||||||
|
|
||||||
|
long product = width * height * getFpsInt(prefs);
|
||||||
|
if (product <= 1280 * 720 * 30) {
|
||||||
|
return BITRATE_DEFAULT_720_30;
|
||||||
|
}
|
||||||
|
else if (product <= 1280 * 720 * 60) {
|
||||||
|
return BITRATE_DEFAULT_720_60;
|
||||||
|
}
|
||||||
|
else if (product <= 1920 * 1080 * 30) {
|
||||||
|
return BITRATE_DEFAULT_1080_30;
|
||||||
|
}
|
||||||
|
else if (product <= 1920 * 1080 * 60) {
|
||||||
|
return BITRATE_DEFAULT_1080_60;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return BITRATE_DEFAULT_MAX;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static int getVideoFormatValue(Context context) {
|
private static int getDecoderValue(Context context) {
|
||||||
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
|
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
|
||||||
|
|
||||||
String str = prefs.getString(VIDEO_FORMAT_PREF_STRING, DEFAULT_VIDEO_FORMAT);
|
String str = prefs.getString(DECODER_PREF_STRING, DEFAULT_DECODER);
|
||||||
if (str.equals("auto")) {
|
if (str.equals("auto")) {
|
||||||
return AUTOSELECT_H265;
|
return AUTOSELECT_DECODER;
|
||||||
}
|
}
|
||||||
else if (str.equals("forceh265")) {
|
else if (str.equals("software")) {
|
||||||
return FORCE_H265_ON;
|
return FORCE_SOFTWARE_DECODER;
|
||||||
}
|
}
|
||||||
else if (str.equals("neverh265")) {
|
else if (str.equals("hardware")) {
|
||||||
return FORCE_H265_OFF;
|
return FORCE_HARDWARE_DECODER;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
// Should never get here
|
// Should never get here
|
||||||
return AUTOSELECT_H265;
|
return AUTOSELECT_DECODER;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void resetStreamingSettings(Context context) {
|
|
||||||
// We consider resolution, FPS, bitrate, and video format as "streaming settings" here
|
|
||||||
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
|
|
||||||
prefs.edit()
|
|
||||||
.remove(BITRATE_PREF_STRING)
|
|
||||||
.remove(RES_FPS_PREF_STRING)
|
|
||||||
.remove(VIDEO_FORMAT_PREF_STRING)
|
|
||||||
.apply();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static PreferenceConfiguration readPreferences(Context context) {
|
public static PreferenceConfiguration readPreferences(Context context) {
|
||||||
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
|
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
|
||||||
PreferenceConfiguration config = new PreferenceConfiguration();
|
PreferenceConfiguration config = new PreferenceConfiguration();
|
||||||
|
|
||||||
config.bitrate = prefs.getInt(BITRATE_PREF_STRING, getDefaultBitrate(context));
|
config.bitrate = prefs.getInt(BITRATE_PREF_STRING, getDefaultBitrate(context));
|
||||||
String str = prefs.getString(RES_FPS_PREF_STRING, DEFAULT_RES_FPS);
|
config.width = prefs.getInt(RESX_PREF_STRING, DEFAULT_RESX);
|
||||||
if (str.equals("720p30")) {
|
config.height = prefs.getInt(RESY_PREF_STRING, DEFAULT_RESY);
|
||||||
config.width = 1280;
|
config.fps = getFpsInt(prefs);
|
||||||
config.height = 720;
|
|
||||||
config.fps = 30;
|
|
||||||
}
|
|
||||||
else if (str.equals("720p60")) {
|
|
||||||
config.width = 1280;
|
|
||||||
config.height = 720;
|
|
||||||
config.fps = 60;
|
|
||||||
}
|
|
||||||
else if (str.equals("1080p30")) {
|
|
||||||
config.width = 1920;
|
|
||||||
config.height = 1080;
|
|
||||||
config.fps = 30;
|
|
||||||
}
|
|
||||||
else if (str.equals("1080p60")) {
|
|
||||||
config.width = 1920;
|
|
||||||
config.height = 1080;
|
|
||||||
config.fps = 60;
|
|
||||||
}
|
|
||||||
else if (str.equals("4K30")) {
|
|
||||||
config.width = 3840;
|
|
||||||
config.height = 2160;
|
|
||||||
config.fps = 30;
|
|
||||||
}
|
|
||||||
else if (str.equals("4K60")) {
|
|
||||||
config.width = 3840;
|
|
||||||
config.height = 2160;
|
|
||||||
config.fps = 60;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
// Should never get here
|
|
||||||
config.width = 1280;
|
|
||||||
config.height = 720;
|
|
||||||
config.fps = 60;
|
|
||||||
}
|
|
||||||
|
|
||||||
config.videoFormat = getVideoFormatValue(context);
|
config.decoder = getDecoderValue(context);
|
||||||
|
|
||||||
config.deadzonePercentage = prefs.getInt(DEADZONE_PREF_STRING, DEFAULT_DEADZONE);
|
config.deadzonePercentage = prefs.getInt(DEADZONE_PREF_STRING, DEFAULT_DEADZONE);
|
||||||
|
|
||||||
@@ -196,11 +127,6 @@ public class PreferenceConfiguration {
|
|||||||
config.playHostAudio = prefs.getBoolean(HOST_AUDIO_PREF_STRING, DEFAULT_HOST_AUDIO);
|
config.playHostAudio = prefs.getBoolean(HOST_AUDIO_PREF_STRING, DEFAULT_HOST_AUDIO);
|
||||||
config.listMode = prefs.getBoolean(LIST_MODE_PREF_STRING, DEFAULT_LIST_MODE);
|
config.listMode = prefs.getBoolean(LIST_MODE_PREF_STRING, DEFAULT_LIST_MODE);
|
||||||
config.smallIconMode = prefs.getBoolean(SMALL_ICONS_PREF_STRING, getDefaultSmallMode(context));
|
config.smallIconMode = prefs.getBoolean(SMALL_ICONS_PREF_STRING, getDefaultSmallMode(context));
|
||||||
config.multiController = prefs.getBoolean(MULTI_CONTROLLER_PREF_STRING, DEFAULT_MULTI_CONTROLLER);
|
|
||||||
config.enable51Surround = prefs.getBoolean(ENABLE_51_SURROUND_PREF_STRING, DEFAULT_ENABLE_51_SURROUND);
|
|
||||||
config.usbDriver = prefs.getBoolean(USB_DRIVER_PREF_SRING, DEFAULT_USB_DRIVER);
|
|
||||||
config.onscreenController = prefs.getBoolean(ONSCREEN_CONTROLLER_PREF_STRING, ONSCREEN_CONTROLLER_DEFAULT);
|
|
||||||
config.batterySaver = prefs.getBoolean(BATTERY_SAVER_PREF_STRING, DEFAULT_BATTERY_SAVER);
|
|
||||||
|
|
||||||
return config;
|
return config;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,208 @@
|
|||||||
|
package com.limelight.preferences;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.SharedPreferences;
|
||||||
|
import android.preference.DialogPreference;
|
||||||
|
import android.util.AttributeSet;
|
||||||
|
import android.view.View;
|
||||||
|
import android.widget.CompoundButton;
|
||||||
|
import android.widget.RadioButton;
|
||||||
|
import android.widget.SeekBar;
|
||||||
|
import android.widget.TextView;
|
||||||
|
|
||||||
|
import com.limelight.R;
|
||||||
|
|
||||||
|
public class ResolutionPreference extends DialogPreference {
|
||||||
|
private RadioButton a4_3;
|
||||||
|
private RadioButton a16_9;
|
||||||
|
private RadioButton a16_10;
|
||||||
|
private SeekBar resolutionSeekBar;
|
||||||
|
private TextView resolutionText;
|
||||||
|
|
||||||
|
private int resolutionX;
|
||||||
|
private int resolutionY;
|
||||||
|
|
||||||
|
public ResolutionPreference(Context context, AttributeSet attrs) {
|
||||||
|
super(context, attrs);
|
||||||
|
|
||||||
|
setDialogLayoutResource(R.layout.dialog_resolution);
|
||||||
|
setPositiveButtonText(android.R.string.ok);
|
||||||
|
setNegativeButtonText(android.R.string.cancel);
|
||||||
|
|
||||||
|
setDialogIcon(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private int getPreferenceState() {
|
||||||
|
int checkboxState;
|
||||||
|
int sliderState;
|
||||||
|
|
||||||
|
if (a4_3.isChecked()) {
|
||||||
|
checkboxState = 1;
|
||||||
|
}
|
||||||
|
else if (a16_10.isChecked()) {
|
||||||
|
checkboxState = 2;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
checkboxState = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
sliderState = resolutionSeekBar.getProgress();
|
||||||
|
|
||||||
|
return checkboxState << 24 | sliderState;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setPreferenceState(int state) {
|
||||||
|
int checkboxState;
|
||||||
|
int sliderState;
|
||||||
|
|
||||||
|
checkboxState = state >> 24;
|
||||||
|
sliderState = state & 0x00FFFFFF;
|
||||||
|
|
||||||
|
a4_3.setChecked(false);
|
||||||
|
a16_10.setChecked(false);
|
||||||
|
a16_9.setChecked(false);
|
||||||
|
|
||||||
|
if (checkboxState == 1) {
|
||||||
|
a4_3.setChecked(true);
|
||||||
|
}
|
||||||
|
else if (checkboxState == 2) {
|
||||||
|
a16_10.setChecked(true);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
a16_9.setChecked(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
resolutionSeekBar.setProgress(sliderState);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateResolution() {
|
||||||
|
double aspectFactor;
|
||||||
|
int step = resolutionSeekBar.getProgress() + 1;
|
||||||
|
|
||||||
|
if (a4_3.isChecked()) {
|
||||||
|
aspectFactor = 4.0/3.0;
|
||||||
|
|
||||||
|
switch (step) {
|
||||||
|
case 1:
|
||||||
|
// 640x480
|
||||||
|
resolutionY = 480;
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
|
// 800x600
|
||||||
|
resolutionY = 600;
|
||||||
|
break;
|
||||||
|
case 3:
|
||||||
|
// 1024x768
|
||||||
|
resolutionY = 768;
|
||||||
|
break;
|
||||||
|
case 4:
|
||||||
|
// 1600x1200
|
||||||
|
resolutionY = 1200;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (a16_9.isChecked()) {
|
||||||
|
aspectFactor = 16.0/9.0;
|
||||||
|
|
||||||
|
switch (step) {
|
||||||
|
case 1:
|
||||||
|
// 1280x720
|
||||||
|
resolutionY = 720;
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
|
// 1920x1080
|
||||||
|
resolutionY = 1080;
|
||||||
|
break;
|
||||||
|
case 3:
|
||||||
|
// 2560x1440
|
||||||
|
resolutionY = 1440;
|
||||||
|
break;
|
||||||
|
case 4:
|
||||||
|
// 3840x2160
|
||||||
|
resolutionY = 2160;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else /* if (a16_10.isChecked() */ {
|
||||||
|
aspectFactor = 16.0/10.0;
|
||||||
|
|
||||||
|
switch (step) {
|
||||||
|
case 1:
|
||||||
|
// 1280x800
|
||||||
|
resolutionY = 800;
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
|
// 1920x1200
|
||||||
|
resolutionY = 1200;
|
||||||
|
break;
|
||||||
|
case 3:
|
||||||
|
// 2560x1600
|
||||||
|
resolutionY = 1600;
|
||||||
|
break;
|
||||||
|
case 4:
|
||||||
|
// 3840x2400
|
||||||
|
resolutionY = 2400;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resolutionX = (int)(resolutionY * aspectFactor);
|
||||||
|
|
||||||
|
resolutionText.setText(resolutionX+"x"+resolutionY);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onDialogClosed(boolean positiveResult) {
|
||||||
|
if (positiveResult) {
|
||||||
|
SharedPreferences.Editor e = getPreferenceManager().getSharedPreferences().edit();
|
||||||
|
e.putInt(PreferenceConfiguration.RESX_PREF_STRING, resolutionX);
|
||||||
|
e.putInt(PreferenceConfiguration.RESY_PREF_STRING, resolutionY);
|
||||||
|
persistInt(getPreferenceState());
|
||||||
|
e.commit();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected View onCreateDialogView() {
|
||||||
|
View v = super.onCreateDialogView();
|
||||||
|
|
||||||
|
a4_3 = (RadioButton) v.findViewById(R.id.aspect_4_3);
|
||||||
|
a16_9 = (RadioButton) v.findViewById(R.id.aspect_16_9);
|
||||||
|
a16_10 = (RadioButton) v.findViewById(R.id.aspect_16_10);
|
||||||
|
resolutionText = (TextView) v.findViewById(R.id.resolutionText);
|
||||||
|
resolutionSeekBar = (SeekBar) v.findViewById(R.id.resolutionSeekbar);
|
||||||
|
|
||||||
|
CompoundButton.OnCheckedChangeListener ccListener = new CompoundButton.OnCheckedChangeListener() {
|
||||||
|
@Override
|
||||||
|
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
|
||||||
|
updateResolution();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
a4_3.setOnCheckedChangeListener(ccListener);
|
||||||
|
a16_9.setOnCheckedChangeListener(ccListener);
|
||||||
|
a16_10.setOnCheckedChangeListener(ccListener);
|
||||||
|
|
||||||
|
resolutionSeekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
|
||||||
|
@Override
|
||||||
|
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
|
||||||
|
updateResolution();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onStartTrackingTouch(SeekBar seekBar) {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onStopTrackingTouch(SeekBar seekBar) {
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Check the correct checkbox
|
||||||
|
setPreferenceState(getPersistedInt(0));
|
||||||
|
|
||||||
|
// Set initial resolution value
|
||||||
|
updateResolution();
|
||||||
|
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -20,14 +20,10 @@ public class SeekBarPreference extends DialogPreference
|
|||||||
|
|
||||||
private SeekBar seekBar;
|
private SeekBar seekBar;
|
||||||
private TextView valueText;
|
private TextView valueText;
|
||||||
private final Context context;
|
private Context context;
|
||||||
|
|
||||||
private final String dialogMessage;
|
private String dialogMessage, suffix;
|
||||||
private final String suffix;
|
private int defaultValue, maxValue, minValue, currentValue;
|
||||||
private final int defaultValue;
|
|
||||||
private final int maxValue;
|
|
||||||
private final int minValue;
|
|
||||||
private int currentValue;
|
|
||||||
|
|
||||||
public SeekBarPreference(Context context, AttributeSet attrs) {
|
public SeekBarPreference(Context context, AttributeSet attrs) {
|
||||||
super(context, attrs);
|
super(context, attrs);
|
||||||
@@ -131,6 +127,13 @@ public class SeekBarPreference extends DialogPreference
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void setMax(int max) {
|
||||||
|
this.maxValue = max;
|
||||||
|
}
|
||||||
|
public int getMax() {
|
||||||
|
return this.maxValue;
|
||||||
|
}
|
||||||
|
|
||||||
public void setProgress(int progress) {
|
public void setProgress(int progress) {
|
||||||
this.currentValue = progress;
|
this.currentValue = progress;
|
||||||
if (seekBar != null) {
|
if (seekBar != null) {
|
||||||
|
|||||||
@@ -2,28 +2,30 @@ package com.limelight.preferences;
|
|||||||
|
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.content.SharedPreferences;
|
import android.content.SharedPreferences;
|
||||||
|
import android.content.res.Configuration;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
import android.preference.Preference;
|
import android.preference.Preference;
|
||||||
import android.preference.PreferenceCategory;
|
|
||||||
import android.preference.PreferenceFragment;
|
import android.preference.PreferenceFragment;
|
||||||
import android.preference.PreferenceManager;
|
import android.preference.PreferenceManager;
|
||||||
import android.preference.PreferenceScreen;
|
|
||||||
|
|
||||||
import com.limelight.PcView;
|
import com.limelight.PcView;
|
||||||
import com.limelight.R;
|
import com.limelight.R;
|
||||||
import com.limelight.utils.UiHelper;
|
import com.limelight.utils.UiHelper;
|
||||||
|
|
||||||
public class StreamSettings extends Activity {
|
import java.util.Locale;
|
||||||
private PreferenceConfiguration previousPrefs;
|
|
||||||
|
|
||||||
|
public class StreamSettings extends Activity {
|
||||||
@Override
|
@Override
|
||||||
protected void onCreate(Bundle savedInstanceState) {
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
|
|
||||||
previousPrefs = PreferenceConfiguration.readPreferences(this);
|
String locale = PreferenceConfiguration.readPreferences(this).language;
|
||||||
|
if (!locale.equals(PreferenceConfiguration.DEFAULT_LANGUAGE)) {
|
||||||
UiHelper.setLocale(this);
|
Configuration config = new Configuration(getResources().getConfiguration());
|
||||||
|
config.locale = new Locale(locale);
|
||||||
|
getResources().updateConfiguration(config, getResources().getDisplayMetrics());
|
||||||
|
}
|
||||||
|
|
||||||
setContentView(R.layout.activity_stream_settings);
|
setContentView(R.layout.activity_stream_settings);
|
||||||
getFragmentManager().beginTransaction().replace(
|
getFragmentManager().beginTransaction().replace(
|
||||||
@@ -37,16 +39,10 @@ public class StreamSettings extends Activity {
|
|||||||
public void onBackPressed() {
|
public void onBackPressed() {
|
||||||
finish();
|
finish();
|
||||||
|
|
||||||
// Check for changes that require a UI reload to take effect
|
// Restart the PC view to apply UI changes
|
||||||
PreferenceConfiguration newPrefs = PreferenceConfiguration.readPreferences(this);
|
Intent intent = new Intent(this, PcView.class);
|
||||||
if (newPrefs.listMode != previousPrefs.listMode ||
|
intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK | Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||||
newPrefs.smallIconMode != previousPrefs.smallIconMode ||
|
startActivity(intent, null);
|
||||||
!newPrefs.language.equals(previousPrefs.language)) {
|
|
||||||
// Restart the PC view to apply UI changes
|
|
||||||
Intent intent = new Intent(this, PcView.class);
|
|
||||||
intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK | Intent.FLAG_ACTIVITY_NEW_TASK);
|
|
||||||
startActivity(intent, null);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class SettingsFragment extends PreferenceFragment {
|
public static class SettingsFragment extends PreferenceFragment {
|
||||||
@@ -55,35 +51,6 @@ public class StreamSettings extends Activity {
|
|||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
|
|
||||||
addPreferencesFromResource(R.xml.preferences);
|
addPreferencesFromResource(R.xml.preferences);
|
||||||
PreferenceScreen screen = getPreferenceScreen();
|
|
||||||
|
|
||||||
// hide on-screen controls category on non touch screen devices
|
|
||||||
if (!getActivity().getPackageManager().
|
|
||||||
hasSystemFeature("android.hardware.touchscreen")) {
|
|
||||||
PreferenceCategory category =
|
|
||||||
(PreferenceCategory) findPreference("category_onscreen_controls");
|
|
||||||
screen.removePreference(category);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add a listener to the FPS and resolution preference
|
|
||||||
// so the bitrate can be auto-adjusted
|
|
||||||
Preference pref = findPreference(PreferenceConfiguration.RES_FPS_PREF_STRING);
|
|
||||||
pref.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
|
|
||||||
@Override
|
|
||||||
public boolean onPreferenceChange(Preference preference, Object newValue) {
|
|
||||||
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(SettingsFragment.this.getActivity());
|
|
||||||
String valueStr = (String) newValue;
|
|
||||||
|
|
||||||
// Write the new bitrate value
|
|
||||||
prefs.edit()
|
|
||||||
.putInt(PreferenceConfiguration.BITRATE_PREF_STRING,
|
|
||||||
PreferenceConfiguration.getDefaultBitrate(valueStr))
|
|
||||||
.apply();
|
|
||||||
|
|
||||||
// Allow the original preference change to take place
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3,6 +3,6 @@ package com.limelight.ui;
|
|||||||
import android.widget.AbsListView;
|
import android.widget.AbsListView;
|
||||||
|
|
||||||
public interface AdapterFragmentCallbacks {
|
public interface AdapterFragmentCallbacks {
|
||||||
int getAdapterFragmentLayoutId();
|
public int getAdapterFragmentLayoutId();
|
||||||
void receiveAbsListView(AbsListView gridView);
|
public void receiveAbsListView(AbsListView gridView);
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
package com.limelight.ui;
|
package com.limelight.ui;
|
||||||
|
|
||||||
public interface GameGestures {
|
public interface GameGestures {
|
||||||
void showKeyboard();
|
public void showKeyboard();
|
||||||
}
|
}
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,7 +1,5 @@
|
|||||||
package com.limelight.utils;
|
package com.limelight.utils;
|
||||||
|
|
||||||
import java.io.BufferedInputStream;
|
|
||||||
import java.io.BufferedOutputStream;
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.FileInputStream;
|
import java.io.FileInputStream;
|
||||||
import java.io.FileNotFoundException;
|
import java.io.FileNotFoundException;
|
||||||
@@ -11,6 +9,8 @@ import java.io.InputStream;
|
|||||||
import java.io.InputStreamReader;
|
import java.io.InputStreamReader;
|
||||||
import java.io.OutputStream;
|
import java.io.OutputStream;
|
||||||
import java.io.Reader;
|
import java.io.Reader;
|
||||||
|
import java.io.UnsupportedEncodingException;
|
||||||
|
import java.util.Scanner;
|
||||||
|
|
||||||
public class CacheHelper {
|
public class CacheHelper {
|
||||||
private static File openPath(boolean createPath, File root, String... path) {
|
private static File openPath(boolean createPath, File root, String... path) {
|
||||||
@@ -30,37 +30,12 @@ public class CacheHelper {
|
|||||||
return f;
|
return f;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static long getFileSize(File root, String... path) {
|
public static FileInputStream openCacheFileForInput(File root, String... path) throws FileNotFoundException {
|
||||||
return openPath(false, root, path).length();
|
return new FileInputStream(openPath(false, root, path));
|
||||||
}
|
}
|
||||||
|
|
||||||
public static boolean deleteCacheFile(File root, String... path) {
|
public static FileOutputStream openCacheFileForOutput(File root, String... path) throws FileNotFoundException {
|
||||||
return openPath(false, root, path).delete();
|
return new FileOutputStream(openPath(true, root, path));
|
||||||
}
|
|
||||||
|
|
||||||
public static boolean cacheFileExists(File root, String... path) {
|
|
||||||
return openPath(false, root, path).exists();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static InputStream openCacheFileForInput(File root, String... path) throws FileNotFoundException {
|
|
||||||
return new BufferedInputStream(new FileInputStream(openPath(false, root, path)));
|
|
||||||
}
|
|
||||||
|
|
||||||
public static OutputStream openCacheFileForOutput(File root, String... path) throws FileNotFoundException {
|
|
||||||
return new BufferedOutputStream(new FileOutputStream(openPath(true, root, path)));
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void writeInputStreamToOutputStream(InputStream in, OutputStream out, long maxLength) throws IOException {
|
|
||||||
byte[] buf = new byte[4096];
|
|
||||||
int bytesRead;
|
|
||||||
|
|
||||||
while ((bytesRead = in.read(buf)) != -1) {
|
|
||||||
maxLength -= bytesRead;
|
|
||||||
if (maxLength <= 0) {
|
|
||||||
throw new IOException("Stream exceeded max size");
|
|
||||||
}
|
|
||||||
out.write(buf, 0, bytesRead);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static String readInputStreamToString(InputStream in) throws IOException {
|
public static String readInputStreamToString(InputStream in) throws IOException {
|
||||||
@@ -73,10 +48,6 @@ public class CacheHelper {
|
|||||||
sb.append(buf, 0, bytesRead);
|
sb.append(buf, 0, bytesRead);
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
|
||||||
in.close();
|
|
||||||
} catch (IOException ignored) {}
|
|
||||||
|
|
||||||
return sb.toString();
|
return sb.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,101 +5,72 @@ import java.util.ArrayList;
|
|||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
import android.app.AlertDialog;
|
import android.app.AlertDialog;
|
||||||
import android.content.DialogInterface;
|
import android.content.DialogInterface;
|
||||||
import android.widget.Button;
|
|
||||||
|
|
||||||
import com.limelight.R;
|
|
||||||
|
|
||||||
public class Dialog implements Runnable {
|
public class Dialog implements Runnable {
|
||||||
private final String title;
|
private String title, message;
|
||||||
private final String message;
|
private Activity activity;
|
||||||
private final Activity activity;
|
private boolean endAfterDismiss;
|
||||||
private final boolean endAfterDismiss;
|
|
||||||
|
private AlertDialog alert;
|
||||||
|
|
||||||
|
private static final ArrayList<Dialog> rundownDialogs = new ArrayList<Dialog>();
|
||||||
|
|
||||||
|
public Dialog(Activity activity, String title, String message, boolean endAfterDismiss)
|
||||||
|
{
|
||||||
|
this.activity = activity;
|
||||||
|
this.title = title;
|
||||||
|
this.message = message;
|
||||||
|
this.endAfterDismiss = endAfterDismiss;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void closeDialogs()
|
||||||
|
{
|
||||||
|
synchronized (rundownDialogs) {
|
||||||
|
for (Dialog d : rundownDialogs) {
|
||||||
|
if (d.alert.isShowing()) {
|
||||||
|
d.alert.dismiss();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
rundownDialogs.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void displayDialog(Activity activity, String title, String message, boolean endAfterDismiss)
|
||||||
|
{
|
||||||
|
activity.runOnUiThread(new Dialog(activity, title, message, endAfterDismiss));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
// If we're dying, don't bother creating a dialog
|
||||||
|
if (activity.isFinishing())
|
||||||
|
return;
|
||||||
|
|
||||||
|
alert = new AlertDialog.Builder(activity).create();
|
||||||
|
|
||||||
private AlertDialog alert;
|
alert.setTitle(title);
|
||||||
|
alert.setMessage(message);
|
||||||
private static final ArrayList<Dialog> rundownDialogs = new ArrayList<>();
|
alert.setCancelable(false);
|
||||||
|
alert.setCanceledOnTouchOutside(false);
|
||||||
private Dialog(Activity activity, String title, String message, boolean endAfterDismiss)
|
|
||||||
{
|
|
||||||
this.activity = activity;
|
|
||||||
this.title = title;
|
|
||||||
this.message = message;
|
|
||||||
this.endAfterDismiss = endAfterDismiss;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void closeDialogs()
|
|
||||||
{
|
|
||||||
synchronized (rundownDialogs) {
|
|
||||||
for (Dialog d : rundownDialogs) {
|
|
||||||
if (d.alert.isShowing()) {
|
|
||||||
d.alert.dismiss();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
rundownDialogs.clear();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void displayDialog(Activity activity, String title, String message, boolean endAfterDismiss)
|
|
||||||
{
|
|
||||||
activity.runOnUiThread(new Dialog(activity, title, message, endAfterDismiss));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
// If we're dying, don't bother creating a dialog
|
|
||||||
if (activity.isFinishing())
|
|
||||||
return;
|
|
||||||
|
|
||||||
alert = new AlertDialog.Builder(activity).create();
|
|
||||||
|
|
||||||
alert.setTitle(title);
|
|
||||||
alert.setMessage(message);
|
|
||||||
alert.setCancelable(false);
|
|
||||||
alert.setCanceledOnTouchOutside(false);
|
|
||||||
|
|
||||||
alert.setButton(AlertDialog.BUTTON_POSITIVE, activity.getResources().getText(android.R.string.ok), new DialogInterface.OnClickListener() {
|
alert.setButton(AlertDialog.BUTTON_NEUTRAL, "OK", new DialogInterface.OnClickListener() {
|
||||||
public void onClick(DialogInterface dialog, int which) {
|
public void onClick(DialogInterface dialog, int which) {
|
||||||
synchronized (rundownDialogs) {
|
synchronized (rundownDialogs) {
|
||||||
rundownDialogs.remove(Dialog.this);
|
rundownDialogs.remove(Dialog.this);
|
||||||
alert.dismiss();
|
alert.dismiss();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (endAfterDismiss) {
|
if (endAfterDismiss) {
|
||||||
activity.finish();
|
activity.finish();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
alert.setButton(AlertDialog.BUTTON_NEUTRAL, activity.getResources().getText(R.string.help), new DialogInterface.OnClickListener() {
|
|
||||||
public void onClick(DialogInterface dialog, int which) {
|
synchronized (rundownDialogs) {
|
||||||
synchronized (rundownDialogs) {
|
rundownDialogs.add(this);
|
||||||
rundownDialogs.remove(Dialog.this);
|
alert.show();
|
||||||
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);
|
|
||||||
alert.show();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
package com.limelight.utils;
|
|
||||||
|
|
||||||
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");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,94 +0,0 @@
|
|||||||
package com.limelight.utils;
|
|
||||||
|
|
||||||
import android.app.Activity;
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.widget.Toast;
|
|
||||||
|
|
||||||
import com.limelight.Game;
|
|
||||||
import com.limelight.R;
|
|
||||||
import com.limelight.binding.PlatformBinding;
|
|
||||||
import com.limelight.computers.ComputerManagerService;
|
|
||||||
import com.limelight.nvstream.http.ComputerDetails;
|
|
||||||
import com.limelight.nvstream.http.GfeHttpResponseException;
|
|
||||||
import com.limelight.nvstream.http.NvApp;
|
|
||||||
import com.limelight.nvstream.http.NvHTTP;
|
|
||||||
|
|
||||||
import java.io.FileNotFoundException;
|
|
||||||
import java.net.UnknownHostException;
|
|
||||||
|
|
||||||
public class ServerHelper {
|
|
||||||
public static String getCurrentAddressFromComputer(ComputerDetails computer) {
|
|
||||||
return computer.reachability == ComputerDetails.Reachability.LOCAL ?
|
|
||||||
computer.localAddress : computer.remoteAddress;
|
|
||||||
}
|
|
||||||
|
|
||||||
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, getCurrentAddressFromComputer(computer));
|
|
||||||
intent.putExtra(Game.EXTRA_APP_NAME, app.getAppName());
|
|
||||||
intent.putExtra(Game.EXTRA_APP_ID, app.getAppId());
|
|
||||||
intent.putExtra(Game.EXTRA_UNIQUEID, managerBinder.getUniqueId());
|
|
||||||
intent.putExtra(Game.EXTRA_STREAMING_REMOTE,
|
|
||||||
computer.reachability != ComputerDetails.Reachability.LOCAL);
|
|
||||||
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,
|
|
||||||
final String address,
|
|
||||||
final NvApp app,
|
|
||||||
final ComputerManagerService.ComputerManagerBinder managerBinder,
|
|
||||||
final Runnable onComplete) {
|
|
||||||
Toast.makeText(parent, parent.getResources().getString(R.string.applist_quit_app) + " " + app.getAppName() + "...", Toast.LENGTH_SHORT).show();
|
|
||||||
new Thread(new Runnable() {
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
NvHTTP httpConn;
|
|
||||||
String message;
|
|
||||||
try {
|
|
||||||
httpConn = new NvHTTP(address,
|
|
||||||
managerBinder.getUniqueId(), null, PlatformBinding.getCryptoProvider(parent));
|
|
||||||
if (httpConn.quitApp()) {
|
|
||||||
message = parent.getResources().getString(R.string.applist_quit_success) + " " + app.getAppName();
|
|
||||||
} else {
|
|
||||||
message = parent.getResources().getString(R.string.applist_quit_fail) + " " + app.getAppName();
|
|
||||||
}
|
|
||||||
} catch (GfeHttpResponseException e) {
|
|
||||||
if (e.getErrorCode() == 599) {
|
|
||||||
message = "This session wasn't started by this device," +
|
|
||||||
" so it cannot be quit. End streaming on the original " +
|
|
||||||
"device or the PC itself. (Error code: "+e.getErrorCode()+")";
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
message = e.getMessage();
|
|
||||||
}
|
|
||||||
} catch (UnknownHostException e) {
|
|
||||||
message = parent.getResources().getString(R.string.error_unknown_host);
|
|
||||||
} catch (FileNotFoundException e) {
|
|
||||||
message = parent.getResources().getString(R.string.error_404);
|
|
||||||
} catch (Exception e) {
|
|
||||||
message = e.getMessage();
|
|
||||||
} finally {
|
|
||||||
if (onComplete != null) {
|
|
||||||
onComplete.run();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
final String toastMessage = message;
|
|
||||||
parent.runOnUiThread(new Runnable() {
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
Toast.makeText(parent, toastMessage, Toast.LENGTH_LONG).show();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}).start();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,138 +0,0 @@
|
|||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -9,112 +9,111 @@ import android.content.DialogInterface;
|
|||||||
import android.content.DialogInterface.OnCancelListener;
|
import android.content.DialogInterface.OnCancelListener;
|
||||||
|
|
||||||
public class SpinnerDialog implements Runnable,OnCancelListener {
|
public class SpinnerDialog implements Runnable,OnCancelListener {
|
||||||
private final String title;
|
private String title, message;
|
||||||
private final String message;
|
private Activity activity;
|
||||||
private final Activity activity;
|
private ProgressDialog progress;
|
||||||
private ProgressDialog progress;
|
private boolean finish;
|
||||||
private final boolean finish;
|
|
||||||
|
private static final ArrayList<SpinnerDialog> rundownDialogs = new ArrayList<SpinnerDialog>();
|
||||||
|
|
||||||
|
public SpinnerDialog(Activity activity, String title, String message, boolean finish)
|
||||||
|
{
|
||||||
|
this.activity = activity;
|
||||||
|
this.title = title;
|
||||||
|
this.message = message;
|
||||||
|
this.progress = null;
|
||||||
|
this.finish = finish;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static SpinnerDialog displayDialog(Activity activity, String title, String message, boolean finish)
|
||||||
|
{
|
||||||
|
SpinnerDialog spinner = new SpinnerDialog(activity, title, message, finish);
|
||||||
|
activity.runOnUiThread(spinner);
|
||||||
|
return spinner;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void closeDialogs(Activity activity)
|
||||||
|
{
|
||||||
|
synchronized (rundownDialogs) {
|
||||||
|
Iterator<SpinnerDialog> i = rundownDialogs.iterator();
|
||||||
|
while (i.hasNext()) {
|
||||||
|
SpinnerDialog dialog = i.next();
|
||||||
|
if (dialog.activity == activity) {
|
||||||
|
i.remove();
|
||||||
|
if (dialog.progress.isShowing()) {
|
||||||
|
dialog.progress.dismiss();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void dismiss()
|
||||||
|
{
|
||||||
|
// Running again with progress != null will destroy it
|
||||||
|
activity.runOnUiThread(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setMessage(final String message)
|
||||||
|
{
|
||||||
|
activity.runOnUiThread(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
progress.setMessage(message);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
|
||||||
private static final ArrayList<SpinnerDialog> rundownDialogs = new ArrayList<>();
|
// If we're dying, don't bother doing anything
|
||||||
|
if (activity.isFinishing()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (progress == null)
|
||||||
|
{
|
||||||
|
progress = new ProgressDialog(activity);
|
||||||
|
|
||||||
|
progress.setTitle(title);
|
||||||
|
progress.setMessage(message);
|
||||||
|
progress.setProgressStyle(ProgressDialog.STYLE_SPINNER);
|
||||||
|
progress.setOnCancelListener(this);
|
||||||
|
|
||||||
|
// If we want to finish the activity when this is killed, make it cancellable
|
||||||
|
if (finish)
|
||||||
|
{
|
||||||
|
progress.setCancelable(true);
|
||||||
|
progress.setCanceledOnTouchOutside(false);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
progress.setCancelable(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
synchronized (rundownDialogs) {
|
||||||
|
rundownDialogs.add(this);
|
||||||
|
progress.show();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
synchronized (rundownDialogs) {
|
||||||
|
if (rundownDialogs.remove(this) && progress.isShowing()) {
|
||||||
|
progress.dismiss();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private SpinnerDialog(Activity activity, String title, String message, boolean finish)
|
@Override
|
||||||
{
|
public void onCancel(DialogInterface dialog) {
|
||||||
this.activity = activity;
|
synchronized (rundownDialogs) {
|
||||||
this.title = title;
|
rundownDialogs.remove(this);
|
||||||
this.message = message;
|
}
|
||||||
this.progress = null;
|
|
||||||
this.finish = finish;
|
// This will only be called if finish was true, so we don't need to check again
|
||||||
}
|
activity.finish();
|
||||||
|
}
|
||||||
public static SpinnerDialog displayDialog(Activity activity, String title, String message, boolean finish)
|
|
||||||
{
|
|
||||||
SpinnerDialog spinner = new SpinnerDialog(activity, title, message, finish);
|
|
||||||
activity.runOnUiThread(spinner);
|
|
||||||
return spinner;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void closeDialogs(Activity activity)
|
|
||||||
{
|
|
||||||
synchronized (rundownDialogs) {
|
|
||||||
Iterator<SpinnerDialog> i = rundownDialogs.iterator();
|
|
||||||
while (i.hasNext()) {
|
|
||||||
SpinnerDialog dialog = i.next();
|
|
||||||
if (dialog.activity == activity) {
|
|
||||||
i.remove();
|
|
||||||
if (dialog.progress.isShowing()) {
|
|
||||||
dialog.progress.dismiss();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void dismiss()
|
|
||||||
{
|
|
||||||
// Running again with progress != null will destroy it
|
|
||||||
activity.runOnUiThread(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setMessage(final String message)
|
|
||||||
{
|
|
||||||
activity.runOnUiThread(new Runnable() {
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
progress.setMessage(message);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
|
|
||||||
// If we're dying, don't bother doing anything
|
|
||||||
if (activity.isFinishing()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (progress == null)
|
|
||||||
{
|
|
||||||
progress = new ProgressDialog(activity);
|
|
||||||
|
|
||||||
progress.setTitle(title);
|
|
||||||
progress.setMessage(message);
|
|
||||||
progress.setProgressStyle(ProgressDialog.STYLE_SPINNER);
|
|
||||||
progress.setOnCancelListener(this);
|
|
||||||
|
|
||||||
// If we want to finish the activity when this is killed, make it cancellable
|
|
||||||
if (finish)
|
|
||||||
{
|
|
||||||
progress.setCancelable(true);
|
|
||||||
progress.setCanceledOnTouchOutside(false);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
progress.setCancelable(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
synchronized (rundownDialogs) {
|
|
||||||
rundownDialogs.add(this);
|
|
||||||
progress.show();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
synchronized (rundownDialogs) {
|
|
||||||
if (rundownDialogs.remove(this) && progress.isShowing()) {
|
|
||||||
progress.dismiss();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onCancel(DialogInterface dialog) {
|
|
||||||
synchronized (rundownDialogs) {
|
|
||||||
rundownDialogs.remove(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
// This will only be called if finish was true, so we don't need to check again
|
|
||||||
activity.finish();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -1,46 +1,17 @@
|
|||||||
package com.limelight.utils;
|
package com.limelight.utils;
|
||||||
|
|
||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
import android.app.AlertDialog;
|
|
||||||
import android.app.UiModeManager;
|
import android.app.UiModeManager;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.DialogInterface;
|
|
||||||
import android.content.res.Configuration;
|
import android.content.res.Configuration;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
|
|
||||||
import com.limelight.R;
|
|
||||||
import com.limelight.preferences.PreferenceConfiguration;
|
|
||||||
|
|
||||||
import java.util.Locale;
|
|
||||||
|
|
||||||
public class UiHelper {
|
public class UiHelper {
|
||||||
|
|
||||||
// Values from https://developer.android.com/training/tv/start/layouts.html
|
// Values from https://developer.android.com/training/tv/start/layouts.html
|
||||||
private static final int TV_VERTICAL_PADDING_DP = 27;
|
private static final int TV_VERTICAL_PADDING_DP = 27;
|
||||||
private static final int TV_HORIZONTAL_PADDING_DP = 48;
|
private static final int TV_HORIZONTAL_PADDING_DP = 48;
|
||||||
|
|
||||||
public static void setLocale(Activity activity)
|
|
||||||
{
|
|
||||||
String locale = PreferenceConfiguration.readPreferences(activity).language;
|
|
||||||
if (!locale.equals(PreferenceConfiguration.DEFAULT_LANGUAGE)) {
|
|
||||||
Configuration config = new Configuration(activity.getResources().getConfiguration());
|
|
||||||
|
|
||||||
// Some locales include both language and country which must be separated
|
|
||||||
// before calling the Locale constructor.
|
|
||||||
if (locale.contains("-"))
|
|
||||||
{
|
|
||||||
config.locale = new Locale(locale.substring(0, locale.indexOf('-')),
|
|
||||||
locale.substring(locale.indexOf('-') + 1));
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
config.locale = new Locale(locale);
|
|
||||||
}
|
|
||||||
|
|
||||||
activity.getResources().updateConfiguration(config, activity.getResources().getDisplayMetrics());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void notifyNewRootView(Activity activity)
|
public static void notifyNewRootView(Activity activity)
|
||||||
{
|
{
|
||||||
View rootView = activity.findViewById(android.R.id.content);
|
View rootView = activity.findViewById(android.R.id.content);
|
||||||
@@ -57,31 +28,4 @@ public class UiHelper {
|
|||||||
horizontalPaddingPixels, verticalPaddingPixels);
|
horizontalPaddingPixels, verticalPaddingPixels);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void displayQuitConfirmationDialog(Activity parent, final Runnable onYes, final Runnable onNo) {
|
|
||||||
DialogInterface.OnClickListener dialogClickListener = new DialogInterface.OnClickListener() {
|
|
||||||
@Override
|
|
||||||
public void onClick(DialogInterface dialog, int which) {
|
|
||||||
switch (which){
|
|
||||||
case DialogInterface.BUTTON_POSITIVE:
|
|
||||||
if (onYes != null) {
|
|
||||||
onYes.run();
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case DialogInterface.BUTTON_NEGATIVE:
|
|
||||||
if (onNo != null) {
|
|
||||||
onNo.run();
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
AlertDialog.Builder builder = new AlertDialog.Builder(parent);
|
|
||||||
builder.setMessage(parent.getResources().getString(R.string.applist_quit_confirmation))
|
|
||||||
.setPositiveButton(parent.getResources().getString(R.string.yes), dialogClickListener)
|
|
||||||
.setNegativeButton(parent.getResources().getString(R.string.no), dialogClickListener)
|
|
||||||
.show();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
package com.limelight.utils;
|
|
||||||
|
|
||||||
public class Vector2d {
|
|
||||||
private float x;
|
|
||||||
private float y;
|
|
||||||
private double magnitude;
|
|
||||||
|
|
||||||
public static final Vector2d ZERO = new Vector2d();
|
|
||||||
|
|
||||||
public Vector2d() {
|
|
||||||
initialize(0, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void initialize(float x, float y) {
|
|
||||||
this.x = x;
|
|
||||||
this.y = y;
|
|
||||||
this.magnitude = Math.sqrt(Math.pow(x, 2) + Math.pow(y, 2));
|
|
||||||
}
|
|
||||||
|
|
||||||
public double getMagnitude() {
|
|
||||||
return magnitude;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void getNormalized(Vector2d vector) {
|
|
||||||
vector.initialize((float)(x / magnitude), (float)(y / magnitude));
|
|
||||||
}
|
|
||||||
|
|
||||||
public void scalarMultiply(double factor) {
|
|
||||||
initialize((float)(x * factor), (float)(y * factor));
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setX(float x) {
|
|
||||||
initialize(x, this.y);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setY(float y) {
|
|
||||||
initialize(this.x, y);
|
|
||||||
}
|
|
||||||
|
|
||||||
public float getX() {
|
|
||||||
return x;
|
|
||||||
}
|
|
||||||
|
|
||||||
public float getY() {
|
|
||||||
return y;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -3,8 +3,7 @@
|
|||||||
# Our minimum version is Android 4.1
|
# Our minimum version is Android 4.1
|
||||||
APP_PLATFORM := android-16
|
APP_PLATFORM := android-16
|
||||||
|
|
||||||
# Support all modern ABIs
|
APP_ABI := armeabi-v7a arm64-v8a x86 x86_64 mips #mips64
|
||||||
APP_ABI := armeabi-v7a arm64-v8a x86 x86_64 mips mips64
|
|
||||||
|
|
||||||
# We want an optimized build
|
# We want an optimized build
|
||||||
APP_OPTIM := release
|
APP_OPTIM := release
|
||||||
@@ -10,23 +10,4 @@ LOCAL_MODULE := evdev_reader
|
|||||||
LOCAL_SRC_FILES := evdev_reader.c
|
LOCAL_SRC_FILES := evdev_reader.c
|
||||||
LOCAL_LDLIBS := -llog
|
LOCAL_LDLIBS := -llog
|
||||||
|
|
||||||
|
include $(BUILD_SHARED_LIBRARY)
|
||||||
# This next portion of the makefile is mostly copied from build-executable.mk but
|
|
||||||
# creates a binary with the libXXX.so form so the APK will install and drop
|
|
||||||
# the binary correctly.
|
|
||||||
|
|
||||||
LOCAL_BUILD_SCRIPT := BUILD_EXECUTABLE
|
|
||||||
LOCAL_MAKEFILE := $(local-makefile)
|
|
||||||
|
|
||||||
$(call check-defined-LOCAL_MODULE,$(LOCAL_BUILD_SCRIPT))
|
|
||||||
$(call check-LOCAL_MODULE,$(LOCAL_MAKEFILE))
|
|
||||||
$(call check-LOCAL_MODULE_FILENAME)
|
|
||||||
|
|
||||||
# we are building target objects
|
|
||||||
my := TARGET_
|
|
||||||
|
|
||||||
$(call handle-module-filename,lib,$(TARGET_SONAME_EXTENSION))
|
|
||||||
$(call handle-module-built)
|
|
||||||
|
|
||||||
LOCAL_MODULE_CLASS := EXECUTABLE
|
|
||||||
include $(BUILD_SYSTEM)/build-module.mk
|
|
||||||
@@ -1,412 +1,118 @@
|
|||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
#include <stdio.h>
|
#include <jni.h>
|
||||||
#include <string.h>
|
|
||||||
|
|
||||||
#include <sys/types.h>
|
#include <sys/types.h>
|
||||||
#include <sys/socket.h>
|
|
||||||
#include <sys/stat.h>
|
#include <sys/stat.h>
|
||||||
#include <fcntl.h>
|
#include <fcntl.h>
|
||||||
#include <linux/input.h>
|
#include <linux/input.h>
|
||||||
#include <unistd.h>
|
#include <unistd.h>
|
||||||
#include <poll.h>
|
#include <poll.h>
|
||||||
#include <errno.h>
|
#include <errno.h>
|
||||||
#include <dirent.h>
|
|
||||||
#include <pthread.h>
|
|
||||||
#include <netinet/in.h>
|
|
||||||
#include <netinet/tcp.h>
|
|
||||||
|
|
||||||
#include <arpa/inet.h>
|
|
||||||
|
|
||||||
#include <android/log.h>
|
#include <android/log.h>
|
||||||
|
|
||||||
#define EVDEV_MAX_EVENT_SIZE 24
|
JNIEXPORT jint JNICALL
|
||||||
|
Java_com_limelight_binding_input_evdev_EvdevReader_open(JNIEnv *env, jobject this, jstring absolutePath) {
|
||||||
|
const char *path;
|
||||||
|
|
||||||
|
path = (*env)->GetStringUTFChars(env, absolutePath, NULL);
|
||||||
|
|
||||||
|
return open(path, O_RDWR);
|
||||||
|
}
|
||||||
|
|
||||||
#define REL_X 0x00
|
JNIEXPORT jboolean JNICALL
|
||||||
#define REL_Y 0x01
|
Java_com_limelight_binding_input_evdev_EvdevReader_grab(JNIEnv *env, jobject this, jint fd) {
|
||||||
#define KEY_Q 16
|
return ioctl(fd, EVIOCGRAB, 1) == 0;
|
||||||
#define BTN_LEFT 0x110
|
}
|
||||||
#define BTN_GAMEPAD 0x130
|
|
||||||
|
|
||||||
struct DeviceEntry {
|
JNIEXPORT jboolean JNICALL
|
||||||
struct DeviceEntry *next;
|
Java_com_limelight_binding_input_evdev_EvdevReader_ungrab(JNIEnv *env, jobject this, jint fd) {
|
||||||
pthread_t thread;
|
return ioctl(fd, EVIOCGRAB, 0) == 0;
|
||||||
int fd;
|
}
|
||||||
char devName[128];
|
|
||||||
};
|
|
||||||
|
|
||||||
static struct DeviceEntry *DeviceListHead;
|
// has*() and friends are based on Android's EventHub.cpp
|
||||||
static int grabbing = 1;
|
|
||||||
static pthread_mutex_t DeviceListLock = PTHREAD_MUTEX_INITIALIZER;
|
|
||||||
static pthread_mutex_t SocketSendLock = PTHREAD_MUTEX_INITIALIZER;
|
|
||||||
static int sock;
|
|
||||||
|
|
||||||
// This is a small executable that runs in a root shell. It reads input
|
|
||||||
// devices and writes the evdev output packets to a socket. This allows
|
|
||||||
// Moonlight to read input devices without having to muck with changing
|
|
||||||
// device permissions or modifying SELinux policy (which is prevented in
|
|
||||||
// Marshmallow anyway).
|
|
||||||
|
|
||||||
#define test_bit(bit, array) (array[bit/8] & (1<<(bit%8)))
|
#define test_bit(bit, array) (array[bit/8] & (1<<(bit%8)))
|
||||||
|
|
||||||
static int hasRelAxis(int fd, short axis) {
|
JNIEXPORT jboolean JNICALL
|
||||||
|
Java_com_limelight_binding_input_evdev_EvdevReader_hasRelAxis(JNIEnv *env, jobject this, jint fd, jshort axis) {
|
||||||
unsigned char relBitmask[(REL_MAX + 1) / 8];
|
unsigned char relBitmask[(REL_MAX + 1) / 8];
|
||||||
|
|
||||||
ioctl(fd, EVIOCGBIT(EV_REL, sizeof(relBitmask)), relBitmask);
|
ioctl(fd, EVIOCGBIT(EV_REL, sizeof(relBitmask)), relBitmask);
|
||||||
|
|
||||||
return test_bit(axis, relBitmask);
|
return test_bit(axis, relBitmask);
|
||||||
}
|
}
|
||||||
|
|
||||||
static int hasKey(int fd, short key) {
|
JNIEXPORT jboolean JNICALL
|
||||||
|
Java_com_limelight_binding_input_evdev_EvdevReader_hasAbsAxis(JNIEnv *env, jobject this, jint fd, jshort axis) {
|
||||||
|
unsigned char absBitmask[(ABS_MAX + 1) / 8];
|
||||||
|
|
||||||
|
ioctl(fd, EVIOCGBIT(EV_ABS, sizeof(absBitmask)), absBitmask);
|
||||||
|
|
||||||
|
return test_bit(axis, absBitmask);
|
||||||
|
}
|
||||||
|
|
||||||
|
JNIEXPORT jboolean JNICALL
|
||||||
|
Java_com_limelight_binding_input_evdev_EvdevReader_hasKey(JNIEnv *env, jobject this, jint fd, jshort key) {
|
||||||
unsigned char keyBitmask[(KEY_MAX + 1) / 8];
|
unsigned char keyBitmask[(KEY_MAX + 1) / 8];
|
||||||
|
|
||||||
ioctl(fd, EVIOCGBIT(EV_KEY, sizeof(keyBitmask)), keyBitmask);
|
ioctl(fd, EVIOCGBIT(EV_KEY, sizeof(keyBitmask)), keyBitmask);
|
||||||
|
|
||||||
return test_bit(key, keyBitmask);
|
return test_bit(key, keyBitmask);
|
||||||
}
|
}
|
||||||
|
|
||||||
static void outputEvdevData(char *data, int dataSize) {
|
JNIEXPORT jint JNICALL
|
||||||
char packetBuffer[EVDEV_MAX_EVENT_SIZE + sizeof(dataSize)];
|
Java_com_limelight_binding_input_evdev_EvdevReader_read(JNIEnv *env, jobject this, jint fd, jbyteArray buffer) {
|
||||||
|
jint ret;
|
||||||
// Copy the full packet into our buffer
|
jbyte *data;
|
||||||
memcpy(packetBuffer, &dataSize, sizeof(dataSize));
|
|
||||||
memcpy(&packetBuffer[sizeof(dataSize)], data, dataSize);
|
|
||||||
|
|
||||||
// Lock to prevent other threads from sending at the same time
|
|
||||||
pthread_mutex_lock(&SocketSendLock);
|
|
||||||
send(sock, packetBuffer, dataSize + sizeof(dataSize), 0);
|
|
||||||
pthread_mutex_unlock(&SocketSendLock);
|
|
||||||
}
|
|
||||||
|
|
||||||
void* pollThreadFunc(void* context) {
|
|
||||||
struct DeviceEntry *device = context;
|
|
||||||
struct pollfd pollinfo;
|
|
||||||
int pollres, ret;
|
|
||||||
char data[EVDEV_MAX_EVENT_SIZE];
|
|
||||||
|
|
||||||
__android_log_print(ANDROID_LOG_INFO, "EvdevReader", "Polling /dev/input/%s", device->devName);
|
|
||||||
|
|
||||||
if (grabbing) {
|
|
||||||
// Exclusively grab the input device (required to make the Android cursor disappear)
|
|
||||||
if (ioctl(device->fd, EVIOCGRAB, 1) < 0) {
|
|
||||||
__android_log_print(ANDROID_LOG_ERROR, "EvdevReader",
|
|
||||||
"EVIOCGRAB failed for %s: %d", device->devName, errno);
|
|
||||||
goto cleanup;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (;;) {
|
|
||||||
do {
|
|
||||||
// Unwait every 250 ms to return to caller if the fd is closed
|
|
||||||
pollinfo.fd = device->fd;
|
|
||||||
pollinfo.events = POLLIN;
|
|
||||||
pollinfo.revents = 0;
|
|
||||||
pollres = poll(&pollinfo, 1, 250);
|
|
||||||
}
|
|
||||||
while (pollres == 0);
|
|
||||||
|
|
||||||
if (pollres > 0 && (pollinfo.revents & POLLIN)) {
|
|
||||||
// We'll have data available now
|
|
||||||
ret = read(device->fd, data, EVDEV_MAX_EVENT_SIZE);
|
|
||||||
if (ret < 0) {
|
|
||||||
__android_log_print(ANDROID_LOG_ERROR, "EvdevReader",
|
|
||||||
"read() failed: %d", errno);
|
|
||||||
goto cleanup;
|
|
||||||
}
|
|
||||||
else if (ret == 0) {
|
|
||||||
__android_log_print(ANDROID_LOG_ERROR, "EvdevReader",
|
|
||||||
"read() graceful EOF");
|
|
||||||
goto cleanup;
|
|
||||||
}
|
|
||||||
else if (grabbing) {
|
|
||||||
// Write out the data to our client
|
|
||||||
outputEvdevData(data, ret);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
if (pollres < 0) {
|
|
||||||
__android_log_print(ANDROID_LOG_ERROR, "EvdevReader",
|
|
||||||
"poll() failed: %d", errno);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
__android_log_print(ANDROID_LOG_ERROR, "EvdevReader",
|
|
||||||
"Unexpected revents: %d", pollinfo.revents);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Terminate this thread
|
|
||||||
goto cleanup;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
cleanup:
|
|
||||||
__android_log_print(ANDROID_LOG_INFO, "EvdevReader", "Closing /dev/input/%s", device->devName);
|
|
||||||
|
|
||||||
// Remove the context from the linked list
|
|
||||||
{
|
|
||||||
struct DeviceEntry *lastEntry;
|
|
||||||
|
|
||||||
// Lock the device list
|
|
||||||
pthread_mutex_lock(&DeviceListLock);
|
|
||||||
|
|
||||||
if (DeviceListHead == device) {
|
|
||||||
DeviceListHead = device->next;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
lastEntry = DeviceListHead;
|
|
||||||
while (lastEntry->next != NULL) {
|
|
||||||
if (lastEntry->next == device) {
|
|
||||||
lastEntry->next = device->next;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
lastEntry = lastEntry->next;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Unlock device list
|
|
||||||
pthread_mutex_unlock(&DeviceListLock);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Free the context
|
|
||||||
ioctl(device->fd, EVIOCGRAB, 0);
|
|
||||||
close(device->fd);
|
|
||||||
free(device);
|
|
||||||
|
|
||||||
return NULL;
|
|
||||||
}
|
|
||||||
|
|
||||||
static int precheckDeviceForPolling(int fd) {
|
|
||||||
int isMouse;
|
|
||||||
int isKeyboard;
|
|
||||||
int isGamepad;
|
|
||||||
|
|
||||||
// This is the same check that Android does in EventHub.cpp
|
|
||||||
isMouse = hasRelAxis(fd, REL_X) &&
|
|
||||||
hasRelAxis(fd, REL_Y) &&
|
|
||||||
hasKey(fd, BTN_LEFT);
|
|
||||||
|
|
||||||
// This is the same check that Android does in EventHub.cpp
|
|
||||||
isKeyboard = hasKey(fd, KEY_Q);
|
|
||||||
|
|
||||||
isGamepad = hasKey(fd, BTN_GAMEPAD);
|
|
||||||
|
|
||||||
// We only handle keyboards and mice that aren't gamepads
|
|
||||||
return (isMouse || isKeyboard) && !isGamepad;
|
|
||||||
}
|
|
||||||
|
|
||||||
static void startPollForDevice(char* deviceName) {
|
|
||||||
struct DeviceEntry *currentEntry;
|
|
||||||
char fullPath[256];
|
|
||||||
int fd;
|
|
||||||
|
|
||||||
// Lock the device list
|
|
||||||
pthread_mutex_lock(&DeviceListLock);
|
|
||||||
|
|
||||||
// Check if the device is already being polled
|
|
||||||
currentEntry = DeviceListHead;
|
|
||||||
while (currentEntry != NULL) {
|
|
||||||
if (strcmp(currentEntry->devName, deviceName) == 0) {
|
|
||||||
// Already polling this device
|
|
||||||
goto unlock;
|
|
||||||
}
|
|
||||||
|
|
||||||
currentEntry = currentEntry->next;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Open the device
|
|
||||||
sprintf(fullPath, "/dev/input/%s", deviceName);
|
|
||||||
fd = open(fullPath, O_RDWR);
|
|
||||||
if (fd < 0) {
|
|
||||||
__android_log_print(ANDROID_LOG_ERROR, "EvdevReader", "Couldn't open %s: %d", fullPath, errno);
|
|
||||||
goto unlock;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Allocate a context
|
|
||||||
currentEntry = malloc(sizeof(*currentEntry));
|
|
||||||
if (currentEntry == NULL) {
|
|
||||||
close(fd);
|
|
||||||
goto unlock;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Populate context
|
|
||||||
currentEntry->fd = fd;
|
|
||||||
strcpy(currentEntry->devName, deviceName);
|
|
||||||
|
|
||||||
// Check if we support polling this device
|
|
||||||
if (!precheckDeviceForPolling(fd)) {
|
|
||||||
// Nope, get out
|
|
||||||
free(currentEntry);
|
|
||||||
close(fd);
|
|
||||||
goto unlock;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Start the polling thread
|
|
||||||
if (pthread_create(¤tEntry->thread, NULL, pollThreadFunc, currentEntry) != 0) {
|
|
||||||
free(currentEntry);
|
|
||||||
close(fd);
|
|
||||||
goto unlock;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Queue this onto the device list
|
|
||||||
currentEntry->next = DeviceListHead;
|
|
||||||
DeviceListHead = currentEntry;
|
|
||||||
|
|
||||||
unlock:
|
|
||||||
// Unlock and return
|
|
||||||
pthread_mutex_unlock(&DeviceListLock);
|
|
||||||
}
|
|
||||||
|
|
||||||
static int enumerateDevices(void) {
|
|
||||||
DIR *inputDir;
|
|
||||||
struct dirent *dirEnt;
|
|
||||||
|
|
||||||
inputDir = opendir("/dev/input");
|
|
||||||
if (!inputDir) {
|
|
||||||
__android_log_print(ANDROID_LOG_ERROR, "EvdevReader", "Couldn't open /dev/input: %d", errno);
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Start polling each device in /dev/input
|
|
||||||
while ((dirEnt = readdir(inputDir)) != NULL) {
|
|
||||||
if (strcmp(dirEnt->d_name, ".") == 0 || strcmp(dirEnt->d_name, "..") == 0) {
|
|
||||||
// Skip these virtual directories
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (strstr(dirEnt->d_name, "event") == NULL) {
|
|
||||||
// Skip non-event devices
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
startPollForDevice(dirEnt->d_name);
|
|
||||||
}
|
|
||||||
|
|
||||||
closedir(inputDir);
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
static int connectSocket(int port) {
|
|
||||||
struct sockaddr_in saddr;
|
|
||||||
int ret;
|
|
||||||
int val;
|
|
||||||
|
|
||||||
sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
|
|
||||||
if (sock < 0) {
|
|
||||||
__android_log_print(ANDROID_LOG_ERROR, "EvdevReader", "socket() failed: %d", errno);
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
memset(&saddr, 0, sizeof(saddr));
|
|
||||||
saddr.sin_family = AF_INET;
|
|
||||||
saddr.sin_port = htons(port);
|
|
||||||
saddr.sin_addr.s_addr = inet_addr("127.0.0.1");
|
|
||||||
ret = connect(sock, (struct sockaddr*)&saddr, sizeof(saddr));
|
|
||||||
if (ret < 0) {
|
|
||||||
__android_log_print(ANDROID_LOG_ERROR, "EvdevReader", "connect() failed: %d", errno);
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
val = 1;
|
|
||||||
ret = setsockopt(sock, IPPROTO_TCP, TCP_NODELAY, (char*)&val, sizeof(val));
|
|
||||||
if (ret < 0) {
|
|
||||||
__android_log_print(ANDROID_LOG_ERROR, "EvdevReader", "setsockopt() failed: %d", errno);
|
|
||||||
// We can continue anyways
|
|
||||||
}
|
|
||||||
|
|
||||||
__android_log_print(ANDROID_LOG_INFO, "EvdevReader", "Connection established to port %d", port);
|
|
||||||
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
#define UNGRAB_REQ 1
|
|
||||||
#define REGRAB_REQ 2
|
|
||||||
|
|
||||||
int main(int argc, char* argv[]) {
|
|
||||||
int ret;
|
|
||||||
int pollres;
|
int pollres;
|
||||||
struct pollfd pollinfo;
|
struct pollfd pollinfo;
|
||||||
int port;
|
|
||||||
|
data = (*env)->GetByteArrayElements(env, buffer, NULL);
|
||||||
__android_log_print(ANDROID_LOG_INFO, "EvdevReader", "Entered main()");
|
if (data == NULL) {
|
||||||
|
__android_log_print(ANDROID_LOG_ERROR, "EvdevReader",
|
||||||
port = atoi(argv[1]);
|
"Failed to get byte array");
|
||||||
__android_log_print(ANDROID_LOG_INFO, "EvdevReader", "Requested port number: %d", port);
|
return -1;
|
||||||
|
|
||||||
// Connect to the app's socket
|
|
||||||
ret = connectSocket(port);
|
|
||||||
if (ret < 0) {
|
|
||||||
return ret;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Perform initial enumeration
|
do
|
||||||
ret = enumerateDevices();
|
{
|
||||||
if (ret < 0) {
|
// Unwait every 250 ms to return to caller if the fd is closed
|
||||||
return ret;
|
pollinfo.fd = fd;
|
||||||
|
pollinfo.events = POLLIN;
|
||||||
|
pollinfo.revents = 0;
|
||||||
|
pollres = poll(&pollinfo, 1, 250);
|
||||||
}
|
}
|
||||||
|
while (pollres == 0);
|
||||||
// Wait for requests from the client
|
|
||||||
for (;;) {
|
if (pollres > 0 && (pollinfo.revents & POLLIN)) {
|
||||||
unsigned char requestId;
|
// We'll have data available now
|
||||||
|
ret = read(fd, data, sizeof(struct input_event));
|
||||||
do {
|
if (ret < 0) {
|
||||||
// Every second we poll again for new devices if
|
__android_log_print(ANDROID_LOG_ERROR, "EvdevReader",
|
||||||
// we haven't received any new events
|
"read() failed: %d", errno);
|
||||||
pollinfo.fd = sock;
|
|
||||||
pollinfo.events = POLLIN;
|
|
||||||
pollinfo.revents = 0;
|
|
||||||
pollres = poll(&pollinfo, 1, 1000);
|
|
||||||
if (pollres == 0) {
|
|
||||||
// Timeout, re-enumerate devices
|
|
||||||
enumerateDevices();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
while (pollres == 0);
|
}
|
||||||
|
else {
|
||||||
if (pollres > 0 && (pollinfo.revents & POLLIN)) {
|
// There must have been a failure
|
||||||
// We'll have data available now
|
ret = -1;
|
||||||
ret = recv(sock, &requestId, sizeof(requestId), 0);
|
|
||||||
if (ret < sizeof(requestId)) {
|
if (pollres < 0) {
|
||||||
__android_log_print(ANDROID_LOG_ERROR, "EvdevReader", "Short read on socket");
|
__android_log_print(ANDROID_LOG_ERROR, "EvdevReader",
|
||||||
return errno;
|
"poll() failed: %d", errno);
|
||||||
}
|
|
||||||
|
|
||||||
if (requestId != UNGRAB_REQ && requestId != REGRAB_REQ) {
|
|
||||||
__android_log_print(ANDROID_LOG_ERROR, "EvdevReader", "Unknown request");
|
|
||||||
return requestId;
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
struct DeviceEntry *currentEntry;
|
|
||||||
|
|
||||||
pthread_mutex_lock(&DeviceListLock);
|
|
||||||
|
|
||||||
// Update state for future devices
|
|
||||||
grabbing = (requestId == REGRAB_REQ);
|
|
||||||
|
|
||||||
// Carry out the requested action on each device
|
|
||||||
currentEntry = DeviceListHead;
|
|
||||||
while (currentEntry != NULL) {
|
|
||||||
ioctl(currentEntry->fd, EVIOCGRAB, grabbing);
|
|
||||||
currentEntry = currentEntry->next;
|
|
||||||
}
|
|
||||||
|
|
||||||
pthread_mutex_unlock(&DeviceListLock);
|
|
||||||
|
|
||||||
__android_log_print(ANDROID_LOG_INFO, "EvdevReader", "New grab status is: %s",
|
|
||||||
grabbing ? "enabled" : "disabled");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
// Terminate this thread
|
__android_log_print(ANDROID_LOG_ERROR, "EvdevReader",
|
||||||
if (pollres < 0) {
|
"Unexpected revents: %d", pollinfo.revents);
|
||||||
__android_log_print(ANDROID_LOG_ERROR, "EvdevReader",
|
|
||||||
"Socket recv poll() failed: %d", errno);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
__android_log_print(ANDROID_LOG_ERROR, "EvdevReader",
|
|
||||||
"Socket poll unexpected revents: %d", pollinfo.revents);
|
|
||||||
}
|
|
||||||
|
|
||||||
return -1;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
(*env)->ReleaseByteArrayElements(env, buffer, data, 0);
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
JNIEXPORT jint JNICALL
|
||||||
|
Java_com_limelight_binding_input_evdev_EvdevReader_close(JNIEnv *env, jobject this, jint fd) {
|
||||||
|
return close(fd);
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
# Android.mk for Limelight's H264 decoder
|
||||||
|
MY_LOCAL_PATH := $(call my-dir)
|
||||||
|
|
||||||
|
include $(call all-subdir-makefiles)
|
||||||
|
|
||||||
|
LOCAL_PATH := $(MY_LOCAL_PATH)
|
||||||
|
|
||||||
|
include $(CLEAR_VARS)
|
||||||
|
LOCAL_MODULE := nv_avc_dec
|
||||||
|
LOCAL_SRC_FILES := nv_avc_dec.c nv_avc_dec_jni.c
|
||||||
|
LOCAL_C_INCLUDES := $(LOCAL_PATH)/ffmpeg/$(TARGET_ARCH_ABI)/include
|
||||||
|
LOCAL_LDLIBS := -L$(SYSROOT)/usr/lib -llog -landroid
|
||||||
|
|
||||||
|
# Link to ffmpeg libraries
|
||||||
|
LOCAL_SHARED_LIBRARIES := libavcodec libavformat libswscale libavutil libwsresample
|
||||||
|
|
||||||
|
include $(BUILD_SHARED_LIBRARY)
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
LOCAL_PATH:= $(call my-dir)
|
||||||
|
|
||||||
|
include $(CLEAR_VARS)
|
||||||
|
LOCAL_MODULE:= libavcodec
|
||||||
|
LOCAL_SRC_FILES:= $(TARGET_ARCH_ABI)/lib/libavcodec-55.so
|
||||||
|
LOCAL_EXPORT_C_INCLUDES := $(LOCAL_PATH)/include
|
||||||
|
include $(PREBUILT_SHARED_LIBRARY)
|
||||||
|
|
||||||
|
include $(CLEAR_VARS)
|
||||||
|
LOCAL_MODULE:= libavformat
|
||||||
|
LOCAL_SRC_FILES:= $(TARGET_ARCH_ABI)/lib/libavformat-55.so
|
||||||
|
LOCAL_EXPORT_C_INCLUDES := $(LOCAL_PATH)/include
|
||||||
|
include $(PREBUILT_SHARED_LIBRARY)
|
||||||
|
|
||||||
|
include $(CLEAR_VARS)
|
||||||
|
LOCAL_MODULE:= libswscale
|
||||||
|
LOCAL_SRC_FILES:= $(TARGET_ARCH_ABI)/lib/libswscale-2.so
|
||||||
|
LOCAL_EXPORT_C_INCLUDES := $(LOCAL_PATH)/include
|
||||||
|
include $(PREBUILT_SHARED_LIBRARY)
|
||||||
|
|
||||||
|
include $(CLEAR_VARS)
|
||||||
|
LOCAL_MODULE:= libavutil
|
||||||
|
LOCAL_SRC_FILES:= $(TARGET_ARCH_ABI)/lib/libavutil-52.so
|
||||||
|
LOCAL_EXPORT_C_INCLUDES := $(LOCAL_PATH)/include
|
||||||
|
include $(PREBUILT_SHARED_LIBRARY)
|
||||||
|
|
||||||
|
include $(CLEAR_VARS)
|
||||||
|
LOCAL_MODULE:= libwsresample
|
||||||
|
LOCAL_SRC_FILES:= $(TARGET_ARCH_ABI)/lib/libswresample-0.so
|
||||||
|
LOCAL_EXPORT_C_INCLUDES := $(LOCAL_PATH)/include
|
||||||
|
include $(PREBUILT_SHARED_LIBRARY)
|
||||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,116 @@
|
|||||||
|
/*
|
||||||
|
* This file is part of FFmpeg.
|
||||||
|
*
|
||||||
|
* FFmpeg is free software; you can redistribute it and/or
|
||||||
|
* modify it under the terms of the GNU Lesser General Public
|
||||||
|
* License as published by the Free Software Foundation; either
|
||||||
|
* version 2.1 of the License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* FFmpeg is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||||
|
* Lesser General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Lesser General Public
|
||||||
|
* License along with FFmpeg; if not, write to the Free Software
|
||||||
|
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef AVCODEC_AVFFT_H
|
||||||
|
#define AVCODEC_AVFFT_H
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @file
|
||||||
|
* @ingroup lavc_fft
|
||||||
|
* FFT functions
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @defgroup lavc_fft FFT functions
|
||||||
|
* @ingroup lavc_misc
|
||||||
|
*
|
||||||
|
* @{
|
||||||
|
*/
|
||||||
|
|
||||||
|
typedef float FFTSample;
|
||||||
|
|
||||||
|
typedef struct FFTComplex {
|
||||||
|
FFTSample re, im;
|
||||||
|
} FFTComplex;
|
||||||
|
|
||||||
|
typedef struct FFTContext FFTContext;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set up a complex FFT.
|
||||||
|
* @param nbits log2 of the length of the input array
|
||||||
|
* @param inverse if 0 perform the forward transform, if 1 perform the inverse
|
||||||
|
*/
|
||||||
|
FFTContext *av_fft_init(int nbits, int inverse);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Do the permutation needed BEFORE calling ff_fft_calc().
|
||||||
|
*/
|
||||||
|
void av_fft_permute(FFTContext *s, FFTComplex *z);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Do a complex FFT with the parameters defined in av_fft_init(). The
|
||||||
|
* input data must be permuted before. No 1.0/sqrt(n) normalization is done.
|
||||||
|
*/
|
||||||
|
void av_fft_calc(FFTContext *s, FFTComplex *z);
|
||||||
|
|
||||||
|
void av_fft_end(FFTContext *s);
|
||||||
|
|
||||||
|
FFTContext *av_mdct_init(int nbits, int inverse, double scale);
|
||||||
|
void av_imdct_calc(FFTContext *s, FFTSample *output, const FFTSample *input);
|
||||||
|
void av_imdct_half(FFTContext *s, FFTSample *output, const FFTSample *input);
|
||||||
|
void av_mdct_calc(FFTContext *s, FFTSample *output, const FFTSample *input);
|
||||||
|
void av_mdct_end(FFTContext *s);
|
||||||
|
|
||||||
|
/* Real Discrete Fourier Transform */
|
||||||
|
|
||||||
|
enum RDFTransformType {
|
||||||
|
DFT_R2C,
|
||||||
|
IDFT_C2R,
|
||||||
|
IDFT_R2C,
|
||||||
|
DFT_C2R,
|
||||||
|
};
|
||||||
|
|
||||||
|
typedef struct RDFTContext RDFTContext;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set up a real FFT.
|
||||||
|
* @param nbits log2 of the length of the input array
|
||||||
|
* @param trans the type of transform
|
||||||
|
*/
|
||||||
|
RDFTContext *av_rdft_init(int nbits, enum RDFTransformType trans);
|
||||||
|
void av_rdft_calc(RDFTContext *s, FFTSample *data);
|
||||||
|
void av_rdft_end(RDFTContext *s);
|
||||||
|
|
||||||
|
/* Discrete Cosine Transform */
|
||||||
|
|
||||||
|
typedef struct DCTContext DCTContext;
|
||||||
|
|
||||||
|
enum DCTTransformType {
|
||||||
|
DCT_II = 0,
|
||||||
|
DCT_III,
|
||||||
|
DCT_I,
|
||||||
|
DST_I,
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set up DCT.
|
||||||
|
* @param nbits size of the input array:
|
||||||
|
* (1 << nbits) for DCT-II, DCT-III and DST-I
|
||||||
|
* (1 << nbits) + 1 for DCT-I
|
||||||
|
*
|
||||||
|
* @note the first element of the input of DST-I is ignored
|
||||||
|
*/
|
||||||
|
DCTContext *av_dct_init(int nbits, enum DCTTransformType type);
|
||||||
|
void av_dct_calc(DCTContext *s, FFTSample *data);
|
||||||
|
void av_dct_end (DCTContext *s);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @}
|
||||||
|
*/
|
||||||
|
|
||||||
|
#endif /* AVCODEC_AVFFT_H */
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
/*
|
||||||
|
* DXVA2 HW acceleration
|
||||||
|
*
|
||||||
|
* copyright (c) 2009 Laurent Aimar
|
||||||
|
*
|
||||||
|
* This file is part of FFmpeg.
|
||||||
|
*
|
||||||
|
* FFmpeg is free software; you can redistribute it and/or
|
||||||
|
* modify it under the terms of the GNU Lesser General Public
|
||||||
|
* License as published by the Free Software Foundation; either
|
||||||
|
* version 2.1 of the License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* FFmpeg is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||||
|
* Lesser General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Lesser General Public
|
||||||
|
* License along with FFmpeg; if not, write to the Free Software
|
||||||
|
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef AVCODEC_DXVA_H
|
||||||
|
#define AVCODEC_DXVA_H
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @file
|
||||||
|
* @ingroup lavc_codec_hwaccel_dxva2
|
||||||
|
* Public libavcodec DXVA2 header.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#if defined(_WIN32_WINNT) && _WIN32_WINNT < 0x0600
|
||||||
|
#undef _WIN32_WINNT
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#if !defined(_WIN32_WINNT)
|
||||||
|
#define _WIN32_WINNT 0x0600
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <d3d9.h>
|
||||||
|
#include <dxva2api.h>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @defgroup lavc_codec_hwaccel_dxva2 DXVA2
|
||||||
|
* @ingroup lavc_codec_hwaccel
|
||||||
|
*
|
||||||
|
* @{
|
||||||
|
*/
|
||||||
|
|
||||||
|
#define FF_DXVA2_WORKAROUND_SCALING_LIST_ZIGZAG 1 ///< Work around for DXVA2 and old UVD/UVD+ ATI video cards
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This structure is used to provides the necessary configurations and data
|
||||||
|
* to the DXVA2 FFmpeg HWAccel implementation.
|
||||||
|
*
|
||||||
|
* The application must make it available as AVCodecContext.hwaccel_context.
|
||||||
|
*/
|
||||||
|
struct dxva_context {
|
||||||
|
/**
|
||||||
|
* DXVA2 decoder object
|
||||||
|
*/
|
||||||
|
IDirectXVideoDecoder *decoder;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DXVA2 configuration used to create the decoder
|
||||||
|
*/
|
||||||
|
const DXVA2_ConfigPictureDecode *cfg;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The number of surface in the surface array
|
||||||
|
*/
|
||||||
|
unsigned surface_count;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The array of Direct3D surfaces used to create the decoder
|
||||||
|
*/
|
||||||
|
LPDIRECT3DSURFACE9 *surface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A bit field configuring the workarounds needed for using the decoder
|
||||||
|
*/
|
||||||
|
uint64_t workaround;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Private to the FFmpeg AVHWAccel implementation
|
||||||
|
*/
|
||||||
|
unsigned report_id;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @}
|
||||||
|
*/
|
||||||
|
|
||||||
|
#endif /* AVCODEC_DXVA_H */
|
||||||
@@ -0,0 +1,397 @@
|
|||||||
|
/*
|
||||||
|
* This file is part of FFmpeg.
|
||||||
|
*
|
||||||
|
* FFmpeg is free software; you can redistribute it and/or
|
||||||
|
* modify it under the terms of the GNU Lesser General Public
|
||||||
|
* License as published by the Free Software Foundation; either
|
||||||
|
* version 2.1 of the License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* FFmpeg is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||||
|
* Lesser General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Lesser General Public
|
||||||
|
* License along with FFmpeg; if not, write to the Free Software
|
||||||
|
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef AVCODEC_OLD_CODEC_IDS_H
|
||||||
|
#define AVCODEC_OLD_CODEC_IDS_H
|
||||||
|
|
||||||
|
#include "libavutil/common.h"
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This header exists to prevent new codec IDs from being accidentally added to
|
||||||
|
* the deprecated list.
|
||||||
|
* Do not include it directly. It will be removed on next major bump
|
||||||
|
*
|
||||||
|
* Do not add new items to this list. Use the AVCodecID enum instead.
|
||||||
|
*/
|
||||||
|
|
||||||
|
CODEC_ID_NONE = AV_CODEC_ID_NONE,
|
||||||
|
|
||||||
|
/* video codecs */
|
||||||
|
CODEC_ID_MPEG1VIDEO,
|
||||||
|
CODEC_ID_MPEG2VIDEO, ///< preferred ID for MPEG-1/2 video decoding
|
||||||
|
CODEC_ID_MPEG2VIDEO_XVMC,
|
||||||
|
CODEC_ID_H261,
|
||||||
|
CODEC_ID_H263,
|
||||||
|
CODEC_ID_RV10,
|
||||||
|
CODEC_ID_RV20,
|
||||||
|
CODEC_ID_MJPEG,
|
||||||
|
CODEC_ID_MJPEGB,
|
||||||
|
CODEC_ID_LJPEG,
|
||||||
|
CODEC_ID_SP5X,
|
||||||
|
CODEC_ID_JPEGLS,
|
||||||
|
CODEC_ID_MPEG4,
|
||||||
|
CODEC_ID_RAWVIDEO,
|
||||||
|
CODEC_ID_MSMPEG4V1,
|
||||||
|
CODEC_ID_MSMPEG4V2,
|
||||||
|
CODEC_ID_MSMPEG4V3,
|
||||||
|
CODEC_ID_WMV1,
|
||||||
|
CODEC_ID_WMV2,
|
||||||
|
CODEC_ID_H263P,
|
||||||
|
CODEC_ID_H263I,
|
||||||
|
CODEC_ID_FLV1,
|
||||||
|
CODEC_ID_SVQ1,
|
||||||
|
CODEC_ID_SVQ3,
|
||||||
|
CODEC_ID_DVVIDEO,
|
||||||
|
CODEC_ID_HUFFYUV,
|
||||||
|
CODEC_ID_CYUV,
|
||||||
|
CODEC_ID_H264,
|
||||||
|
CODEC_ID_INDEO3,
|
||||||
|
CODEC_ID_VP3,
|
||||||
|
CODEC_ID_THEORA,
|
||||||
|
CODEC_ID_ASV1,
|
||||||
|
CODEC_ID_ASV2,
|
||||||
|
CODEC_ID_FFV1,
|
||||||
|
CODEC_ID_4XM,
|
||||||
|
CODEC_ID_VCR1,
|
||||||
|
CODEC_ID_CLJR,
|
||||||
|
CODEC_ID_MDEC,
|
||||||
|
CODEC_ID_ROQ,
|
||||||
|
CODEC_ID_INTERPLAY_VIDEO,
|
||||||
|
CODEC_ID_XAN_WC3,
|
||||||
|
CODEC_ID_XAN_WC4,
|
||||||
|
CODEC_ID_RPZA,
|
||||||
|
CODEC_ID_CINEPAK,
|
||||||
|
CODEC_ID_WS_VQA,
|
||||||
|
CODEC_ID_MSRLE,
|
||||||
|
CODEC_ID_MSVIDEO1,
|
||||||
|
CODEC_ID_IDCIN,
|
||||||
|
CODEC_ID_8BPS,
|
||||||
|
CODEC_ID_SMC,
|
||||||
|
CODEC_ID_FLIC,
|
||||||
|
CODEC_ID_TRUEMOTION1,
|
||||||
|
CODEC_ID_VMDVIDEO,
|
||||||
|
CODEC_ID_MSZH,
|
||||||
|
CODEC_ID_ZLIB,
|
||||||
|
CODEC_ID_QTRLE,
|
||||||
|
CODEC_ID_TSCC,
|
||||||
|
CODEC_ID_ULTI,
|
||||||
|
CODEC_ID_QDRAW,
|
||||||
|
CODEC_ID_VIXL,
|
||||||
|
CODEC_ID_QPEG,
|
||||||
|
CODEC_ID_PNG,
|
||||||
|
CODEC_ID_PPM,
|
||||||
|
CODEC_ID_PBM,
|
||||||
|
CODEC_ID_PGM,
|
||||||
|
CODEC_ID_PGMYUV,
|
||||||
|
CODEC_ID_PAM,
|
||||||
|
CODEC_ID_FFVHUFF,
|
||||||
|
CODEC_ID_RV30,
|
||||||
|
CODEC_ID_RV40,
|
||||||
|
CODEC_ID_VC1,
|
||||||
|
CODEC_ID_WMV3,
|
||||||
|
CODEC_ID_LOCO,
|
||||||
|
CODEC_ID_WNV1,
|
||||||
|
CODEC_ID_AASC,
|
||||||
|
CODEC_ID_INDEO2,
|
||||||
|
CODEC_ID_FRAPS,
|
||||||
|
CODEC_ID_TRUEMOTION2,
|
||||||
|
CODEC_ID_BMP,
|
||||||
|
CODEC_ID_CSCD,
|
||||||
|
CODEC_ID_MMVIDEO,
|
||||||
|
CODEC_ID_ZMBV,
|
||||||
|
CODEC_ID_AVS,
|
||||||
|
CODEC_ID_SMACKVIDEO,
|
||||||
|
CODEC_ID_NUV,
|
||||||
|
CODEC_ID_KMVC,
|
||||||
|
CODEC_ID_FLASHSV,
|
||||||
|
CODEC_ID_CAVS,
|
||||||
|
CODEC_ID_JPEG2000,
|
||||||
|
CODEC_ID_VMNC,
|
||||||
|
CODEC_ID_VP5,
|
||||||
|
CODEC_ID_VP6,
|
||||||
|
CODEC_ID_VP6F,
|
||||||
|
CODEC_ID_TARGA,
|
||||||
|
CODEC_ID_DSICINVIDEO,
|
||||||
|
CODEC_ID_TIERTEXSEQVIDEO,
|
||||||
|
CODEC_ID_TIFF,
|
||||||
|
CODEC_ID_GIF,
|
||||||
|
CODEC_ID_DXA,
|
||||||
|
CODEC_ID_DNXHD,
|
||||||
|
CODEC_ID_THP,
|
||||||
|
CODEC_ID_SGI,
|
||||||
|
CODEC_ID_C93,
|
||||||
|
CODEC_ID_BETHSOFTVID,
|
||||||
|
CODEC_ID_PTX,
|
||||||
|
CODEC_ID_TXD,
|
||||||
|
CODEC_ID_VP6A,
|
||||||
|
CODEC_ID_AMV,
|
||||||
|
CODEC_ID_VB,
|
||||||
|
CODEC_ID_PCX,
|
||||||
|
CODEC_ID_SUNRAST,
|
||||||
|
CODEC_ID_INDEO4,
|
||||||
|
CODEC_ID_INDEO5,
|
||||||
|
CODEC_ID_MIMIC,
|
||||||
|
CODEC_ID_RL2,
|
||||||
|
CODEC_ID_ESCAPE124,
|
||||||
|
CODEC_ID_DIRAC,
|
||||||
|
CODEC_ID_BFI,
|
||||||
|
CODEC_ID_CMV,
|
||||||
|
CODEC_ID_MOTIONPIXELS,
|
||||||
|
CODEC_ID_TGV,
|
||||||
|
CODEC_ID_TGQ,
|
||||||
|
CODEC_ID_TQI,
|
||||||
|
CODEC_ID_AURA,
|
||||||
|
CODEC_ID_AURA2,
|
||||||
|
CODEC_ID_V210X,
|
||||||
|
CODEC_ID_TMV,
|
||||||
|
CODEC_ID_V210,
|
||||||
|
CODEC_ID_DPX,
|
||||||
|
CODEC_ID_MAD,
|
||||||
|
CODEC_ID_FRWU,
|
||||||
|
CODEC_ID_FLASHSV2,
|
||||||
|
CODEC_ID_CDGRAPHICS,
|
||||||
|
CODEC_ID_R210,
|
||||||
|
CODEC_ID_ANM,
|
||||||
|
CODEC_ID_BINKVIDEO,
|
||||||
|
CODEC_ID_IFF_ILBM,
|
||||||
|
CODEC_ID_IFF_BYTERUN1,
|
||||||
|
CODEC_ID_KGV1,
|
||||||
|
CODEC_ID_YOP,
|
||||||
|
CODEC_ID_VP8,
|
||||||
|
CODEC_ID_PICTOR,
|
||||||
|
CODEC_ID_ANSI,
|
||||||
|
CODEC_ID_A64_MULTI,
|
||||||
|
CODEC_ID_A64_MULTI5,
|
||||||
|
CODEC_ID_R10K,
|
||||||
|
CODEC_ID_MXPEG,
|
||||||
|
CODEC_ID_LAGARITH,
|
||||||
|
CODEC_ID_PRORES,
|
||||||
|
CODEC_ID_JV,
|
||||||
|
CODEC_ID_DFA,
|
||||||
|
CODEC_ID_WMV3IMAGE,
|
||||||
|
CODEC_ID_VC1IMAGE,
|
||||||
|
CODEC_ID_UTVIDEO,
|
||||||
|
CODEC_ID_BMV_VIDEO,
|
||||||
|
CODEC_ID_VBLE,
|
||||||
|
CODEC_ID_DXTORY,
|
||||||
|
CODEC_ID_V410,
|
||||||
|
CODEC_ID_XWD,
|
||||||
|
CODEC_ID_CDXL,
|
||||||
|
CODEC_ID_XBM,
|
||||||
|
CODEC_ID_ZEROCODEC,
|
||||||
|
CODEC_ID_MSS1,
|
||||||
|
CODEC_ID_MSA1,
|
||||||
|
CODEC_ID_TSCC2,
|
||||||
|
CODEC_ID_MTS2,
|
||||||
|
CODEC_ID_CLLC,
|
||||||
|
CODEC_ID_Y41P = MKBETAG('Y','4','1','P'),
|
||||||
|
CODEC_ID_ESCAPE130 = MKBETAG('E','1','3','0'),
|
||||||
|
CODEC_ID_EXR = MKBETAG('0','E','X','R'),
|
||||||
|
CODEC_ID_AVRP = MKBETAG('A','V','R','P'),
|
||||||
|
|
||||||
|
CODEC_ID_G2M = MKBETAG( 0 ,'G','2','M'),
|
||||||
|
CODEC_ID_AVUI = MKBETAG('A','V','U','I'),
|
||||||
|
CODEC_ID_AYUV = MKBETAG('A','Y','U','V'),
|
||||||
|
CODEC_ID_V308 = MKBETAG('V','3','0','8'),
|
||||||
|
CODEC_ID_V408 = MKBETAG('V','4','0','8'),
|
||||||
|
CODEC_ID_YUV4 = MKBETAG('Y','U','V','4'),
|
||||||
|
CODEC_ID_SANM = MKBETAG('S','A','N','M'),
|
||||||
|
CODEC_ID_PAF_VIDEO = MKBETAG('P','A','F','V'),
|
||||||
|
CODEC_ID_SNOW = AV_CODEC_ID_SNOW,
|
||||||
|
|
||||||
|
/* various PCM "codecs" */
|
||||||
|
CODEC_ID_FIRST_AUDIO = 0x10000, ///< A dummy id pointing at the start of audio codecs
|
||||||
|
CODEC_ID_PCM_S16LE = 0x10000,
|
||||||
|
CODEC_ID_PCM_S16BE,
|
||||||
|
CODEC_ID_PCM_U16LE,
|
||||||
|
CODEC_ID_PCM_U16BE,
|
||||||
|
CODEC_ID_PCM_S8,
|
||||||
|
CODEC_ID_PCM_U8,
|
||||||
|
CODEC_ID_PCM_MULAW,
|
||||||
|
CODEC_ID_PCM_ALAW,
|
||||||
|
CODEC_ID_PCM_S32LE,
|
||||||
|
CODEC_ID_PCM_S32BE,
|
||||||
|
CODEC_ID_PCM_U32LE,
|
||||||
|
CODEC_ID_PCM_U32BE,
|
||||||
|
CODEC_ID_PCM_S24LE,
|
||||||
|
CODEC_ID_PCM_S24BE,
|
||||||
|
CODEC_ID_PCM_U24LE,
|
||||||
|
CODEC_ID_PCM_U24BE,
|
||||||
|
CODEC_ID_PCM_S24DAUD,
|
||||||
|
CODEC_ID_PCM_ZORK,
|
||||||
|
CODEC_ID_PCM_S16LE_PLANAR,
|
||||||
|
CODEC_ID_PCM_DVD,
|
||||||
|
CODEC_ID_PCM_F32BE,
|
||||||
|
CODEC_ID_PCM_F32LE,
|
||||||
|
CODEC_ID_PCM_F64BE,
|
||||||
|
CODEC_ID_PCM_F64LE,
|
||||||
|
CODEC_ID_PCM_BLURAY,
|
||||||
|
CODEC_ID_PCM_LXF,
|
||||||
|
CODEC_ID_S302M,
|
||||||
|
CODEC_ID_PCM_S8_PLANAR,
|
||||||
|
|
||||||
|
/* various ADPCM codecs */
|
||||||
|
CODEC_ID_ADPCM_IMA_QT = 0x11000,
|
||||||
|
CODEC_ID_ADPCM_IMA_WAV,
|
||||||
|
CODEC_ID_ADPCM_IMA_DK3,
|
||||||
|
CODEC_ID_ADPCM_IMA_DK4,
|
||||||
|
CODEC_ID_ADPCM_IMA_WS,
|
||||||
|
CODEC_ID_ADPCM_IMA_SMJPEG,
|
||||||
|
CODEC_ID_ADPCM_MS,
|
||||||
|
CODEC_ID_ADPCM_4XM,
|
||||||
|
CODEC_ID_ADPCM_XA,
|
||||||
|
CODEC_ID_ADPCM_ADX,
|
||||||
|
CODEC_ID_ADPCM_EA,
|
||||||
|
CODEC_ID_ADPCM_G726,
|
||||||
|
CODEC_ID_ADPCM_CT,
|
||||||
|
CODEC_ID_ADPCM_SWF,
|
||||||
|
CODEC_ID_ADPCM_YAMAHA,
|
||||||
|
CODEC_ID_ADPCM_SBPRO_4,
|
||||||
|
CODEC_ID_ADPCM_SBPRO_3,
|
||||||
|
CODEC_ID_ADPCM_SBPRO_2,
|
||||||
|
CODEC_ID_ADPCM_THP,
|
||||||
|
CODEC_ID_ADPCM_IMA_AMV,
|
||||||
|
CODEC_ID_ADPCM_EA_R1,
|
||||||
|
CODEC_ID_ADPCM_EA_R3,
|
||||||
|
CODEC_ID_ADPCM_EA_R2,
|
||||||
|
CODEC_ID_ADPCM_IMA_EA_SEAD,
|
||||||
|
CODEC_ID_ADPCM_IMA_EA_EACS,
|
||||||
|
CODEC_ID_ADPCM_EA_XAS,
|
||||||
|
CODEC_ID_ADPCM_EA_MAXIS_XA,
|
||||||
|
CODEC_ID_ADPCM_IMA_ISS,
|
||||||
|
CODEC_ID_ADPCM_G722,
|
||||||
|
CODEC_ID_ADPCM_IMA_APC,
|
||||||
|
CODEC_ID_VIMA = MKBETAG('V','I','M','A'),
|
||||||
|
|
||||||
|
/* AMR */
|
||||||
|
CODEC_ID_AMR_NB = 0x12000,
|
||||||
|
CODEC_ID_AMR_WB,
|
||||||
|
|
||||||
|
/* RealAudio codecs*/
|
||||||
|
CODEC_ID_RA_144 = 0x13000,
|
||||||
|
CODEC_ID_RA_288,
|
||||||
|
|
||||||
|
/* various DPCM codecs */
|
||||||
|
CODEC_ID_ROQ_DPCM = 0x14000,
|
||||||
|
CODEC_ID_INTERPLAY_DPCM,
|
||||||
|
CODEC_ID_XAN_DPCM,
|
||||||
|
CODEC_ID_SOL_DPCM,
|
||||||
|
|
||||||
|
/* audio codecs */
|
||||||
|
CODEC_ID_MP2 = 0x15000,
|
||||||
|
CODEC_ID_MP3, ///< preferred ID for decoding MPEG audio layer 1, 2 or 3
|
||||||
|
CODEC_ID_AAC,
|
||||||
|
CODEC_ID_AC3,
|
||||||
|
CODEC_ID_DTS,
|
||||||
|
CODEC_ID_VORBIS,
|
||||||
|
CODEC_ID_DVAUDIO,
|
||||||
|
CODEC_ID_WMAV1,
|
||||||
|
CODEC_ID_WMAV2,
|
||||||
|
CODEC_ID_MACE3,
|
||||||
|
CODEC_ID_MACE6,
|
||||||
|
CODEC_ID_VMDAUDIO,
|
||||||
|
CODEC_ID_FLAC,
|
||||||
|
CODEC_ID_MP3ADU,
|
||||||
|
CODEC_ID_MP3ON4,
|
||||||
|
CODEC_ID_SHORTEN,
|
||||||
|
CODEC_ID_ALAC,
|
||||||
|
CODEC_ID_WESTWOOD_SND1,
|
||||||
|
CODEC_ID_GSM, ///< as in Berlin toast format
|
||||||
|
CODEC_ID_QDM2,
|
||||||
|
CODEC_ID_COOK,
|
||||||
|
CODEC_ID_TRUESPEECH,
|
||||||
|
CODEC_ID_TTA,
|
||||||
|
CODEC_ID_SMACKAUDIO,
|
||||||
|
CODEC_ID_QCELP,
|
||||||
|
CODEC_ID_WAVPACK,
|
||||||
|
CODEC_ID_DSICINAUDIO,
|
||||||
|
CODEC_ID_IMC,
|
||||||
|
CODEC_ID_MUSEPACK7,
|
||||||
|
CODEC_ID_MLP,
|
||||||
|
CODEC_ID_GSM_MS, /* as found in WAV */
|
||||||
|
CODEC_ID_ATRAC3,
|
||||||
|
CODEC_ID_VOXWARE,
|
||||||
|
CODEC_ID_APE,
|
||||||
|
CODEC_ID_NELLYMOSER,
|
||||||
|
CODEC_ID_MUSEPACK8,
|
||||||
|
CODEC_ID_SPEEX,
|
||||||
|
CODEC_ID_WMAVOICE,
|
||||||
|
CODEC_ID_WMAPRO,
|
||||||
|
CODEC_ID_WMALOSSLESS,
|
||||||
|
CODEC_ID_ATRAC3P,
|
||||||
|
CODEC_ID_EAC3,
|
||||||
|
CODEC_ID_SIPR,
|
||||||
|
CODEC_ID_MP1,
|
||||||
|
CODEC_ID_TWINVQ,
|
||||||
|
CODEC_ID_TRUEHD,
|
||||||
|
CODEC_ID_MP4ALS,
|
||||||
|
CODEC_ID_ATRAC1,
|
||||||
|
CODEC_ID_BINKAUDIO_RDFT,
|
||||||
|
CODEC_ID_BINKAUDIO_DCT,
|
||||||
|
CODEC_ID_AAC_LATM,
|
||||||
|
CODEC_ID_QDMC,
|
||||||
|
CODEC_ID_CELT,
|
||||||
|
CODEC_ID_G723_1,
|
||||||
|
CODEC_ID_G729,
|
||||||
|
CODEC_ID_8SVX_EXP,
|
||||||
|
CODEC_ID_8SVX_FIB,
|
||||||
|
CODEC_ID_BMV_AUDIO,
|
||||||
|
CODEC_ID_RALF,
|
||||||
|
CODEC_ID_IAC,
|
||||||
|
CODEC_ID_ILBC,
|
||||||
|
CODEC_ID_FFWAVESYNTH = MKBETAG('F','F','W','S'),
|
||||||
|
CODEC_ID_SONIC = MKBETAG('S','O','N','C'),
|
||||||
|
CODEC_ID_SONIC_LS = MKBETAG('S','O','N','L'),
|
||||||
|
CODEC_ID_PAF_AUDIO = MKBETAG('P','A','F','A'),
|
||||||
|
CODEC_ID_OPUS = MKBETAG('O','P','U','S'),
|
||||||
|
|
||||||
|
/* subtitle codecs */
|
||||||
|
CODEC_ID_FIRST_SUBTITLE = 0x17000, ///< A dummy ID pointing at the start of subtitle codecs.
|
||||||
|
CODEC_ID_DVD_SUBTITLE = 0x17000,
|
||||||
|
CODEC_ID_DVB_SUBTITLE,
|
||||||
|
CODEC_ID_TEXT, ///< raw UTF-8 text
|
||||||
|
CODEC_ID_XSUB,
|
||||||
|
CODEC_ID_SSA,
|
||||||
|
CODEC_ID_MOV_TEXT,
|
||||||
|
CODEC_ID_HDMV_PGS_SUBTITLE,
|
||||||
|
CODEC_ID_DVB_TELETEXT,
|
||||||
|
CODEC_ID_SRT,
|
||||||
|
CODEC_ID_MICRODVD = MKBETAG('m','D','V','D'),
|
||||||
|
CODEC_ID_EIA_608 = MKBETAG('c','6','0','8'),
|
||||||
|
CODEC_ID_JACOSUB = MKBETAG('J','S','U','B'),
|
||||||
|
CODEC_ID_SAMI = MKBETAG('S','A','M','I'),
|
||||||
|
CODEC_ID_REALTEXT = MKBETAG('R','T','X','T'),
|
||||||
|
CODEC_ID_SUBVIEWER = MKBETAG('S','u','b','V'),
|
||||||
|
|
||||||
|
/* other specific kind of codecs (generally used for attachments) */
|
||||||
|
CODEC_ID_FIRST_UNKNOWN = 0x18000, ///< A dummy ID pointing at the start of various fake codecs.
|
||||||
|
CODEC_ID_TTF = 0x18000,
|
||||||
|
CODEC_ID_BINTEXT = MKBETAG('B','T','X','T'),
|
||||||
|
CODEC_ID_XBIN = MKBETAG('X','B','I','N'),
|
||||||
|
CODEC_ID_IDF = MKBETAG( 0 ,'I','D','F'),
|
||||||
|
CODEC_ID_OTF = MKBETAG( 0 ,'O','T','F'),
|
||||||
|
|
||||||
|
CODEC_ID_PROBE = 0x19000, ///< codec_id is not known (like CODEC_ID_NONE) but lavf should attempt to identify it
|
||||||
|
|
||||||
|
CODEC_ID_MPEG2TS = 0x20000, /**< _FAKE_ codec to indicate a raw MPEG-2 TS
|
||||||
|
* stream (only used by libavformat) */
|
||||||
|
CODEC_ID_MPEG4SYSTEMS = 0x20001, /**< _FAKE_ codec to indicate a MPEG-4 Systems
|
||||||
|
* stream (only used by libavformat) */
|
||||||
|
CODEC_ID_FFMETADATA = 0x21000, ///< Dummy codec for streams containing only metadata information.
|
||||||
|
|
||||||
|
#endif /* AVCODEC_OLD_CODEC_IDS_H */
|
||||||
@@ -0,0 +1,173 @@
|
|||||||
|
/*
|
||||||
|
* Video Acceleration API (shared data between FFmpeg and the video player)
|
||||||
|
* HW decode acceleration for MPEG-2, MPEG-4, H.264 and VC-1
|
||||||
|
*
|
||||||
|
* Copyright (C) 2008-2009 Splitted-Desktop Systems
|
||||||
|
*
|
||||||
|
* This file is part of FFmpeg.
|
||||||
|
*
|
||||||
|
* FFmpeg is free software; you can redistribute it and/or
|
||||||
|
* modify it under the terms of the GNU Lesser General Public
|
||||||
|
* License as published by the Free Software Foundation; either
|
||||||
|
* version 2.1 of the License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* FFmpeg is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||||
|
* Lesser General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Lesser General Public
|
||||||
|
* License along with FFmpeg; if not, write to the Free Software
|
||||||
|
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef AVCODEC_VAAPI_H
|
||||||
|
#define AVCODEC_VAAPI_H
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @file
|
||||||
|
* @ingroup lavc_codec_hwaccel_vaapi
|
||||||
|
* Public libavcodec VA API header.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @defgroup lavc_codec_hwaccel_vaapi VA API Decoding
|
||||||
|
* @ingroup lavc_codec_hwaccel
|
||||||
|
* @{
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This structure is used to share data between the FFmpeg library and
|
||||||
|
* the client video application.
|
||||||
|
* This shall be zero-allocated and available as
|
||||||
|
* AVCodecContext.hwaccel_context. All user members can be set once
|
||||||
|
* during initialization or through each AVCodecContext.get_buffer()
|
||||||
|
* function call. In any case, they must be valid prior to calling
|
||||||
|
* decoding functions.
|
||||||
|
*/
|
||||||
|
struct vaapi_context {
|
||||||
|
/**
|
||||||
|
* Window system dependent data
|
||||||
|
*
|
||||||
|
* - encoding: unused
|
||||||
|
* - decoding: Set by user
|
||||||
|
*/
|
||||||
|
void *display;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configuration ID
|
||||||
|
*
|
||||||
|
* - encoding: unused
|
||||||
|
* - decoding: Set by user
|
||||||
|
*/
|
||||||
|
uint32_t config_id;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Context ID (video decode pipeline)
|
||||||
|
*
|
||||||
|
* - encoding: unused
|
||||||
|
* - decoding: Set by user
|
||||||
|
*/
|
||||||
|
uint32_t context_id;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* VAPictureParameterBuffer ID
|
||||||
|
*
|
||||||
|
* - encoding: unused
|
||||||
|
* - decoding: Set by libavcodec
|
||||||
|
*/
|
||||||
|
uint32_t pic_param_buf_id;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* VAIQMatrixBuffer ID
|
||||||
|
*
|
||||||
|
* - encoding: unused
|
||||||
|
* - decoding: Set by libavcodec
|
||||||
|
*/
|
||||||
|
uint32_t iq_matrix_buf_id;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* VABitPlaneBuffer ID (for VC-1 decoding)
|
||||||
|
*
|
||||||
|
* - encoding: unused
|
||||||
|
* - decoding: Set by libavcodec
|
||||||
|
*/
|
||||||
|
uint32_t bitplane_buf_id;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Slice parameter/data buffer IDs
|
||||||
|
*
|
||||||
|
* - encoding: unused
|
||||||
|
* - decoding: Set by libavcodec
|
||||||
|
*/
|
||||||
|
uint32_t *slice_buf_ids;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Number of effective slice buffer IDs to send to the HW
|
||||||
|
*
|
||||||
|
* - encoding: unused
|
||||||
|
* - decoding: Set by libavcodec
|
||||||
|
*/
|
||||||
|
unsigned int n_slice_buf_ids;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Size of pre-allocated slice_buf_ids
|
||||||
|
*
|
||||||
|
* - encoding: unused
|
||||||
|
* - decoding: Set by libavcodec
|
||||||
|
*/
|
||||||
|
unsigned int slice_buf_ids_alloc;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pointer to VASliceParameterBuffers
|
||||||
|
*
|
||||||
|
* - encoding: unused
|
||||||
|
* - decoding: Set by libavcodec
|
||||||
|
*/
|
||||||
|
void *slice_params;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Size of a VASliceParameterBuffer element
|
||||||
|
*
|
||||||
|
* - encoding: unused
|
||||||
|
* - decoding: Set by libavcodec
|
||||||
|
*/
|
||||||
|
unsigned int slice_param_size;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Size of pre-allocated slice_params
|
||||||
|
*
|
||||||
|
* - encoding: unused
|
||||||
|
* - decoding: Set by libavcodec
|
||||||
|
*/
|
||||||
|
unsigned int slice_params_alloc;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Number of slices currently filled in
|
||||||
|
*
|
||||||
|
* - encoding: unused
|
||||||
|
* - decoding: Set by libavcodec
|
||||||
|
*/
|
||||||
|
unsigned int slice_count;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pointer to slice data buffer base
|
||||||
|
* - encoding: unused
|
||||||
|
* - decoding: Set by libavcodec
|
||||||
|
*/
|
||||||
|
const uint8_t *slice_data;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Current size of slice data
|
||||||
|
*
|
||||||
|
* - encoding: unused
|
||||||
|
* - decoding: Set by libavcodec
|
||||||
|
*/
|
||||||
|
uint32_t slice_data_size;
|
||||||
|
};
|
||||||
|
|
||||||
|
/* @} */
|
||||||
|
|
||||||
|
#endif /* AVCODEC_VAAPI_H */
|
||||||
@@ -0,0 +1,162 @@
|
|||||||
|
/*
|
||||||
|
* VDA HW acceleration
|
||||||
|
*
|
||||||
|
* copyright (c) 2011 Sebastien Zwickert
|
||||||
|
*
|
||||||
|
* This file is part of FFmpeg.
|
||||||
|
*
|
||||||
|
* FFmpeg is free software; you can redistribute it and/or
|
||||||
|
* modify it under the terms of the GNU Lesser General Public
|
||||||
|
* License as published by the Free Software Foundation; either
|
||||||
|
* version 2.1 of the License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* FFmpeg is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||||
|
* Lesser General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Lesser General Public
|
||||||
|
* License along with FFmpeg; if not, write to the Free Software
|
||||||
|
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef AVCODEC_VDA_H
|
||||||
|
#define AVCODEC_VDA_H
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @file
|
||||||
|
* @ingroup lavc_codec_hwaccel_vda
|
||||||
|
* Public libavcodec VDA header.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
// emmintrin.h is unable to compile with -std=c99 -Werror=missing-prototypes
|
||||||
|
// http://openradar.appspot.com/8026390
|
||||||
|
#undef __GNUC_STDC_INLINE__
|
||||||
|
|
||||||
|
#define Picture QuickdrawPicture
|
||||||
|
#include <VideoDecodeAcceleration/VDADecoder.h>
|
||||||
|
#undef Picture
|
||||||
|
|
||||||
|
#include "libavcodec/version.h"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @defgroup lavc_codec_hwaccel_vda VDA
|
||||||
|
* @ingroup lavc_codec_hwaccel
|
||||||
|
*
|
||||||
|
* @{
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This structure is used to provide the necessary configurations and data
|
||||||
|
* to the VDA FFmpeg HWAccel implementation.
|
||||||
|
*
|
||||||
|
* The application must make it available as AVCodecContext.hwaccel_context.
|
||||||
|
*/
|
||||||
|
struct vda_context {
|
||||||
|
/**
|
||||||
|
* VDA decoder object.
|
||||||
|
*
|
||||||
|
* - encoding: unused
|
||||||
|
* - decoding: Set/Unset by libavcodec.
|
||||||
|
*/
|
||||||
|
VDADecoder decoder;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The Core Video pixel buffer that contains the current image data.
|
||||||
|
*
|
||||||
|
* encoding: unused
|
||||||
|
* decoding: Set by libavcodec. Unset by user.
|
||||||
|
*/
|
||||||
|
CVPixelBufferRef cv_buffer;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Use the hardware decoder in synchronous mode.
|
||||||
|
*
|
||||||
|
* encoding: unused
|
||||||
|
* decoding: Set by user.
|
||||||
|
*/
|
||||||
|
int use_sync_decoding;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The frame width.
|
||||||
|
*
|
||||||
|
* - encoding: unused
|
||||||
|
* - decoding: Set/Unset by user.
|
||||||
|
*/
|
||||||
|
int width;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The frame height.
|
||||||
|
*
|
||||||
|
* - encoding: unused
|
||||||
|
* - decoding: Set/Unset by user.
|
||||||
|
*/
|
||||||
|
int height;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The frame format.
|
||||||
|
*
|
||||||
|
* - encoding: unused
|
||||||
|
* - decoding: Set/Unset by user.
|
||||||
|
*/
|
||||||
|
int format;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The pixel format for output image buffers.
|
||||||
|
*
|
||||||
|
* - encoding: unused
|
||||||
|
* - decoding: Set/Unset by user.
|
||||||
|
*/
|
||||||
|
OSType cv_pix_fmt_type;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The current bitstream buffer.
|
||||||
|
*
|
||||||
|
* - encoding: unused
|
||||||
|
* - decoding: Set/Unset by libavcodec.
|
||||||
|
*/
|
||||||
|
uint8_t *priv_bitstream;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The current size of the bitstream.
|
||||||
|
*
|
||||||
|
* - encoding: unused
|
||||||
|
* - decoding: Set/Unset by libavcodec.
|
||||||
|
*/
|
||||||
|
int priv_bitstream_size;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The reference size used for fast reallocation.
|
||||||
|
*
|
||||||
|
* - encoding: unused
|
||||||
|
* - decoding: Set/Unset by libavcodec.
|
||||||
|
*/
|
||||||
|
int priv_allocated_size;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Use av_buffer to manage buffer.
|
||||||
|
* When the flag is set, the CVPixelBuffers returned by the decoder will
|
||||||
|
* be released automatically, so you have to retain them if necessary.
|
||||||
|
* Not setting this flag may cause memory leak.
|
||||||
|
*
|
||||||
|
* encoding: unused
|
||||||
|
* decoding: Set by user.
|
||||||
|
*/
|
||||||
|
int use_ref_buffer;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Create the video decoder. */
|
||||||
|
int ff_vda_create_decoder(struct vda_context *vda_ctx,
|
||||||
|
uint8_t *extradata,
|
||||||
|
int extradata_size);
|
||||||
|
|
||||||
|
/** Destroy the video decoder. */
|
||||||
|
int ff_vda_destroy_decoder(struct vda_context *vda_ctx);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @}
|
||||||
|
*/
|
||||||
|
|
||||||
|
#endif /* AVCODEC_VDA_H */
|
||||||
Loaded 100 of 570 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user