mirror of
https://github.com/moonlight-stream/moonlight-android.git
synced 2026-10-20 23:36:53 +00:00
Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fdbecfa6dd | ||
|
|
5e45aeb70b |
No files matched your search
@@ -1,8 +0,0 @@
|
|||||||
# ProBot No Response (https://probot.github.io/apps/no-response/)
|
|
||||||
|
|
||||||
daysUntilClose: 7
|
|
||||||
responseRequiredLabel: 'need more info'
|
|
||||||
closeComment: >
|
|
||||||
This issue has been automatically closed because there was no response to a
|
|
||||||
request for more information from the issue opener. Please leave a comment or
|
|
||||||
open a new issue if you have additional information related to this issue.
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
# ProBot Stale (https://probot.github.io/apps/stale/)
|
|
||||||
|
|
||||||
daysUntilStale: 90
|
|
||||||
daysUntilClose: 7
|
|
||||||
exemptLabels:
|
|
||||||
- accepted
|
|
||||||
- bug
|
|
||||||
- enhancement
|
|
||||||
- meta
|
|
||||||
staleLabel: stale
|
|
||||||
markComment: >
|
|
||||||
This issue has been automatically marked as stale because it has not had
|
|
||||||
recent activity. It will be closed if no further activity occurs.
|
|
||||||
closeComment: false
|
|
||||||
+1
-6
@@ -1,9 +1,6 @@
|
|||||||
# built application files
|
#built application files
|
||||||
*.apk
|
*.apk
|
||||||
*.ap_
|
*.ap_
|
||||||
*.aab
|
|
||||||
output.json
|
|
||||||
out/
|
|
||||||
|
|
||||||
# files for the dex VM
|
# files for the dex VM
|
||||||
*.dex
|
*.dex
|
||||||
@@ -33,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/
|
||||||
*.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,42 +1,44 @@
|
|||||||
# Moonlight Android
|
#Limelight
|
||||||
|
|
||||||
[Moonlight](https://moonlight-stream.org) 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.
|
||||||
|
|
||||||
Check our [wiki](https://github.com/moonlight-stream/moonlight-docs/wiki) for more detailed information or a troubleshooting guide.
|
[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.
|
||||||
|
|
||||||
## Features
|
Check our [wiki](https://github.com/limelight-stream/limelight-android/wiki) for more detailed information or a troubleshooting guide.
|
||||||
|
|
||||||
|
##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)
|
||||||
|
|
||||||
@@ -44,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>
|
||||||
|
|
||||||
+49
-91
@@ -1,122 +1,80 @@
|
|||||||
|
import com.android.builder.model.ProductFlavor
|
||||||
|
import org.apache.tools.ant.taskdefs.condition.Os
|
||||||
|
|
||||||
apply plugin: 'com.android.application'
|
apply plugin: 'com.android.application'
|
||||||
|
|
||||||
android {
|
android {
|
||||||
buildToolsVersion '28.0.3'
|
compileSdkVersion 21
|
||||||
compileSdkVersion 28
|
buildToolsVersion "21.1.2"
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
minSdkVersion 16
|
minSdkVersion 16
|
||||||
targetSdkVersion 28
|
targetSdkVersion 21
|
||||||
|
|
||||||
versionName "7.2"
|
versionName "3.1-beta1"
|
||||||
versionCode = 191
|
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
|
|
||||||
|
|
||||||
externalNativeBuild {
|
|
||||||
ndkBuild {
|
|
||||||
arguments "PRODUCT_FLAVOR=root"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
applicationId "com.limelight.root"
|
applicationId "com.limelight.root"
|
||||||
dimension "root"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
nonRoot {
|
nonRoot {
|
||||||
externalNativeBuild {
|
|
||||||
ndkBuild {
|
|
||||||
arguments "PRODUCT_FLAVOR=nonRoot"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
applicationId "com.limelight"
|
applicationId "com.limelight"
|
||||||
dimension "root"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
lintOptions {
|
|
||||||
disable 'MissingTranslation'
|
|
||||||
}
|
|
||||||
|
|
||||||
bundle {
|
|
||||||
language {
|
|
||||||
// Avoid splitting by language, since we allow users
|
|
||||||
// to manually switch language in settings.
|
|
||||||
enableSplit = false
|
|
||||||
}
|
|
||||||
density {
|
|
||||||
// FIXME: This should not be neccessary but we get
|
|
||||||
// weird crashes due to missing drawable resources
|
|
||||||
// when this split is enabled.
|
|
||||||
enableSplit = false
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
buildTypes {
|
buildTypes {
|
||||||
debug {
|
|
||||||
applicationIdSuffix ".debug"
|
|
||||||
|
|
||||||
minifyEnabled true
|
|
||||||
useProguard false
|
|
||||||
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
|
||||||
}
|
|
||||||
release {
|
release {
|
||||||
// To whomever is releasing/using an APK in release mode with
|
minifyEnabled false
|
||||||
// Moonlight's official application ID, please stop. I see every
|
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt'
|
||||||
// single one of your crashes in my Play Console and it makes
|
|
||||||
// Moonlight's reliability look worse and makes it more difficult
|
|
||||||
// to distinguish real crashes from your crashy VR app. Seriously,
|
|
||||||
// 44 of the *same* native crash in 72 hours and a few each of
|
|
||||||
// several other crashes.
|
|
||||||
//
|
|
||||||
// This is technically not your fault. I would have hoped Google
|
|
||||||
// would validate the signature of the APK before attributing
|
|
||||||
// the crash to it. I asked their Play Store support about this
|
|
||||||
// and they said they don't and don't have plans to, so that sucks.
|
|
||||||
//
|
|
||||||
// In any case, it's bad form to release an APK using someone
|
|
||||||
// else's application ID. There is no legitimate reason, that
|
|
||||||
// anyone would need to comment out the following line, except me
|
|
||||||
// when I release an official signed Moonlight build. If you feel
|
|
||||||
// like doing so would solve something, I can tell you it will not.
|
|
||||||
// You can't upgrade an app while retaining data without having the
|
|
||||||
// same signature as the official version. Nor can you post it on
|
|
||||||
// the Play Store, since that application ID is already taken.
|
|
||||||
// Reputable APK hosting websites similarly validate the signature
|
|
||||||
// is consistent with the Play Store and won't allow an APK that
|
|
||||||
// isn't signed the same as the original.
|
|
||||||
//
|
|
||||||
// I wish any and all people using Moonlight as the basis of other
|
|
||||||
// cool projects the best of luck with their efforts. All I ask
|
|
||||||
// is to please change the applicationId before you publish.
|
|
||||||
//
|
|
||||||
// TL;DR: Leave the following line alone!
|
|
||||||
applicationIdSuffix ".unofficial"
|
|
||||||
|
|
||||||
minifyEnabled true
|
|
||||||
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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.59'
|
compile group: 'org.jcodec', name: 'jcodec', version: '+'
|
||||||
implementation 'org.bouncycastle:bcpkix-jdk15on:1.59'
|
|
||||||
implementation 'org.jcodec:jcodec:0.2.3'
|
|
||||||
|
|
||||||
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.
Vendored
-28
@@ -1,28 +0,0 @@
|
|||||||
# Don't obfuscate code
|
|
||||||
-dontobfuscate
|
|
||||||
|
|
||||||
# Our code
|
|
||||||
-keep class com.limelight.binding.input.evdev.* {*;}
|
|
||||||
|
|
||||||
# Moonlight common
|
|
||||||
-keep class com.limelight.nvstream.jni.* {*;}
|
|
||||||
|
|
||||||
# Okio
|
|
||||||
-keep class sun.misc.Unsafe {*;}
|
|
||||||
-dontwarn java.nio.file.*
|
|
||||||
-dontwarn org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement
|
|
||||||
-dontwarn okio.**
|
|
||||||
|
|
||||||
# BouncyCastle
|
|
||||||
-keep class org.bouncycastle.jcajce.provider.asymmetric.* {*;}
|
|
||||||
-keep class org.bouncycastle.jcajce.provider.asymmetric.util.* {*;}
|
|
||||||
-keep class org.bouncycastle.jcajce.provider.asymmetric.rsa.* {*;}
|
|
||||||
-keep class org.bouncycastle.jcajce.provider.digest.** {*;}
|
|
||||||
-keep class org.bouncycastle.jcajce.provider.symmetric.** {*;}
|
|
||||||
-keep class org.bouncycastle.jcajce.spec.* {*;}
|
|
||||||
-keep class org.bouncycastle.jce.** {*;}
|
|
||||||
-dontwarn javax.naming.**
|
|
||||||
|
|
||||||
# jMDNS
|
|
||||||
-dontwarn javax.jmdns.impl.DNSCache
|
|
||||||
-dontwarn org.slf4j.**
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<resources>
|
|
||||||
|
|
||||||
<string name="app_label" translatable="false">Moonlight (Debug)</string>
|
|
||||||
<string name="app_label_root" translatable="false">Moonlight (Root Debug)</string>
|
|
||||||
|
|
||||||
</resources>
|
|
||||||
@@ -4,137 +4,78 @@
|
|||||||
|
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||||
<uses-permission android:name="android.permission.VIBRATE" />
|
|
||||||
<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:networkSecurityConfig="@xml/network_security_config"
|
android:theme="@style/AppTheme" >
|
||||||
android:isGame="true"
|
|
||||||
android:banner="@drawable/atv_banner"
|
|
||||||
android:appCategory="game"
|
|
||||||
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+ -->
|
|
||||||
|
|
||||||
|
<!-- Launcher for traditional devices -->
|
||||||
<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=".ShortcutTrampoline"
|
android:name=".PcViewTv"
|
||||||
android:noHistory="true"
|
android:logo="@drawable/atv_banner"
|
||||||
android:exported="true"
|
android:icon="@drawable/atv_banner"
|
||||||
android:resizeableActivity="true"
|
|
||||||
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>
|
||||||
<!-- This will fall back to sensorLandscape at runtime on Android 4.2 and below -->
|
|
||||||
<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="userLandscape"
|
android:theme="@style/StreamTheme"
|
||||||
android:noHistory="true"
|
android:configChanges="mcc|mnc|locale|touchscreen|keyboard|keyboardHidden|navigation|screenLayout|fontScale|uiMode|orientation|screenSize|smallestScreenSize|layoutDirection" >
|
||||||
android:supportsPictureInPicture="true"
|
|
||||||
android:resizeableActivity="true"
|
|
||||||
android:launchMode="singleTask"
|
|
||||||
android:excludeFromRecents="true"
|
|
||||||
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,34 +1,43 @@
|
|||||||
package com.limelight;
|
package com.limelight;
|
||||||
|
|
||||||
|
import java.io.FileNotFoundException;
|
||||||
|
import java.io.FileOutputStream;
|
||||||
import java.io.IOException;
|
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 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.graphics.Bitmap;
|
import android.content.res.Configuration;
|
||||||
import android.graphics.drawable.BitmapDrawable;
|
|
||||||
import android.os.Build;
|
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.os.IBinder;
|
import android.os.IBinder;
|
||||||
import android.view.ContextMenu;
|
import android.view.ContextMenu;
|
||||||
@@ -39,38 +48,33 @@ 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.ImageView;
|
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;
|
||||||
|
|
||||||
import org.xmlpull.v1.XmlPullParserException;
|
|
||||||
|
|
||||||
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_WITH_QUIT = 4;
|
private final static int START_OR_RESUME_ID = 1;
|
||||||
private final static int VIEW_DETAILS_ID = 5;
|
private final static int QUIT_ID = 2;
|
||||||
private final static int CREATE_SHORTCUT_ID = 6;
|
private final static int CANCEL_ID = 3;
|
||||||
|
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);
|
||||||
@@ -82,57 +86,32 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
// Wait for the binder to be ready
|
// Wait for the binder to be ready
|
||||||
localBinder.waitForReady();
|
localBinder.waitForReady();
|
||||||
|
|
||||||
|
// Now make the binder visible
|
||||||
|
managerBinder = localBinder;
|
||||||
|
|
||||||
// Get the computer object
|
// Get the computer object
|
||||||
computer = localBinder.getComputer(uuidString);
|
computer = managerBinder.getComputer(UUID.fromString(uuidString));
|
||||||
if (computer == null) {
|
|
||||||
finish();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
appGridAdapter = new AppGridAdapter(AppView.this,
|
appGridAdapter = new AppGridAdapter(AppView.this,
|
||||||
PreferenceConfiguration.readPreferences(AppView.this).listMode,
|
PreferenceConfiguration.readPreferences(AppView.this).listMode,
|
||||||
PreferenceConfiguration.readPreferences(AppView.this).smallIconMode,
|
PreferenceConfiguration.readPreferences(AppView.this).smallIconMode,
|
||||||
computer, localBinder.getUniqueId());
|
computer, managerBinder.getUniqueId());
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
e.printStackTrace();
|
e.printStackTrace();
|
||||||
finish();
|
finish();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Now make the binder visible. We must do this after appGridAdapter
|
|
||||||
// is set to prevent us from reaching updateUiWithServerinfo() and
|
|
||||||
// touching the appGridAdapter prior to initialization.
|
|
||||||
managerBinder = localBinder;
|
|
||||||
|
|
||||||
// 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();
|
||||||
}
|
}
|
||||||
@@ -142,84 +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.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,
|
});
|
||||||
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 (XmlPullParserException | IOException e) {
|
} catch (Exception e) {}
|
||||||
e.printStackTrace();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -237,44 +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
|
@Override
|
||||||
protected void onCreate(Bundle savedInstanceState) {
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
|
|
||||||
// Assume we're in the foreground when created to avoid a race
|
String locale = PreferenceConfiguration.readPreferences(this).language;
|
||||||
// between binding to CMS and onResume()
|
if (!locale.equals(PreferenceConfiguration.DEFAULT_LANGUAGE)) {
|
||||||
inForeground = true;
|
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);
|
||||||
TextView label = findViewById(R.id.appListText);
|
setTitle(labelText);
|
||||||
setTitle(computerName);
|
label.setText(labelText);
|
||||||
label.setText(computerName);
|
|
||||||
|
|
||||||
// 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,7 +226,7 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
List<NvApp> applist = NvHTTP.getAppListByReader(new StringReader(lastRawApplist));
|
List<NvApp> applist = NvHTTP.getAppListByReader(new StringReader(lastRawApplist));
|
||||||
updateUiWithAppList(applist);
|
updateUiWithAppList(applist);
|
||||||
LimeLog.info("Loaded applist from cache");
|
LimeLog.info("Loaded applist from cache");
|
||||||
} catch (IOException | XmlPullParserException e) {
|
} catch (Exception e) {
|
||||||
if (lastRawApplist != null) {
|
if (lastRawApplist != null) {
|
||||||
LimeLog.warning("Saved applist corrupted: "+lastRawApplist);
|
LimeLog.warning("Saved applist corrupted: "+lastRawApplist);
|
||||||
e.printStackTrace();
|
e.printStackTrace();
|
||||||
@@ -299,72 +242,100 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
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
|
@Override
|
||||||
protected void onResume() {
|
protected void onResume() {
|
||||||
super.onResume();
|
super.onResume();
|
||||||
|
|
||||||
// Display a decoder crash notification if we've returned after a crash
|
|
||||||
UiHelper.showDecoderCrashDialog(this);
|
|
||||||
|
|
||||||
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_WITH_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));
|
||||||
}
|
}
|
||||||
}
|
|
||||||
menu.add(Menu.NONE, VIEW_DETAILS_ID, 3, getResources().getString(R.string.applist_menu_details));
|
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
||||||
// Only add an option to create shortcut if box art is loaded
|
|
||||||
// and when we're in grid-mode (not list-mode).
|
|
||||||
ImageView appImageView = info.targetView.findViewById(R.id.grid_image);
|
|
||||||
if (appImageView != null) {
|
|
||||||
// We have a grid ImageView, so we must be in grid-mode
|
|
||||||
BitmapDrawable drawable = (BitmapDrawable)appImageView.getDrawable();
|
|
||||||
if (drawable != null && drawable.getBitmap() != null) {
|
|
||||||
// We have a bitmap loaded too
|
|
||||||
menu.add(Menu.NONE, CREATE_SHORTCUT_ID, 4, getResources().getString(R.string.applist_menu_scut));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onContextMenuClosed(Menu menu) {
|
public void onContextMenuClosed(Menu menu) {
|
||||||
|
}
|
||||||
|
|
||||||
|
private void displayQuitConfirmationDialog(final Runnable onYes, final Runnable onNo) {
|
||||||
|
DialogInterface.OnClickListener dialogClickListener = new DialogInterface.OnClickListener() {
|
||||||
|
@Override
|
||||||
|
public void onClick(DialogInterface dialog, int which) {
|
||||||
|
switch (which){
|
||||||
|
case DialogInterface.BUTTON_POSITIVE:
|
||||||
|
if (onYes != null) {
|
||||||
|
onYes.run();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case DialogInterface.BUTTON_NEGATIVE:
|
||||||
|
if (onNo != null) {
|
||||||
|
onNo.run();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
AlertDialog.Builder builder = new AlertDialog.Builder(this);
|
||||||
|
builder.setMessage(getResources().getString(R.string.applist_quit_confirmation))
|
||||||
|
.setPositiveButton(getResources().getString(R.string.yes), dialogClickListener)
|
||||||
|
.setNegativeButton(getResources().getString(R.string.no), dialogClickListener)
|
||||||
|
.show();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -372,38 +343,27 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
AdapterContextMenuInfo info = (AdapterContextMenuInfo) item.getMenuInfo();
|
AdapterContextMenuInfo info = (AdapterContextMenuInfo) item.getMenuInfo();
|
||||||
final AppObject app = (AppObject) appGridAdapter.getItem(info.position);
|
final AppObject app = (AppObject) appGridAdapter.getItem(info.position);
|
||||||
switch (item.getItemId()) {
|
switch (item.getItemId()) {
|
||||||
case START_WITH_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, 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;
|
||||||
@@ -411,77 +371,33 @@ public class AppView extends Activity implements AdapterFragmentCallbacks {
|
|||||||
case CANCEL_ID:
|
case CANCEL_ID:
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
case VIEW_DETAILS_ID:
|
|
||||||
Dialog.displayDialog(AppView.this, getResources().getString(R.string.title_details),
|
|
||||||
getResources().getString(R.string.applist_details_id) + " " + app.app.getAppId(), false);
|
|
||||||
return true;
|
|
||||||
|
|
||||||
case CREATE_SHORTCUT_ID:
|
|
||||||
ImageView appImageView = info.targetView.findViewById(R.id.grid_image);
|
|
||||||
Bitmap appBits = ((BitmapDrawable)appImageView.getDrawable()).getBitmap();
|
|
||||||
if (!shortcutHelper.createPinnedGameShortcut(uuidString + Integer.valueOf(app.app.getAppId()).toString(), appBits, computer, app.app)) {
|
|
||||||
Toast.makeText(AppView.this, getResources().getString(R.string.unable_to_pin_shortcut), Toast.LENGTH_LONG).show();
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return super.onContextItemSelected(item);
|
return super.onContextItemSelected(item);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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;
|
||||||
@@ -499,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();
|
||||||
}
|
}
|
||||||
@@ -534,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 ?
|
||||||
@@ -549,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) {
|
||||||
if (app == null) {
|
this.app = app;
|
||||||
throw new IllegalArgumentException("app must not be null");
|
}
|
||||||
}
|
|
||||||
this.app = app;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String toString() {
|
public String toString() {
|
||||||
return app.getAppName();
|
return app.getAppName();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
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 {}
|
||||||
@@ -1,275 +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.nvstream.http.PairingManager;
|
|
||||||
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 ShortcutTrampoline extends Activity {
|
|
||||||
private String uuidString;
|
|
||||||
private String appIdString;
|
|
||||||
private ArrayList<Intent> intentStack = new ArrayList<>();
|
|
||||||
|
|
||||||
private ComputerDetails computer;
|
|
||||||
private SpinnerDialog blockingLoadSpinner;
|
|
||||||
|
|
||||||
public final static String APP_ID_EXTRA = "AppId";
|
|
||||||
|
|
||||||
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(uuidString);
|
|
||||||
|
|
||||||
if (computer == null) {
|
|
||||||
Dialog.displayDialog(ShortcutTrampoline.this,
|
|
||||||
getResources().getString(R.string.conn_error_title),
|
|
||||||
getResources().getString(R.string.scut_pc_not_found),
|
|
||||||
true);
|
|
||||||
|
|
||||||
if (blockingLoadSpinner != null) {
|
|
||||||
blockingLoadSpinner.dismiss();
|
|
||||||
blockingLoadSpinner = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (managerBinder != null) {
|
|
||||||
unbindService(serviceConnection);
|
|
||||||
managerBinder = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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.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 the managerBinder was destroyed before this callback,
|
|
||||||
// just finish the activity.
|
|
||||||
if (managerBinder == null) {
|
|
||||||
finish();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (details.state == ComputerDetails.State.ONLINE && details.pairState == PairingManager.PairState.PAIRED) {
|
|
||||||
|
|
||||||
// Launch game if provided app ID, otherwise launch app view
|
|
||||||
if (appIdString != null && appIdString.length() > 0) {
|
|
||||||
if (details.runningGameId == 0 || details.runningGameId == Integer.parseInt(appIdString)) {
|
|
||||||
intentStack.add(ServerHelper.createStartIntent(ShortcutTrampoline.this,
|
|
||||||
new NvApp("app", Integer.parseInt(appIdString), false), details, managerBinder));
|
|
||||||
|
|
||||||
// Close this activity
|
|
||||||
finish();
|
|
||||||
|
|
||||||
// Now start the activities
|
|
||||||
startActivities(intentStack.toArray(new Intent[]{}));
|
|
||||||
} else {
|
|
||||||
// Create the start intent immediately, so we can safely unbind the managerBinder
|
|
||||||
// below before we return.
|
|
||||||
final Intent startIntent = ServerHelper.createStartIntent(ShortcutTrampoline.this,
|
|
||||||
new NvApp("app", Integer.parseInt(appIdString), false), details, managerBinder);
|
|
||||||
|
|
||||||
UiHelper.displayQuitConfirmationDialog(ShortcutTrampoline.this, new Runnable() {
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
intentStack.add(startIntent);
|
|
||||||
|
|
||||||
// Close this activity
|
|
||||||
finish();
|
|
||||||
|
|
||||||
// Now start the activities
|
|
||||||
startActivities(intentStack.toArray(new Intent[]{}));
|
|
||||||
}
|
|
||||||
}, new Runnable() {
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
// Close this activity
|
|
||||||
finish();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Close this activity
|
|
||||||
finish();
|
|
||||||
|
|
||||||
// Add the PC view at the back (and clear the task)
|
|
||||||
Intent i;
|
|
||||||
i = new Intent(ShortcutTrampoline.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(ShortcutTrampoline.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(ShortcutTrampoline.this,
|
|
||||||
new NvApp("app", details.runningGameId, false), 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(ShortcutTrampoline.this,
|
|
||||||
getResources().getString(R.string.conn_error_title),
|
|
||||||
getResources().getString(R.string.error_pc_offline),
|
|
||||||
true);
|
|
||||||
} else if (details.pairState != PairingManager.PairState.PAIRED) {
|
|
||||||
// Computer not paired - display an error dialog
|
|
||||||
Dialog.displayDialog(ShortcutTrampoline.this,
|
|
||||||
getResources().getString(R.string.conn_error_title),
|
|
||||||
getResources().getString(R.string.scut_not_paired),
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
protected boolean validateInput() {
|
|
||||||
// Validate UUID
|
|
||||||
if (uuidString == null) {
|
|
||||||
Dialog.displayDialog(ShortcutTrampoline.this,
|
|
||||||
getResources().getString(R.string.conn_error_title),
|
|
||||||
getResources().getString(R.string.scut_invalid_uuid),
|
|
||||||
true);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
UUID.fromString(uuidString);
|
|
||||||
} catch (IllegalArgumentException ex) {
|
|
||||||
Dialog.displayDialog(ShortcutTrampoline.this,
|
|
||||||
getResources().getString(R.string.conn_error_title),
|
|
||||||
getResources().getString(R.string.scut_invalid_uuid),
|
|
||||||
true);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate App ID (if provided)
|
|
||||||
if (appIdString != null && !appIdString.isEmpty()) {
|
|
||||||
try {
|
|
||||||
Integer.parseInt(appIdString);
|
|
||||||
} catch (NumberFormatException ex) {
|
|
||||||
Dialog.displayDialog(ShortcutTrampoline.this,
|
|
||||||
getResources().getString(R.string.conn_error_title),
|
|
||||||
getResources().getString(R.string.scut_invalid_app_id),
|
|
||||||
true);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onCreate(Bundle savedInstanceState) {
|
|
||||||
super.onCreate(savedInstanceState);
|
|
||||||
|
|
||||||
UiHelper.notifyNewRootView(this);
|
|
||||||
|
|
||||||
uuidString = getIntent().getStringExtra(AppView.UUID_EXTRA);
|
|
||||||
appIdString = getIntent().getStringExtra(APP_ID_EXTRA);
|
|
||||||
|
|
||||||
if (validateInput()) {
|
|
||||||
// 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 onStop() {
|
|
||||||
super.onStop();
|
|
||||||
|
|
||||||
if (blockingLoadSpinner != null) {
|
|
||||||
blockingLoadSpinner.dismiss();
|
|
||||||
blockingLoadSpinner = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
Dialog.closeDialogs();
|
|
||||||
|
|
||||||
if (managerBinder != null) {
|
|
||||||
managerBinder.stopPolling();
|
|
||||||
unbindService(serviceConnection);
|
|
||||||
managerBinder = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
finish();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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 createAudioTrack(int channelConfig, int bufferSize, boolean lowLatency) {
|
private AudioTrack track;
|
||||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
|
|
||||||
return new AudioTrack(AudioManager.STREAM_MUSIC,
|
|
||||||
48000,
|
|
||||||
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) {
|
@Override
|
||||||
// Use FLAG_LOW_LATENCY on L through N
|
public boolean streamInitialized(int channelCount, int sampleRate) {
|
||||||
if (lowLatency) {
|
int channelConfig;
|
||||||
attributesBuilder.setFlags(AudioAttributes.FLAG_LOW_LATENCY);
|
int bufferSize;
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
switch (channelCount)
|
||||||
AudioTrack.Builder trackBuilder = new AudioTrack.Builder()
|
{
|
||||||
.setAudioFormat(format)
|
case 1:
|
||||||
.setAudioAttributes(attributesBuilder.build())
|
channelConfig = AudioFormat.CHANNEL_OUT_MONO;
|
||||||
.setTransferMode(AudioTrack.MODE_STREAM)
|
break;
|
||||||
.setBufferSizeInBytes(bufferSize);
|
case 2:
|
||||||
|
channelConfig = AudioFormat.CHANNEL_OUT_STEREO;
|
||||||
// Use PERFORMANCE_MODE_LOW_LATENCY on O and later
|
break;
|
||||||
if (lowLatency) {
|
default:
|
||||||
trackBuilder.setPerformanceMode(AudioTrack.PERFORMANCE_MODE_LOW_LATENCY);
|
LimeLog.severe("Decoder returned unhandled channel count");
|
||||||
}
|
return false;
|
||||||
|
}
|
||||||
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) {}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (track == null) {
|
LimeLog.info("Audio track buffer size: "+bufferSize);
|
||||||
// Couldn't create any audio track for playback
|
|
||||||
return -2;
|
|
||||||
}
|
|
||||||
|
|
||||||
return 0;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void playDecodedAudio(byte[] audioData) {
|
public void playDecodedAudio(byte[] audioData, int offset, int length) {
|
||||||
track.write(audioData, 0, audioData.length);
|
track.write(audioData, offset, length);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void start() {}
|
public void streamClosing() {
|
||||||
|
if (track != null) {
|
||||||
|
track.release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void stop() {}
|
public int getCapabilities() {
|
||||||
|
return 0;
|
||||||
@Override
|
}
|
||||||
public void cleanup() {
|
|
||||||
// Immediately drop all pending data
|
|
||||||
track.pause();
|
|
||||||
track.flush();
|
|
||||||
|
|
||||||
track.release();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -12,8 +12,9 @@ import java.security.KeyFactory;
|
|||||||
import java.security.KeyPair;
|
import java.security.KeyPair;
|
||||||
import java.security.KeyPairGenerator;
|
import java.security.KeyPairGenerator;
|
||||||
import java.security.NoSuchAlgorithmException;
|
import java.security.NoSuchAlgorithmException;
|
||||||
import java.security.Provider;
|
import java.security.NoSuchProviderException;
|
||||||
import java.security.SecureRandom;
|
import java.security.SecureRandom;
|
||||||
|
import java.security.Security;
|
||||||
import java.security.cert.CertificateException;
|
import java.security.cert.CertificateException;
|
||||||
import java.security.cert.CertificateFactory;
|
import java.security.cert.CertificateFactory;
|
||||||
import java.security.cert.X509Certificate;
|
import java.security.cert.X509Certificate;
|
||||||
@@ -44,228 +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();
|
||||||
|
|
||||||
private static final Provider bcProvider = new BouncyCastleProvider();
|
static {
|
||||||
|
// Install the Bouncy Castle provider
|
||||||
|
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() {
|
private boolean loadCertKeyPair() {
|
||||||
byte[] certBytes = loadFileToBytes(certFile);
|
byte[] certBytes = loadFileToBytes(certFile);
|
||||||
byte[] keyBytes = loadFileToBytes(keyFile);
|
byte[] keyBytes = loadFileToBytes(keyFile);
|
||||||
|
|
||||||
// If either file was missing, we definitely can't succeed
|
// If either file was missing, we definitely can't succeed
|
||||||
if (certBytes == null || keyBytes == null) {
|
if (certBytes == null || keyBytes == null) {
|
||||||
LimeLog.info("Missing cert or key; need to generate a new one");
|
LimeLog.info("Missing cert or key; need to generate a new one");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
CertificateFactory certFactory = CertificateFactory.getInstance("X.509", bcProvider);
|
CertificateFactory certFactory = CertificateFactory.getInstance("X.509", "BC");
|
||||||
cert = (X509Certificate) certFactory.generateCertificate(new ByteArrayInputStream(certBytes));
|
cert = (X509Certificate) certFactory.generateCertificate(new ByteArrayInputStream(certBytes));
|
||||||
pemCertBytes = certBytes;
|
pemCertBytes = certBytes;
|
||||||
KeyFactory keyFactory = KeyFactory.getInstance("RSA", bcProvider);
|
KeyFactory keyFactory = KeyFactory.getInstance("RSA", "BC");
|
||||||
key = (RSAPrivateKey) keyFactory.generatePrivate(new PKCS8EncodedKeySpec(keyBytes));
|
key = (RSAPrivateKey) keyFactory.generatePrivate(new PKCS8EncodedKeySpec(keyBytes));
|
||||||
} catch (CertificateException e) {
|
} catch (CertificateException e) {
|
||||||
// May happen if the cert is corrupt
|
// May happen if the cert is corrupt
|
||||||
LimeLog.warning("Corrupted certificate");
|
LimeLog.warning("Corrupted certificate");
|
||||||
return false;
|
return false;
|
||||||
} catch (NoSuchAlgorithmException e) {
|
} catch (NoSuchAlgorithmException e) {
|
||||||
// Should never happen
|
// Should never happen
|
||||||
e.printStackTrace();
|
e.printStackTrace();
|
||||||
return false;
|
return false;
|
||||||
} catch (InvalidKeySpecException e) {
|
} catch (InvalidKeySpecException e) {
|
||||||
// May happen if the key is corrupt
|
// May happen if the key is corrupt
|
||||||
LimeLog.warning("Corrupted key");
|
LimeLog.warning("Corrupted key");
|
||||||
return false;
|
return false;
|
||||||
}
|
} catch (NoSuchProviderException e) {
|
||||||
|
// Should never happen
|
||||||
|
e.printStackTrace();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("TrulyRandom")
|
@SuppressLint("TrulyRandom")
|
||||||
private boolean generateCertKeyPair() {
|
private boolean generateCertKeyPair() {
|
||||||
byte[] snBytes = new byte[8];
|
byte[] snBytes = new byte[8];
|
||||||
new SecureRandom().nextBytes(snBytes);
|
new SecureRandom().nextBytes(snBytes);
|
||||||
|
|
||||||
KeyPair keyPair;
|
KeyPair keyPair;
|
||||||
try {
|
try {
|
||||||
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA", bcProvider);
|
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA", "BC");
|
||||||
keyPairGenerator.initialize(2048);
|
keyPairGenerator.initialize(2048);
|
||||||
keyPair = keyPairGenerator.generateKeyPair();
|
keyPair = keyPairGenerator.generateKeyPair();
|
||||||
} catch (NoSuchAlgorithmException e1) {
|
} catch (NoSuchAlgorithmException e1) {
|
||||||
// Should never happen
|
// Should never happen
|
||||||
e1.printStackTrace();
|
e1.printStackTrace();
|
||||||
return false;
|
return false;
|
||||||
}
|
} catch (NoSuchProviderException e) {
|
||||||
|
// Should never happen
|
||||||
|
e.printStackTrace();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
Date now = new Date();
|
Date now = new Date();
|
||||||
|
|
||||||
// Expires in 20 years
|
// Expires in 20 years
|
||||||
Calendar calendar = Calendar.getInstance();
|
Calendar calendar = Calendar.getInstance();
|
||||||
calendar.setTime(now);
|
calendar.setTime(now);
|
||||||
calendar.add(Calendar.YEAR, 20);
|
calendar.add(Calendar.YEAR, 20);
|
||||||
Date expirationDate = calendar.getTime();
|
Date expirationDate = calendar.getTime();
|
||||||
|
|
||||||
BigInteger serial = new BigInteger(snBytes).abs();
|
BigInteger serial = new BigInteger(snBytes).abs();
|
||||||
|
|
||||||
X500NameBuilder nameBuilder = new X500NameBuilder(BCStyle.INSTANCE);
|
X500NameBuilder nameBuilder = new X500NameBuilder(BCStyle.INSTANCE);
|
||||||
nameBuilder.addRDN(BCStyle.CN, "NVIDIA GameStream Client");
|
nameBuilder.addRDN(BCStyle.CN, "NVIDIA GameStream Client");
|
||||||
X500Name name = nameBuilder.build();
|
X500Name name = nameBuilder.build();
|
||||||
|
|
||||||
X509v3CertificateBuilder certBuilder = new X509v3CertificateBuilder(name, serial, now, expirationDate, Locale.ENGLISH, name,
|
X509v3CertificateBuilder certBuilder = new X509v3CertificateBuilder(name, serial, now, expirationDate, Locale.ENGLISH, name,
|
||||||
SubjectPublicKeyInfo.getInstance(keyPair.getPublic().getEncoded()));
|
SubjectPublicKeyInfo.getInstance(keyPair.getPublic().getEncoded()));
|
||||||
|
|
||||||
try {
|
try {
|
||||||
ContentSigner sigGen = new JcaContentSignerBuilder("SHA1withRSA").setProvider(bcProvider).build(keyPair.getPrivate());
|
ContentSigner sigGen = new JcaContentSignerBuilder("SHA1withRSA").setProvider(BouncyCastleProvider.PROVIDER_NAME).build(keyPair.getPrivate());
|
||||||
cert = new JcaX509CertificateConverter().setProvider(bcProvider).getCertificate(certBuilder.build(sigGen));
|
cert = new JcaX509CertificateConverter().setProvider(BouncyCastleProvider.PROVIDER_NAME).getCertificate(certBuilder.build(sigGen));
|
||||||
key = (RSAPrivateKey) keyPair.getPrivate();
|
key = (RSAPrivateKey) keyPair.getPrivate();
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
// Nothing should go wrong here
|
// Nothing should go wrong here
|
||||||
e.printStackTrace();
|
e.printStackTrace();
|
||||||
throw new RuntimeException(e);
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
LimeLog.info("Generated a new key pair");
|
LimeLog.info("Generated a new key pair");
|
||||||
|
|
||||||
// Save the resulting pair
|
// Save the resulting pair
|
||||||
saveCertKeyPair();
|
saveCertKeyPair();
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void saveCertKeyPair() {
|
private void saveCertKeyPair() {
|
||||||
try {
|
try {
|
||||||
FileOutputStream certOut = new FileOutputStream(certFile);
|
FileOutputStream certOut = new FileOutputStream(certFile);
|
||||||
FileOutputStream keyOut = new FileOutputStream(keyFile);
|
FileOutputStream keyOut = new FileOutputStream(keyFile);
|
||||||
|
|
||||||
// Write the certificate in OpenSSL PEM format (important for the server)
|
// Write the certificate in OpenSSL PEM format (important for the server)
|
||||||
StringWriter strWriter = new StringWriter();
|
StringWriter strWriter = new StringWriter();
|
||||||
JcaPEMWriter pemWriter = new JcaPEMWriter(strWriter);
|
JcaPEMWriter pemWriter = new JcaPEMWriter(strWriter);
|
||||||
pemWriter.writeObject(cert);
|
pemWriter.writeObject(cert);
|
||||||
pemWriter.close();
|
pemWriter.close();
|
||||||
|
|
||||||
// Line endings MUST be UNIX for the PC to accept the cert properly
|
// Line endings MUST be UNIX for the PC to accept the cert properly
|
||||||
OutputStreamWriter certWriter = new OutputStreamWriter(certOut);
|
OutputStreamWriter certWriter = new OutputStreamWriter(certOut);
|
||||||
String pemStr = strWriter.getBuffer().toString();
|
String pemStr = strWriter.getBuffer().toString();
|
||||||
for (int i = 0; i < pemStr.length(); i++) {
|
for (int i = 0; i < pemStr.length(); i++) {
|
||||||
char c = pemStr.charAt(i);
|
char c = pemStr.charAt(i);
|
||||||
if (c != '\r')
|
if (c != '\r')
|
||||||
certWriter.append(c);
|
certWriter.append(c);
|
||||||
}
|
}
|
||||||
certWriter.close();
|
certWriter.close();
|
||||||
|
|
||||||
// Write the private out in PKCS8 format
|
// Write the private out in PKCS8 format
|
||||||
keyOut.write(key.getEncoded());
|
keyOut.write(key.getEncoded());
|
||||||
|
|
||||||
certOut.close();
|
certOut.close();
|
||||||
keyOut.close();
|
keyOut.close();
|
||||||
|
|
||||||
LimeLog.info("Saved generated key pair to disk");
|
LimeLog.info("Saved generated key pair to disk");
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
// This isn't good because it means we'll have
|
// This isn't good because it means we'll have
|
||||||
// to re-pair next time
|
// to re-pair next time
|
||||||
e.printStackTrace();
|
e.printStackTrace();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public X509Certificate getClientCertificate() {
|
public X509Certificate getClientCertificate() {
|
||||||
// Use a lock here to ensure only one guy will be generating or loading
|
// Use a lock here to ensure only one guy will be generating or loading
|
||||||
// the certificate and key at a time
|
// the certificate and key at a time
|
||||||
synchronized (globalCryptoLock) {
|
synchronized (globalCryptoLock) {
|
||||||
// Return a loaded cert if we have one
|
// Return a loaded cert if we have one
|
||||||
if (cert != null) {
|
if (cert != null) {
|
||||||
return cert;
|
return cert;
|
||||||
}
|
}
|
||||||
|
|
||||||
// No loaded cert yet, let's see if we have one on disk
|
// No loaded cert yet, let's see if we have one on disk
|
||||||
if (loadCertKeyPair()) {
|
if (loadCertKeyPair()) {
|
||||||
// Got one
|
// Got one
|
||||||
return cert;
|
return cert;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try to generate a new key pair
|
// Try to generate a new key pair
|
||||||
if (!generateCertKeyPair()) {
|
if (!generateCertKeyPair()) {
|
||||||
// Failed
|
// Failed
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load the generated pair
|
// Load the generated pair
|
||||||
loadCertKeyPair();
|
loadCertKeyPair();
|
||||||
return cert;
|
return cert;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public RSAPrivateKey getClientPrivateKey() {
|
public RSAPrivateKey getClientPrivateKey() {
|
||||||
// Use a lock here to ensure only one guy will be generating or loading
|
// Use a lock here to ensure only one guy will be generating or loading
|
||||||
// the certificate and key at a time
|
// the certificate and key at a time
|
||||||
synchronized (globalCryptoLock) {
|
synchronized (globalCryptoLock) {
|
||||||
// Return a loaded key if we have one
|
// Return a loaded key if we have one
|
||||||
if (key != null) {
|
if (key != null) {
|
||||||
return key;
|
return key;
|
||||||
}
|
}
|
||||||
|
|
||||||
// No loaded key yet, let's see if we have one on disk
|
// No loaded key yet, let's see if we have one on disk
|
||||||
if (loadCertKeyPair()) {
|
if (loadCertKeyPair()) {
|
||||||
// Got one
|
// Got one
|
||||||
return key;
|
return key;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try to generate a new key pair
|
// Try to generate a new key pair
|
||||||
if (!generateCertKeyPair()) {
|
if (!generateCertKeyPair()) {
|
||||||
// Failed
|
// Failed
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load the generated pair
|
// Load the generated pair
|
||||||
loadCertKeyPair();
|
loadCertKeyPair();
|
||||||
return key;
|
return key;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public byte[] getPemEncodedClientCertificate() {
|
public byte[] getPemEncodedClientCertificate() {
|
||||||
synchronized (globalCryptoLock) {
|
synchronized (globalCryptoLock) {
|
||||||
// Call our helper function to do the cert loading/generation for us
|
// Call our helper function to do the cert loading/generation for us
|
||||||
getClientCertificate();
|
getClientCertificate();
|
||||||
|
|
||||||
// Return a cached value if we have it
|
// Return a cached value if we have it
|
||||||
return pemCertBytes;
|
return pemCertBytes;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String encodeBase64String(byte[] data) {
|
public String encodeBase64String(byte[] data) {
|
||||||
return Base64.encodeToString(data, Base64.NO_WRAP);
|
return Base64.encodeToString(data, Base64.NO_WRAP);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
File diff suppressed because it is too large.
Load diff
@@ -2,31 +2,41 @@ 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;
|
||||||
public static final int VK_A = 65;
|
public static final int VK_A = 65;
|
||||||
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_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_BACK_SPACE = 8;
|
public static final int VK_BACK_SPACE = 8;
|
||||||
public static final int VK_EQUALS = 61;
|
public static final int VK_EQUALS = 61;
|
||||||
public static final int VK_ESCAPE = 27;
|
public static final int VK_ESCAPE = 27;
|
||||||
public static final int VK_F1 = 112;
|
public static final int VK_F1 = 112;
|
||||||
|
public static final int VK_PERIOD = 46;
|
||||||
|
public static final int VK_INSERT = 155;
|
||||||
|
public static final int VK_OPEN_BRACKET = 91;
|
||||||
|
public static final int VK_WINDOWS = 524;
|
||||||
|
public static final int VK_MINUS = 45;
|
||||||
public static final int VK_END = 35;
|
public static final int VK_END = 35;
|
||||||
public static final int VK_HOME = 36;
|
public static final int VK_HOME = 36;
|
||||||
public static final int VK_NUM_LOCK = 144;
|
public static final int VK_NUM_LOCK = 144;
|
||||||
@@ -36,6 +46,7 @@ public class KeyboardTranslator {
|
|||||||
public static final int VK_CLOSE_BRACKET = 93;
|
public static final int VK_CLOSE_BRACKET = 93;
|
||||||
public static final int VK_SCROLL_LOCK = 145;
|
public static final int VK_SCROLL_LOCK = 145;
|
||||||
public static final int VK_SEMICOLON = 59;
|
public static final int VK_SEMICOLON = 59;
|
||||||
|
public static final int VK_SHIFT = 16;
|
||||||
public static final int VK_SLASH = 47;
|
public static final int VK_SLASH = 47;
|
||||||
public static final int VK_SPACE = 32;
|
public static final int VK_SPACE = 32;
|
||||||
public static final int VK_PRINTSCREEN = 154;
|
public static final int VK_PRINTSCREEN = 154;
|
||||||
@@ -48,18 +59,12 @@ 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;
|
||||||
|
|
||||||
public static boolean needsShift(int keycode) {
|
/**
|
||||||
switch (keycode)
|
* Constructs a new translator for the specified connection
|
||||||
{
|
* @param conn the connection to which the translated codes are sent
|
||||||
case KeyEvent.KEYCODE_AT:
|
*/
|
||||||
case KeyEvent.KEYCODE_POUND:
|
public KeyboardTranslator(NvConnection conn) {
|
||||||
case KeyEvent.KEYCODE_PLUS:
|
super(conn);
|
||||||
case KeyEvent.KEYCODE_STAR:
|
|
||||||
return true;
|
|
||||||
|
|
||||||
default:
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -67,12 +72,14 @@ public class KeyboardTranslator {
|
|||||||
* @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;
|
||||||
|
|
||||||
// This is a poor man's mapping between Android key codes
|
/* There seems to be no clean mapping between Android key codes
|
||||||
// and Windows VK_* codes. For all defined VK_ codes, see:
|
* and what Nvidia sends over the wire. If someone finds one,
|
||||||
// https://msdn.microsoft.com/en-us/library/windows/desktop/dd375731(v=vs.85).aspx
|
* I'll happily delete this code :)
|
||||||
|
*/
|
||||||
if (keycode >= KeyEvent.KEYCODE_0 &&
|
if (keycode >= KeyEvent.KEYCODE_0 &&
|
||||||
keycode <= KeyEvent.KEYCODE_9) {
|
keycode <= KeyEvent.KEYCODE_9) {
|
||||||
translated = (keycode - KeyEvent.KEYCODE_0) + VK_0;
|
translated = (keycode - KeyEvent.KEYCODE_0) + VK_0;
|
||||||
@@ -92,11 +99,8 @@ public class KeyboardTranslator {
|
|||||||
else {
|
else {
|
||||||
switch (keycode) {
|
switch (keycode) {
|
||||||
case KeyEvent.KEYCODE_ALT_LEFT:
|
case KeyEvent.KEYCODE_ALT_LEFT:
|
||||||
translated = 0xA4;
|
|
||||||
break;
|
|
||||||
|
|
||||||
case KeyEvent.KEYCODE_ALT_RIGHT:
|
case KeyEvent.KEYCODE_ALT_RIGHT:
|
||||||
translated = 0xA5;
|
translated = VK_ALT;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case KeyEvent.KEYCODE_BACKSLASH:
|
case KeyEvent.KEYCODE_BACKSLASH:
|
||||||
@@ -116,11 +120,8 @@ public class KeyboardTranslator {
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case KeyEvent.KEYCODE_CTRL_LEFT:
|
case KeyEvent.KEYCODE_CTRL_LEFT:
|
||||||
translated = 0xA2;
|
|
||||||
break;
|
|
||||||
|
|
||||||
case KeyEvent.KEYCODE_CTRL_RIGHT:
|
case KeyEvent.KEYCODE_CTRL_RIGHT:
|
||||||
translated = 0xA3;
|
translated = VK_CONTROL;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case KeyEvent.KEYCODE_DEL:
|
case KeyEvent.KEYCODE_DEL:
|
||||||
@@ -131,7 +132,6 @@ public class KeyboardTranslator {
|
|||||||
translated = 0x0d;
|
translated = 0x0d;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case KeyEvent.KEYCODE_PLUS:
|
|
||||||
case KeyEvent.KEYCODE_EQUALS:
|
case KeyEvent.KEYCODE_EQUALS:
|
||||||
translated = 0xbb;
|
translated = 0xbb;
|
||||||
break;
|
break;
|
||||||
@@ -141,11 +141,12 @@ public class KeyboardTranslator {
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case KeyEvent.KEYCODE_FORWARD_DEL:
|
case KeyEvent.KEYCODE_FORWARD_DEL:
|
||||||
translated = 0x2e;
|
// Nvidia maps period to delete
|
||||||
|
translated = VK_PERIOD;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case KeyEvent.KEYCODE_INSERT:
|
case KeyEvent.KEYCODE_INSERT:
|
||||||
translated = 0x2d;
|
translated = -1;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case KeyEvent.KEYCODE_LEFT_BRACKET:
|
case KeyEvent.KEYCODE_LEFT_BRACKET:
|
||||||
@@ -153,11 +154,8 @@ public class KeyboardTranslator {
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case KeyEvent.KEYCODE_META_LEFT:
|
case KeyEvent.KEYCODE_META_LEFT:
|
||||||
translated = 0x5b;
|
|
||||||
break;
|
|
||||||
|
|
||||||
case KeyEvent.KEYCODE_META_RIGHT:
|
case KeyEvent.KEYCODE_META_RIGHT:
|
||||||
translated = 0x5c;
|
translated = VK_WINDOWS;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case KeyEvent.KEYCODE_MINUS:
|
case KeyEvent.KEYCODE_MINUS:
|
||||||
@@ -201,11 +199,8 @@ public class KeyboardTranslator {
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case KeyEvent.KEYCODE_SHIFT_LEFT:
|
case KeyEvent.KEYCODE_SHIFT_LEFT:
|
||||||
translated = 0xA0;
|
|
||||||
break;
|
|
||||||
|
|
||||||
case KeyEvent.KEYCODE_SHIFT_RIGHT:
|
case KeyEvent.KEYCODE_SHIFT_RIGHT:
|
||||||
translated = 0xA1;
|
translated = VK_SHIFT;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case KeyEvent.KEYCODE_SLASH:
|
case KeyEvent.KEYCODE_SLASH:
|
||||||
@@ -253,38 +248,6 @@ public class KeyboardTranslator {
|
|||||||
translated = VK_PAUSE;
|
translated = VK_PAUSE;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case KeyEvent.KEYCODE_NUMPAD_DIVIDE:
|
|
||||||
translated = 0x6F;
|
|
||||||
break;
|
|
||||||
|
|
||||||
case KeyEvent.KEYCODE_NUMPAD_MULTIPLY:
|
|
||||||
translated = 0x6A;
|
|
||||||
break;
|
|
||||||
|
|
||||||
case KeyEvent.KEYCODE_NUMPAD_SUBTRACT:
|
|
||||||
translated = 0x6D;
|
|
||||||
break;
|
|
||||||
|
|
||||||
case KeyEvent.KEYCODE_NUMPAD_ADD:
|
|
||||||
translated = 0x6B;
|
|
||||||
break;
|
|
||||||
|
|
||||||
case KeyEvent.KEYCODE_NUMPAD_DOT:
|
|
||||||
translated = 0x6E;
|
|
||||||
break;
|
|
||||||
|
|
||||||
case KeyEvent.KEYCODE_AT:
|
|
||||||
translated = 2 + VK_0;
|
|
||||||
break;
|
|
||||||
|
|
||||||
case KeyEvent.KEYCODE_POUND:
|
|
||||||
translated = 3 + VK_0;
|
|
||||||
break;
|
|
||||||
|
|
||||||
case KeyEvent.KEYCODE_STAR:
|
|
||||||
translated = 8 + VK_0;
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
default:
|
||||||
System.out.println("No key for "+keycode);
|
System.out.println("No key for "+keycode);
|
||||||
return 0;
|
return 0;
|
||||||
|
|||||||
@@ -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;
|
|
||||||
private final View targetView;
|
|
||||||
|
|
||||||
private static final int TAP_MOVEMENT_THRESHOLD = 20;
|
public TouchContext(NvConnection conn, int actionIndex, double xFactor, double yFactor)
|
||||||
private static final int TAP_DISTANCE_THRESHOLD = 25;
|
{
|
||||||
private static final int TAP_TIME_THRESHOLD = 250;
|
this.conn = conn;
|
||||||
private static final int DRAG_TIME_THRESHOLD = 650;
|
this.actionIndex = actionIndex;
|
||||||
|
this.xFactor = xFactor;
|
||||||
public TouchContext(NvConnection conn, int actionIndex,
|
this.yFactor = yFactor;
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean isTap()
|
return xDelta <= TAP_MOVEMENT_THRESHOLD &&
|
||||||
{
|
yDelta <= TAP_MOVEMENT_THRESHOLD &&
|
||||||
long timeDelta = System.currentTimeMillis() - originalTouchTime;
|
timeDelta <= TAP_TIME_THRESHOLD;
|
||||||
|
}
|
||||||
|
|
||||||
return isWithinTapBounds(lastTouchX, lastTouchY) && timeDelta <= TAP_TIME_THRESHOLD;
|
private byte getMouseButtonIndex()
|
||||||
}
|
{
|
||||||
|
if (actionIndex == 1) {
|
||||||
|
return MouseButtonPacket.BUTTON_RIGHT;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return MouseButtonPacket.BUTTON_LEFT;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private byte getMouseButtonIndex()
|
public boolean touchDownEvent(int eventX, int eventY)
|
||||||
{
|
{
|
||||||
if (actionIndex == 1) {
|
originalTouchX = lastTouchX = eventX;
|
||||||
return MouseButtonPacket.BUTTON_RIGHT;
|
originalTouchY = lastTouchY = eventY;
|
||||||
}
|
originalTouchTime = System.currentTimeMillis();
|
||||||
else {
|
cancelled = false;
|
||||||
return MouseButtonPacket.BUTTON_LEFT;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean touchDownEvent(int eventX, int eventY)
|
|
||||||
{
|
|
||||||
// Get the view dimensions to scale inputs on this touch
|
|
||||||
xFactor = referenceWidth / (double)targetView.getWidth();
|
|
||||||
yFactor = referenceHeight / (double)targetView.getHeight();
|
|
||||||
|
|
||||||
originalTouchX = lastTouchX = eventX;
|
|
||||||
originalTouchY = lastTouchY = eventY;
|
|
||||||
originalTouchTime = System.currentTimeMillis();
|
|
||||||
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
|
||||||
|
conn.sendMouseButtonDown(buttonIndex);
|
||||||
|
|
||||||
if (confirmedDrag) {
|
// We need to sleep a bit here because some games
|
||||||
// Raise the button after a drag
|
// do input detection by polling
|
||||||
conn.sendMouseButtonUp(buttonIndex);
|
try {
|
||||||
}
|
Thread.sleep(100);
|
||||||
else if (isTap())
|
} catch (InterruptedException ignored) {}
|
||||||
{
|
|
||||||
// Lower the mouse button
|
|
||||||
conn.sendMouseButtonDown(buttonIndex);
|
|
||||||
|
|
||||||
// We need to sleep a bit here because some games
|
// Raise the mouse button
|
||||||
// do input detection by polling
|
conn.sendMouseButtonUp(buttonIndex);
|
||||||
try {
|
}
|
||||||
Thread.sleep(100);
|
}
|
||||||
} catch (InterruptedException ignored) {}
|
|
||||||
|
|
||||||
// Raise the mouse button
|
public boolean touchMoveEvent(int eventX, int eventY)
|
||||||
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;
|
|
||||||
}
|
|
||||||
if (eventY < lastTouchY) {
|
|
||||||
deltaY = -deltaY;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the scaling factor ended up rounding deltas to zero, wait until they are
|
lastTouchX = eventX;
|
||||||
// non-zero to update lastTouch that way devices that report small touch events often
|
lastTouchY = eventY;
|
||||||
// will work correctly
|
}
|
||||||
if (deltaX != 0) {
|
|
||||||
lastTouchX = eventX;
|
|
||||||
}
|
|
||||||
if (deltaY != 0) {
|
|
||||||
lastTouchY = eventY;
|
|
||||||
}
|
|
||||||
|
|
||||||
conn.sendMouseMove((short)deltaX, (short)deltaY);
|
return true;
|
||||||
}
|
}
|
||||||
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() {
|
||||||
|
|||||||
-61
@@ -1,61 +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) {
|
|
||||||
float x = event.getX();
|
|
||||||
for (int i = 0; i < event.getHistorySize(); i++) {
|
|
||||||
x += event.getHistoricalX(i);
|
|
||||||
}
|
|
||||||
return x;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public float getRelativeAxisY(MotionEvent event) {
|
|
||||||
float y = event.getY();
|
|
||||||
for (int i = 0; i < event.getHistorySize(); i++) {
|
|
||||||
y += event.getHistoricalY(i);
|
|
||||||
}
|
|
||||||
return y;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-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,38 +0,0 @@
|
|||||||
package com.limelight.binding.input.capture;
|
|
||||||
|
|
||||||
import android.app.Activity;
|
|
||||||
|
|
||||||
import com.limelight.LimeLog;
|
|
||||||
import com.limelight.LimelightBuildProps;
|
|
||||||
import com.limelight.R;
|
|
||||||
import com.limelight.binding.input.evdev.EvdevCaptureProviderShim;
|
|
||||||
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));
|
|
||||||
}
|
|
||||||
// LineageOS implemented broken NVIDIA capture extensions, so avoid using them on root builds.
|
|
||||||
// See https://github.com/LineageOS/android_frameworks_base/commit/d304f478a023430f4712dbdc3ee69d9ad02cebd3
|
|
||||||
else if (!LimelightBuildProps.ROOT_BUILD && ShieldCaptureProvider.isCaptureProviderSupported()) {
|
|
||||||
LimeLog.info("Using NVIDIA mouse capture extension");
|
|
||||||
return new ShieldCaptureProvider(activity);
|
|
||||||
}
|
|
||||||
else if (EvdevCaptureProviderShim.isCaptureProviderSupported()) {
|
|
||||||
LimeLog.info("Using Evdev mouse capture");
|
|
||||||
return EvdevCaptureProviderShim.createEvdevCaptureProvider(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,49 +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;
|
|
||||||
}
|
|
||||||
|
|
||||||
public abstract void rumble(short lowFreqMotor, short highFreqMotor);
|
|
||||||
|
|
||||||
protected void notifyDeviceRemoved() {
|
|
||||||
listener.deviceRemoved(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected void notifyDeviceAdded() {
|
|
||||||
listener.deviceAdded(this);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,152 +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;
|
|
||||||
|
|
||||||
// Cancel any rumble effects
|
|
||||||
rumble((short)0, (short)0);
|
|
||||||
|
|
||||||
// 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(AbstractController controller);
|
|
||||||
void deviceAdded(AbstractController controller);
|
|
||||||
}
|
|
||||||
@@ -1,240 +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.Handler;
|
|
||||||
import android.os.IBinder;
|
|
||||||
import android.view.InputDevice;
|
|
||||||
import android.widget.Toast;
|
|
||||||
|
|
||||||
import com.limelight.LimeLog;
|
|
||||||
import com.limelight.R;
|
|
||||||
import com.limelight.preferences.PreferenceConfiguration;
|
|
||||||
|
|
||||||
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 PreferenceConfiguration prefConfig;
|
|
||||||
|
|
||||||
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(AbstractController controller) {
|
|
||||||
// Remove the the controller from our list (if not removed already)
|
|
||||||
controllers.remove(controller);
|
|
||||||
|
|
||||||
// Call through to the client's listener
|
|
||||||
if (listener != null) {
|
|
||||||
listener.deviceRemoved(controller);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void deviceAdded(AbstractController controller) {
|
|
||||||
// Call through to the client's listener
|
|
||||||
if (listener != null) {
|
|
||||||
listener.deviceAdded(controller);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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)) {
|
|
||||||
final UsbDevice device = intent.getParcelableExtra(UsbManager.EXTRA_DEVICE);
|
|
||||||
|
|
||||||
// shouldClaimDevice() looks at the kernel's enumerated input
|
|
||||||
// devices to make its decision about whether to prompt to take
|
|
||||||
// control of the device. The kernel bringing up the input stack
|
|
||||||
// may race with this callback and cause us to prompt when the
|
|
||||||
// kernel is capable of running the device. Let's post a delayed
|
|
||||||
// message to process this state change to allow the kernel
|
|
||||||
// some time to bring up the stack.
|
|
||||||
new Handler().postDelayed(new Runnable() {
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
// Continue the state machine
|
|
||||||
handleUsbDeviceState(device);
|
|
||||||
}
|
|
||||||
}, 1000);
|
|
||||||
}
|
|
||||||
// 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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void handleUsbDeviceState(UsbDevice device) {
|
|
||||||
// Are we able to operate it?
|
|
||||||
if (shouldClaimDevice(device, prefConfig.bindAllUsb)) {
|
|
||||||
// Do we have permission yet?
|
|
||||||
if (!usbManager.hasPermission(device)) {
|
|
||||||
// Let's ask for permission
|
|
||||||
try {
|
|
||||||
// This function is not documented as throwing any exceptions (denying access
|
|
||||||
// is indicated by calling the PendingIntent with a false result). However,
|
|
||||||
// Samsung Knox has some policies which block this request, but rather than
|
|
||||||
// just returning a false result or returning 0 enumerated devices,
|
|
||||||
// they throw an undocumented SecurityException from this call, crashing
|
|
||||||
// the whole app. :(
|
|
||||||
usbManager.requestPermission(device, PendingIntent.getBroadcast(UsbDriverService.this, 0, new Intent(ACTION_USB_PERMISSION), 0));
|
|
||||||
} catch (SecurityException e) {
|
|
||||||
Toast.makeText(this, this.getText(R.string.error_usb_prohibited), Toast.LENGTH_LONG).show();
|
|
||||||
}
|
|
||||||
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 static 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 == null) {
|
|
||||||
// Device was removed while looping
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (inputDev.getVendorId() == device.getVendorId() &&
|
|
||||||
inputDev.getProductId() == device.getProductId()) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static boolean shouldClaimDevice(UsbDevice device, boolean claimAllAvailable) {
|
|
||||||
// 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) || claimAllAvailable) && Xbox360Controller.canClaimDevice(device));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onCreate() {
|
|
||||||
this.usbManager = (UsbManager) getSystemService(Context.USB_SERVICE);
|
|
||||||
this.prefConfig = PreferenceConfiguration.readPreferences(this);
|
|
||||||
|
|
||||||
// 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, prefConfig.bindAllUsb)) {
|
|
||||||
// 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,155 +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 = {
|
|
||||||
0x0079, // GPD Win 2
|
|
||||||
0x044f, // Thrustmaster
|
|
||||||
0x045e, // Microsoft
|
|
||||||
0x046d, // Logitech
|
|
||||||
0x056e, // Elecom
|
|
||||||
0x06a3, // Saitek
|
|
||||||
0x0738, // Mad Catz
|
|
||||||
0x07ff, // Mad Catz
|
|
||||||
0x0e6f, // Unknown
|
|
||||||
0x0f0d, // Hori
|
|
||||||
0x1038, // SteelSeries
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void rumble(short lowFreqMotor, short highFreqMotor) {
|
|
||||||
byte[] data = {
|
|
||||||
0x00, 0x08, 0x00,
|
|
||||||
(byte)(lowFreqMotor >> 8), (byte)(highFreqMotor >> 8),
|
|
||||||
0x00, 0x00, 0x00
|
|
||||||
};
|
|
||||||
int res = connection.bulkTransfer(outEndpt, data, data.length, 100);
|
|
||||||
if (res != data.length) {
|
|
||||||
LimeLog.warning("Rumble transfer failed: "+res);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,189 +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;
|
|
||||||
import java.util.Arrays;
|
|
||||||
|
|
||||||
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
|
|
||||||
};
|
|
||||||
|
|
||||||
private static final byte[] FW2015_INIT = {0x05, 0x20, 0x00, 0x01, 0x00};
|
|
||||||
private static final byte[] HORI_INIT = {0x01, 0x20, 0x00, 0x09, 0x00, 0x04, 0x20, 0x3a,
|
|
||||||
0x00, 0x00, 0x00, (byte)0x80, 0x00};
|
|
||||||
private static final byte[] PDP_INIT1 = {0x0a, 0x20, 0x00, 0x03, 0x00, 0x01, 0x14};
|
|
||||||
private static final byte[] PDP_INIT2 = {0x06, 0x20, 0x00, 0x02, 0x01, 0x00};
|
|
||||||
private static final byte[] RUMBLE_INIT1 = {0x09, 0x00, 0x00, 0x09, 0x00, 0x0F, 0x00, 0x00,
|
|
||||||
0x1D, 0x1D, (byte)0xFF, 0x00, 0x00};
|
|
||||||
private static final byte[] RUMBLE_INIT2 = {0x09, 0x00, 0x00, 0x09, 0x00, 0x0F, 0x00, 0x00,
|
|
||||||
0x00, 0x00, 0x00, 0x00, 0x00};
|
|
||||||
|
|
||||||
private static InitPacket[] INIT_PKTS = {
|
|
||||||
new InitPacket(0x0e6f, 0x0165, HORI_INIT),
|
|
||||||
new InitPacket(0x0f0d, 0x0067, HORI_INIT),
|
|
||||||
new InitPacket(0x0000, 0x0000, FW2015_INIT),
|
|
||||||
new InitPacket(0x0e6f, 0x0000, PDP_INIT1),
|
|
||||||
new InitPacket(0x0e6f, 0x0000, PDP_INIT2),
|
|
||||||
new InitPacket(0x24c6, 0x541a, RUMBLE_INIT1),
|
|
||||||
new InitPacket(0x24c6, 0x542a, RUMBLE_INIT1),
|
|
||||||
new InitPacket(0x24c6, 0x543a, RUMBLE_INIT1),
|
|
||||||
new InitPacket(0x24c6, 0x541a, RUMBLE_INIT2),
|
|
||||||
new InitPacket(0x24c6, 0x542a, RUMBLE_INIT2),
|
|
||||||
new InitPacket(0x24c6, 0x543a, RUMBLE_INIT2),
|
|
||||||
};
|
|
||||||
|
|
||||||
private byte seqNum = 0;
|
|
||||||
|
|
||||||
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 all applicable init packets
|
|
||||||
for (InitPacket pkt : INIT_PKTS) {
|
|
||||||
if (pkt.vendorId != 0 && device.getVendorId() != pkt.vendorId) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (pkt.productId != 0 && device.getProductId() != pkt.productId) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
byte[] data = Arrays.copyOf(pkt.data, pkt.data.length);
|
|
||||||
|
|
||||||
// Populate sequence number
|
|
||||||
data[2] = seqNum++;
|
|
||||||
|
|
||||||
// Send the initialization packet
|
|
||||||
int res = connection.bulkTransfer(outEndpt, data, data.length, 3000);
|
|
||||||
if (res != data.length) {
|
|
||||||
LimeLog.warning("Initialization transfer failed: "+res);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void rumble(short lowFreqMotor, short highFreqMotor) {
|
|
||||||
byte[] data = {
|
|
||||||
0x09, 0x00, seqNum++, 0x09, 0x00,
|
|
||||||
0x0F, 0x00, 0x00,
|
|
||||||
(byte)(lowFreqMotor >> 9), (byte)(highFreqMotor >> 9),
|
|
||||||
(byte)0xFF, 0x00, (byte)0xFF
|
|
||||||
};
|
|
||||||
int res = connection.bulkTransfer(outEndpt, data, data.length, 100);
|
|
||||||
if (res != data.length) {
|
|
||||||
LimeLog.warning("Rumble transfer failed: "+res);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static class InitPacket {
|
|
||||||
final int vendorId;
|
|
||||||
final int productId;
|
|
||||||
final byte[] data;
|
|
||||||
|
|
||||||
InitPacket(int vendorId, int productId, byte[] data) {
|
|
||||||
this.vendorId = vendorId;
|
|
||||||
this.productId = productId;
|
|
||||||
this.data = data;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
package com.limelight.binding.input.evdev;
|
|
||||||
|
|
||||||
|
|
||||||
import android.app.Activity;
|
|
||||||
|
|
||||||
import com.limelight.LimelightBuildProps;
|
|
||||||
import com.limelight.binding.input.capture.InputCaptureProvider;
|
|
||||||
|
|
||||||
public class EvdevCaptureProviderShim {
|
|
||||||
public static boolean isCaptureProviderSupported() {
|
|
||||||
return LimelightBuildProps.ROOT_BUILD;
|
|
||||||
}
|
|
||||||
|
|
||||||
// We need to construct our capture provider using reflection because it isn't included in non-root builds
|
|
||||||
public static InputCaptureProvider createEvdevCaptureProvider(Activity activity, EvdevListener listener) {
|
|
||||||
try {
|
|
||||||
Class providerClass = Class.forName("com.limelight.binding.input.evdev.EvdevCaptureProvider");
|
|
||||||
return (InputCaptureProvider) providerClass.getConstructors()[0].newInstance(activity, listener);
|
|
||||||
} catch (Exception e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
throw new RuntimeException(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
package com.limelight.binding.input.evdev;
|
||||||
|
|
||||||
|
public class EvdevEvent {
|
||||||
|
public static final int EVDEV_MIN_EVENT_SIZE = 16;
|
||||||
|
public static final int EVDEV_MAX_EVENT_SIZE = 24;
|
||||||
|
|
||||||
|
/* Event types */
|
||||||
|
public static final short EV_SYN = 0x00;
|
||||||
|
public static final short EV_KEY = 0x01;
|
||||||
|
public static final short EV_REL = 0x02;
|
||||||
|
public static final short EV_MSC = 0x04;
|
||||||
|
|
||||||
|
/* Relative axes */
|
||||||
|
public static final short REL_X = 0x00;
|
||||||
|
public static final short REL_Y = 0x01;
|
||||||
|
public static final short REL_WHEEL = 0x08;
|
||||||
|
|
||||||
|
/* Buttons */
|
||||||
|
public static final short BTN_LEFT = 0x110;
|
||||||
|
public static final short BTN_RIGHT = 0x111;
|
||||||
|
public static final short BTN_MIDDLE = 0x112;
|
||||||
|
public static final short BTN_SIDE = 0x113;
|
||||||
|
public static final short BTN_EXTRA = 0x114;
|
||||||
|
public static final short BTN_FORWARD = 0x115;
|
||||||
|
public static final short BTN_BACK = 0x116;
|
||||||
|
public static final short BTN_TASK = 0x117;
|
||||||
|
public static final short BTN_GAMEPAD = 0x130;
|
||||||
|
|
||||||
|
/* Keys */
|
||||||
|
public static final short KEY_Q = 16;
|
||||||
|
|
||||||
|
public short type;
|
||||||
|
public short code;
|
||||||
|
public int 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,14 +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;
|
||||||
int BUTTON_X1 = 4;
|
|
||||||
int BUTTON_X2 = 5;
|
|
||||||
|
|
||||||
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);
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
package com.limelight.binding.input.evdev;
|
||||||
|
|
||||||
|
import android.os.Build;
|
||||||
|
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
import java.util.Locale;
|
||||||
|
|
||||||
|
import com.limelight.LimeLog;
|
||||||
|
|
||||||
|
public class EvdevReader {
|
||||||
|
static {
|
||||||
|
System.loadLibrary("evdev_reader");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void patchSeLinuxPolicies() {
|
||||||
|
//
|
||||||
|
// FIXME: We REALLY shouldn't being changing permissions on the input devices like this.
|
||||||
|
// We should probably do something clever with a separate daemon and talk via a localhost
|
||||||
|
// socket. We don't return the SELinux policies back to default after we're done which I feel
|
||||||
|
// bad about, but we do chmod the input devices back so I don't think any additional attack surface
|
||||||
|
// remains opened after streaming other than listing the /dev/input directory which you wouldn't
|
||||||
|
// normally be able to do with SELinux enforcing on Lollipop.
|
||||||
|
//
|
||||||
|
// We need to modify SELinux policies to allow us to capture input devices on Lollipop and possibly other
|
||||||
|
// more restrictive ROMs. Per Chainfire's SuperSU documentation, the supolicy binary is provided on
|
||||||
|
// 4.4 and later to do live SELinux policy changes.
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
|
||||||
|
EvdevShell shell = EvdevShell.getInstance();
|
||||||
|
shell.runCommand("supolicy --live \"allow untrusted_app input_device dir { 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();
|
||||||
|
|
||||||
|
for (String file : files) {
|
||||||
|
shell.runCommand(String.format((Locale)null, "chmod %o %s", octalPermissions, file));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns the fd to be passed to other function or -1 on error
|
||||||
|
public static native int open(String fileName);
|
||||||
|
|
||||||
|
// Prevent other apps (including Android itself) from using the device while "grabbed"
|
||||||
|
public static native boolean grab(int fd);
|
||||||
|
public static native boolean ungrab(int fd);
|
||||||
|
|
||||||
|
// Used for checking device capabilities
|
||||||
|
public static native boolean hasRelAxis(int fd, short axis);
|
||||||
|
public static native boolean hasAbsAxis(int fd, short axis);
|
||||||
|
public static native boolean hasKey(int fd, short key);
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,139 @@
|
|||||||
|
package com.limelight.binding.input.evdev;
|
||||||
|
|
||||||
|
import android.view.KeyEvent;
|
||||||
|
|
||||||
|
public class EvdevTranslator {
|
||||||
|
|
||||||
|
public static final short EVDEV_KEY_CODES[] = {
|
||||||
|
0, //KeyEvent.VK_RESERVED
|
||||||
|
KeyEvent.KEYCODE_ESCAPE,
|
||||||
|
KeyEvent.KEYCODE_1,
|
||||||
|
KeyEvent.KEYCODE_2,
|
||||||
|
KeyEvent.KEYCODE_3,
|
||||||
|
KeyEvent.KEYCODE_4,
|
||||||
|
KeyEvent.KEYCODE_5,
|
||||||
|
KeyEvent.KEYCODE_6,
|
||||||
|
KeyEvent.KEYCODE_7,
|
||||||
|
KeyEvent.KEYCODE_8,
|
||||||
|
KeyEvent.KEYCODE_9,
|
||||||
|
KeyEvent.KEYCODE_0,
|
||||||
|
KeyEvent.KEYCODE_MINUS,
|
||||||
|
KeyEvent.KEYCODE_EQUALS,
|
||||||
|
KeyEvent.KEYCODE_DEL,
|
||||||
|
KeyEvent.KEYCODE_TAB,
|
||||||
|
KeyEvent.KEYCODE_Q,
|
||||||
|
KeyEvent.KEYCODE_W,
|
||||||
|
KeyEvent.KEYCODE_E,
|
||||||
|
KeyEvent.KEYCODE_R,
|
||||||
|
KeyEvent.KEYCODE_T,
|
||||||
|
KeyEvent.KEYCODE_Y,
|
||||||
|
KeyEvent.KEYCODE_U,
|
||||||
|
KeyEvent.KEYCODE_I,
|
||||||
|
KeyEvent.KEYCODE_O,
|
||||||
|
KeyEvent.KEYCODE_P,
|
||||||
|
KeyEvent.KEYCODE_LEFT_BRACKET,
|
||||||
|
KeyEvent.KEYCODE_RIGHT_BRACKET,
|
||||||
|
KeyEvent.KEYCODE_ENTER,
|
||||||
|
KeyEvent.KEYCODE_CTRL_LEFT,
|
||||||
|
KeyEvent.KEYCODE_A,
|
||||||
|
KeyEvent.KEYCODE_S,
|
||||||
|
KeyEvent.KEYCODE_D,
|
||||||
|
KeyEvent.KEYCODE_F,
|
||||||
|
KeyEvent.KEYCODE_G,
|
||||||
|
KeyEvent.KEYCODE_H,
|
||||||
|
KeyEvent.KEYCODE_J,
|
||||||
|
KeyEvent.KEYCODE_K,
|
||||||
|
KeyEvent.KEYCODE_L,
|
||||||
|
KeyEvent.KEYCODE_SEMICOLON,
|
||||||
|
KeyEvent.KEYCODE_APOSTROPHE,
|
||||||
|
KeyEvent.KEYCODE_GRAVE,
|
||||||
|
KeyEvent.KEYCODE_SHIFT_LEFT,
|
||||||
|
KeyEvent.KEYCODE_BACKSLASH,
|
||||||
|
KeyEvent.KEYCODE_Z,
|
||||||
|
KeyEvent.KEYCODE_X,
|
||||||
|
KeyEvent.KEYCODE_C,
|
||||||
|
KeyEvent.KEYCODE_V,
|
||||||
|
KeyEvent.KEYCODE_B,
|
||||||
|
KeyEvent.KEYCODE_N,
|
||||||
|
KeyEvent.KEYCODE_M,
|
||||||
|
KeyEvent.KEYCODE_COMMA,
|
||||||
|
KeyEvent.KEYCODE_PERIOD,
|
||||||
|
KeyEvent.KEYCODE_SLASH,
|
||||||
|
KeyEvent.KEYCODE_SHIFT_RIGHT,
|
||||||
|
KeyEvent.KEYCODE_NUMPAD_MULTIPLY,
|
||||||
|
KeyEvent.KEYCODE_ALT_LEFT,
|
||||||
|
KeyEvent.KEYCODE_SPACE,
|
||||||
|
KeyEvent.KEYCODE_CAPS_LOCK,
|
||||||
|
KeyEvent.KEYCODE_F1,
|
||||||
|
KeyEvent.KEYCODE_F2,
|
||||||
|
KeyEvent.KEYCODE_F3,
|
||||||
|
KeyEvent.KEYCODE_F4,
|
||||||
|
KeyEvent.KEYCODE_F5,
|
||||||
|
KeyEvent.KEYCODE_F6,
|
||||||
|
KeyEvent.KEYCODE_F7,
|
||||||
|
KeyEvent.KEYCODE_F8,
|
||||||
|
KeyEvent.KEYCODE_F9,
|
||||||
|
KeyEvent.KEYCODE_F10,
|
||||||
|
KeyEvent.KEYCODE_NUM_LOCK,
|
||||||
|
KeyEvent.KEYCODE_SCROLL_LOCK,
|
||||||
|
KeyEvent.KEYCODE_NUMPAD_7,
|
||||||
|
KeyEvent.KEYCODE_NUMPAD_8,
|
||||||
|
KeyEvent.KEYCODE_NUMPAD_9,
|
||||||
|
KeyEvent.KEYCODE_NUMPAD_SUBTRACT,
|
||||||
|
KeyEvent.KEYCODE_NUMPAD_4,
|
||||||
|
KeyEvent.KEYCODE_NUMPAD_5,
|
||||||
|
KeyEvent.KEYCODE_NUMPAD_6,
|
||||||
|
KeyEvent.KEYCODE_NUMPAD_ADD,
|
||||||
|
KeyEvent.KEYCODE_NUMPAD_1,
|
||||||
|
KeyEvent.KEYCODE_NUMPAD_2,
|
||||||
|
KeyEvent.KEYCODE_NUMPAD_3,
|
||||||
|
KeyEvent.KEYCODE_NUMPAD_0,
|
||||||
|
KeyEvent.KEYCODE_NUMPAD_DOT,
|
||||||
|
0,
|
||||||
|
0, //KeyEvent.VK_ZENKAKUHANKAKU,
|
||||||
|
0, //KeyEvent.VK_102ND,
|
||||||
|
KeyEvent.KEYCODE_F11,
|
||||||
|
KeyEvent.KEYCODE_F12,
|
||||||
|
0, //KeyEvent.VK_RO,
|
||||||
|
0, //KeyEvent.VK_KATAKANA,
|
||||||
|
0, //KeyEvent.VK_HIRAGANA,
|
||||||
|
0, //KeyEvent.VK_HENKAN,
|
||||||
|
0, //KeyEvent.VK_KATAKANAHIRAGANA,
|
||||||
|
0, //KeyEvent.VK_MUHENKAN,
|
||||||
|
0, //KeyEvent.VK_KPJPCOMMA,
|
||||||
|
KeyEvent.KEYCODE_NUMPAD_ENTER,
|
||||||
|
KeyEvent.KEYCODE_CTRL_RIGHT,
|
||||||
|
KeyEvent.KEYCODE_NUMPAD_DIVIDE,
|
||||||
|
KeyEvent.KEYCODE_SYSRQ,
|
||||||
|
KeyEvent.KEYCODE_ALT_RIGHT,
|
||||||
|
0, //KeyEvent.VK_LINEFEED,
|
||||||
|
KeyEvent.KEYCODE_HOME,
|
||||||
|
KeyEvent.KEYCODE_DPAD_UP,
|
||||||
|
KeyEvent.KEYCODE_PAGE_UP,
|
||||||
|
KeyEvent.KEYCODE_DPAD_LEFT,
|
||||||
|
KeyEvent.KEYCODE_DPAD_RIGHT,
|
||||||
|
KeyEvent.KEYCODE_MOVE_END,
|
||||||
|
KeyEvent.KEYCODE_DPAD_DOWN,
|
||||||
|
KeyEvent.KEYCODE_PAGE_DOWN,
|
||||||
|
KeyEvent.KEYCODE_INSERT,
|
||||||
|
KeyEvent.KEYCODE_FORWARD_DEL,
|
||||||
|
0, //KeyEvent.VK_MACRO,
|
||||||
|
0, //KeyEvent.VK_MUTE,
|
||||||
|
0, //KeyEvent.VK_VOLUMEDOWN,
|
||||||
|
0, //KeyEvent.VK_VOLUMEUP,
|
||||||
|
0, //KeyEvent.VK_POWER, /* SC System Power Down */
|
||||||
|
KeyEvent.KEYCODE_NUMPAD_EQUALS,
|
||||||
|
0, //KeyEvent.VK_KPPLUSMINUS,
|
||||||
|
KeyEvent.KEYCODE_BREAK,
|
||||||
|
0, //KeyEvent.VK_SCALE, /* AL Compiz Scale (Expose) */
|
||||||
|
};
|
||||||
|
|
||||||
|
public static short translateEvdevKeyCode(short evdevKeyCode) {
|
||||||
|
if (evdevKeyCode < EVDEV_KEY_CODES.length) {
|
||||||
|
return EVDEV_KEY_CODES[evdevKeyCode];
|
||||||
|
}
|
||||||
|
|
||||||
|
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,350 +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, int elementId) {
|
|
||||||
super(controller, context, elementId);
|
|
||||||
// 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, 100);
|
|
||||||
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 elementId, int layer, Context context) {
|
|
||||||
super(controller, context, elementId);
|
|
||||||
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, EID_DPAD);
|
|
||||||
}
|
|
||||||
|
|
||||||
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, EID_LS);
|
|
||||||
|
|
||||||
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, EID_LT, 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, EID_RS);
|
|
||||||
|
|
||||||
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, EID_RT, 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();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-189
@@ -1,189 +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.binding.input.ControllerHandler;
|
|
||||||
import com.limelight.nvstream.NvConnection;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Timer;
|
|
||||||
import java.util.TimerTask;
|
|
||||||
|
|
||||||
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 ControllerHandler controllerHandler;
|
|
||||||
private Context context = null;
|
|
||||||
|
|
||||||
private FrameLayout frame_layout = null;
|
|
||||||
private RelativeLayout relative_layout = null;
|
|
||||||
|
|
||||||
private Timer retransmitTimer;
|
|
||||||
|
|
||||||
ControllerMode currentMode = ControllerMode.Active;
|
|
||||||
ControllerInputContext inputContext = new ControllerInputContext();
|
|
||||||
|
|
||||||
private Button buttonConfigure = null;
|
|
||||||
|
|
||||||
private List<VirtualControllerElement> elements = new ArrayList<>();
|
|
||||||
|
|
||||||
public VirtualController(final ControllerHandler controllerHandler, FrameLayout layout, final Context context) {
|
|
||||||
this.controllerHandler = controllerHandler;
|
|
||||||
this.frame_layout = layout;
|
|
||||||
this.context = context;
|
|
||||||
|
|
||||||
relative_layout = new RelativeLayout(context);
|
|
||||||
|
|
||||||
frame_layout.addView(relative_layout);
|
|
||||||
|
|
||||||
buttonConfigure = new Button(context);
|
|
||||||
buttonConfigure.setAlpha(0.25f);
|
|
||||||
buttonConfigure.setFocusable(false);
|
|
||||||
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;
|
|
||||||
VirtualControllerConfigurationLoader.saveProfile(VirtualController.this, context);
|
|
||||||
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 hide() {
|
|
||||||
retransmitTimer.cancel();
|
|
||||||
relative_layout.setVisibility(View.INVISIBLE);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void show() {
|
|
||||||
relative_layout.setVisibility(View.VISIBLE);
|
|
||||||
|
|
||||||
// HACK: GFE sometimes discards gamepad packets when they are received
|
|
||||||
// very shortly after another. This can be critical if an axis zeroing packet
|
|
||||||
// is lost and causes an analog stick to get stuck. To avoid this, we send
|
|
||||||
// a gamepad input packet every 100 ms to ensure any loss can be recovered.
|
|
||||||
retransmitTimer = new Timer("OSC timer", true);
|
|
||||||
retransmitTimer.schedule(new TimerTask() {
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
sendControllerInputContext();
|
|
||||||
}
|
|
||||||
}, 100, 100);
|
|
||||||
}
|
|
||||||
|
|
||||||
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.06f);
|
|
||||||
RelativeLayout.LayoutParams params = new RelativeLayout.LayoutParams(buttonSize, buttonSize);
|
|
||||||
params.addRule(RelativeLayout.ALIGN_PARENT_LEFT, RelativeLayout.TRUE);
|
|
||||||
params.addRule(RelativeLayout.ALIGN_PARENT_TOP, RelativeLayout.TRUE);
|
|
||||||
params.leftMargin = 15;
|
|
||||||
params.topMargin = 15;
|
|
||||||
relative_layout.addView(buttonConfigure, params);
|
|
||||||
|
|
||||||
// Start with the default layout
|
|
||||||
VirtualControllerConfigurationLoader.createDefaultLayout(this, context);
|
|
||||||
|
|
||||||
// Apply user preferences onto the default layout
|
|
||||||
VirtualControllerConfigurationLoader.loadFromPreferences(this, context);
|
|
||||||
}
|
|
||||||
|
|
||||||
public ControllerMode getControllerMode() {
|
|
||||||
return currentMode;
|
|
||||||
}
|
|
||||||
|
|
||||||
public ControllerInputContext getControllerInputContext() {
|
|
||||||
return inputContext;
|
|
||||||
}
|
|
||||||
|
|
||||||
void sendControllerInputContext() {
|
|
||||||
_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);
|
|
||||||
|
|
||||||
if (controllerHandler != null) {
|
|
||||||
controllerHandler.reportOscState(
|
|
||||||
inputContext.inputMap,
|
|
||||||
inputContext.leftStickX,
|
|
||||||
inputContext.leftStickY,
|
|
||||||
inputContext.rightStickX,
|
|
||||||
inputContext.rightStickY,
|
|
||||||
inputContext.leftTrigger,
|
|
||||||
inputContext.rightTrigger
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-328
@@ -1,328 +0,0 @@
|
|||||||
/**
|
|
||||||
* Created by Karim Mreisi.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package com.limelight.binding.input.virtual_controller;
|
|
||||||
|
|
||||||
import android.app.Activity;
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.SharedPreferences;
|
|
||||||
import android.util.DisplayMetrics;
|
|
||||||
|
|
||||||
import com.limelight.nvstream.input.ControllerPacket;
|
|
||||||
import com.limelight.preferences.PreferenceConfiguration;
|
|
||||||
|
|
||||||
import org.json.JSONException;
|
|
||||||
import org.json.JSONObject;
|
|
||||||
|
|
||||||
public class VirtualControllerConfigurationLoader {
|
|
||||||
public static final String OSC_PREFERENCE = "OSC";
|
|
||||||
|
|
||||||
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 elementId,
|
|
||||||
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, elementId, 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();
|
|
||||||
PreferenceConfiguration config = PreferenceConfiguration.readPreferences(context);
|
|
||||||
|
|
||||||
// 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.
|
|
||||||
|
|
||||||
if (!config.onlyL3R3)
|
|
||||||
{
|
|
||||||
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(
|
|
||||||
VirtualControllerElement.EID_A,
|
|
||||||
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(
|
|
||||||
VirtualControllerElement.EID_B,
|
|
||||||
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(
|
|
||||||
VirtualControllerElement.EID_X,
|
|
||||||
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(
|
|
||||||
VirtualControllerElement.EID_Y,
|
|
||||||
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(
|
|
||||||
VirtualControllerElement.EID_LB,
|
|
||||||
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(
|
|
||||||
VirtualControllerElement.EID_RB,
|
|
||||||
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(
|
|
||||||
VirtualControllerElement.EID_BACK,
|
|
||||||
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(
|
|
||||||
VirtualControllerElement.EID_START,
|
|
||||||
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)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
controller.addElement(createDigitalButton(
|
|
||||||
VirtualControllerElement.EID_LSB,
|
|
||||||
ControllerPacket.LS_CLK_FLAG, 0, 1, "L3", -1, controller, context),
|
|
||||||
getPercent(2, screen.widthPixels),
|
|
||||||
getPercent(80, screen.heightPixels),
|
|
||||||
getPercent(BUTTON_WIDTH, screen.widthPixels),
|
|
||||||
getPercent(BUTTON_HEIGHT, screen.heightPixels)
|
|
||||||
);
|
|
||||||
|
|
||||||
controller.addElement(createDigitalButton(
|
|
||||||
VirtualControllerElement.EID_RSB,
|
|
||||||
ControllerPacket.RS_CLK_FLAG, 0, 1, "R3", -1, controller, context),
|
|
||||||
getPercent(89, screen.widthPixels),
|
|
||||||
getPercent(80, screen.heightPixels),
|
|
||||||
getPercent(BUTTON_WIDTH, screen.widthPixels),
|
|
||||||
getPercent(BUTTON_HEIGHT, screen.heightPixels)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void saveProfile(final VirtualController controller,
|
|
||||||
final Context context) {
|
|
||||||
SharedPreferences.Editor prefEditor = context.getSharedPreferences(OSC_PREFERENCE, Activity.MODE_PRIVATE).edit();
|
|
||||||
|
|
||||||
for (VirtualControllerElement element : controller.getElements()) {
|
|
||||||
String prefKey = ""+element.elementId;
|
|
||||||
try {
|
|
||||||
prefEditor.putString(prefKey, element.getConfiguration().toString());
|
|
||||||
} catch (JSONException e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
prefEditor.apply();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void loadFromPreferences(final VirtualController controller, final Context context) {
|
|
||||||
SharedPreferences pref = context.getSharedPreferences(OSC_PREFERENCE, Activity.MODE_PRIVATE);
|
|
||||||
|
|
||||||
for (VirtualControllerElement element : controller.getElements()) {
|
|
||||||
String prefKey = ""+element.elementId;
|
|
||||||
|
|
||||||
String jsonConfig = pref.getString(prefKey, null);
|
|
||||||
if (jsonConfig != null) {
|
|
||||||
try {
|
|
||||||
element.loadConfiguration(new JSONObject(jsonConfig));
|
|
||||||
} catch (JSONException e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
|
|
||||||
// Remove the corrupt element from the preferences
|
|
||||||
pref.edit().remove(prefKey).apply();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-322
@@ -1,322 +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;
|
|
||||||
|
|
||||||
import org.json.JSONException;
|
|
||||||
import org.json.JSONObject;
|
|
||||||
|
|
||||||
public abstract class VirtualControllerElement extends View {
|
|
||||||
protected static boolean _PRINT_DEBUG_INFORMATION = false;
|
|
||||||
|
|
||||||
public static final int EID_DPAD = 1;
|
|
||||||
public static final int EID_LT = 2;
|
|
||||||
public static final int EID_RT = 3;
|
|
||||||
public static final int EID_LB = 4;
|
|
||||||
public static final int EID_RB = 5;
|
|
||||||
public static final int EID_A = 6;
|
|
||||||
public static final int EID_B = 7;
|
|
||||||
public static final int EID_X = 8;
|
|
||||||
public static final int EID_Y = 9;
|
|
||||||
public static final int EID_BACK = 10;
|
|
||||||
public static final int EID_START = 11;
|
|
||||||
public static final int EID_LS = 12;
|
|
||||||
public static final int EID_RS = 13;
|
|
||||||
public static final int EID_LSB = 14;
|
|
||||||
public static final int EID_RSB = 15;
|
|
||||||
|
|
||||||
protected VirtualController virtualController;
|
|
||||||
protected final int elementId;
|
|
||||||
|
|
||||||
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, int elementId) {
|
|
||||||
super(context);
|
|
||||||
|
|
||||||
this.virtualController = controller;
|
|
||||||
this.elementId = elementId;
|
|
||||||
}
|
|
||||||
|
|
||||||
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() {
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
|
|
||||||
@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() throws JSONException {
|
|
||||||
JSONObject configuration = new JSONObject();
|
|
||||||
|
|
||||||
RelativeLayout.LayoutParams layoutParams = (RelativeLayout.LayoutParams) getLayoutParams();
|
|
||||||
|
|
||||||
configuration.put("LEFT", layoutParams.leftMargin);
|
|
||||||
configuration.put("TOP", layoutParams.topMargin);
|
|
||||||
configuration.put("WIDTH", layoutParams.width);
|
|
||||||
configuration.put("HEIGHT", layoutParams.height);
|
|
||||||
|
|
||||||
return configuration;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void loadConfiguration(JSONObject configuration) throws JSONException {
|
|
||||||
RelativeLayout.LayoutParams layoutParams = (RelativeLayout.LayoutParams) getLayoutParams();
|
|
||||||
|
|
||||||
layoutParams.leftMargin = configuration.getInt("LEFT");
|
|
||||||
layoutParams.topMargin = configuration.getInt("TOP");
|
|
||||||
layoutParams.width = configuration.getInt("WIDTH");
|
|
||||||
layoutParams.height = configuration.getInt("HEIGHT");
|
|
||||||
|
|
||||||
requestLayout();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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
@@ -7,13 +7,9 @@ import java.util.Collections;
|
|||||||
import java.util.LinkedList;
|
import java.util.LinkedList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
import java.util.regex.Matcher;
|
|
||||||
import java.util.regex.Pattern;
|
|
||||||
|
|
||||||
import android.annotation.SuppressLint;
|
import android.annotation.SuppressLint;
|
||||||
import android.app.ActivityManager;
|
import android.annotation.TargetApi;
|
||||||
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;
|
||||||
@@ -24,267 +20,43 @@ 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;
|
public static final List<String> blacklistedDecoderPrefixes;
|
||||||
private static final List<String> spsFixupBitstreamFixupDecoderPrefixes;
|
public static final List<String> spsFixupBitstreamFixupDecoderPrefixes;
|
||||||
private static final List<String> blacklistedAdaptivePlaybackPrefixes;
|
public static final List<String> whitelistedAdaptiveResolutionPrefixes;
|
||||||
private static final List<String> deprioritizedHevcDecoders;
|
public static final List<String> baselineProfileHackPrefixes;
|
||||||
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;
|
|
||||||
private static final List<String> blacklisted49FpsDecoderPrefixes;
|
|
||||||
|
|
||||||
private static boolean isLowEndSnapdragon = false;
|
static {
|
||||||
private static boolean initialized = false;
|
preferredDecoders = new LinkedList<String>();
|
||||||
|
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static {
|
static {
|
||||||
preferredDecoders = new LinkedList<>();
|
blacklistedDecoderPrefixes = new LinkedList<String>();
|
||||||
|
|
||||||
|
// Software decoders that don't support H264 high profile
|
||||||
|
blacklistedDecoderPrefixes.add("omx.google");
|
||||||
|
blacklistedDecoderPrefixes.add("AVCDecoder");
|
||||||
}
|
}
|
||||||
|
|
||||||
static {
|
static {
|
||||||
blacklistedDecoderPrefixes = new LinkedList<>();
|
spsFixupBitstreamFixupDecoderPrefixes = new LinkedList<String>();
|
||||||
|
|
||||||
// Blacklist software decoders that don't support H264 high profile,
|
|
||||||
// but exclude the official AOSP and CrOS emulator from this restriction.
|
|
||||||
if (!Build.HARDWARE.equals("ranchu") && !Build.HARDWARE.equals("cheets")) {
|
|
||||||
blacklistedDecoderPrefixes.add("omx.google");
|
|
||||||
blacklistedDecoderPrefixes.add("AVCDecoder");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Never use ffmpeg decoders since they're software decoders
|
|
||||||
blacklistedDecoderPrefixes.add("OMX.ffmpeg");
|
|
||||||
|
|
||||||
// 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 {
|
|
||||||
// If a decoder qualifies for reference frame invalidation,
|
|
||||||
// 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");
|
||||||
|
|
||||||
blacklistedAdaptivePlaybackPrefixes = new LinkedList<>();
|
whitelistedAdaptiveResolutionPrefixes = new LinkedList<String>();
|
||||||
// The Intel decoder on Lollipop on Nexus Player would increase latency badly
|
whitelistedAdaptiveResolutionPrefixes.add("omx.nvidia");
|
||||||
// if adaptive playback was enabled so let's avoid it to be safe.
|
whitelistedAdaptiveResolutionPrefixes.add("omx.qcom");
|
||||||
blacklistedAdaptivePlaybackPrefixes.add("omx.intel");
|
whitelistedAdaptiveResolutionPrefixes.add("omx.sec");
|
||||||
// The MediaTek decoder crashes at 1080p when adaptive playback is enabled
|
whitelistedAdaptiveResolutionPrefixes.add("omx.TI");
|
||||||
// on some Android TV devices with H.265 only.
|
|
||||||
blacklistedAdaptivePlaybackPrefixes.add("omx.mtk");
|
|
||||||
|
|
||||||
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");
|
|
||||||
|
|
||||||
// On Darcy (Shield 2017), HEVC runs fine with no fixups required.
|
|
||||||
// For some reason, other X1 implementations require bitstream fixups.
|
|
||||||
if (Build.DEVICE.equalsIgnoreCase("darcy")) {
|
|
||||||
whitelistedHevcDecoders.add("omx.nvidia");
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
// TODO: This needs a similar fixup to the Tegra 3 otherwise it buffers 16 frames
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sony ATVs have broken MediaTek codecs (decoder hangs after rendering the first frame).
|
|
||||||
// I know the Fire TV 2 and 3 works, so I'll just whitelist Amazon devices which seem
|
|
||||||
// to actually be tested.
|
|
||||||
if (Build.MANUFACTURER.equalsIgnoreCase("Amazon")) {
|
|
||||||
whitelistedHevcDecoders.add("omx.mtk");
|
|
||||||
whitelistedHevcDecoders.add("omx.amlogic");
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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.
|
|
||||||
}
|
|
||||||
|
|
||||||
static {
|
|
||||||
deprioritizedHevcDecoders = new LinkedList<>();
|
|
||||||
|
|
||||||
// These are decoders that work but aren't used by default for various reasons.
|
|
||||||
|
|
||||||
// Qualcomm is currently the only decoders in this group.
|
|
||||||
}
|
|
||||||
|
|
||||||
static {
|
|
||||||
blacklisted49FpsDecoderPrefixes = new LinkedList<>();
|
|
||||||
|
|
||||||
// We see a bunch of crashes on MediaTek Android TVs running
|
|
||||||
// at 49 FPS (PAL 50 Hz - 1). Blacklist this frame rate for
|
|
||||||
// these devices and hope they fix it in Pie.
|
|
||||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) {
|
|
||||||
blacklisted49FpsDecoderPrefixes.add("omx.mtk");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static boolean isPowerVR(String glRenderer) {
|
|
||||||
return glRenderer.toLowerCase().contains("powervr");
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String getAdrenoVersionString(String glRenderer) {
|
|
||||||
glRenderer = glRenderer.toLowerCase().trim();
|
|
||||||
|
|
||||||
if (!glRenderer.contains("adreno")) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
Pattern modelNumberPattern = Pattern.compile("(.*)([0-9]{3})(.*)");
|
|
||||||
|
|
||||||
Matcher matcher = modelNumberPattern.matcher(glRenderer);
|
|
||||||
if (!matcher.matches()) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
String modelNumber = matcher.group(2);
|
|
||||||
LimeLog.info("Found Adreno GPU: "+modelNumber);
|
|
||||||
return modelNumber;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static boolean isLowEndSnapdragonRenderer(String glRenderer) {
|
|
||||||
String modelNumber = getAdrenoVersionString(glRenderer);
|
|
||||||
if (modelNumber == null) {
|
|
||||||
// Not an Adreno GPU
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// The current logic is to identify low-end SoCs based on a zero in the x0x place.
|
|
||||||
return modelNumber.charAt(1) == '0';
|
|
||||||
}
|
|
||||||
|
|
||||||
// This is a workaround for some broken devices that report
|
|
||||||
// only GLES 3.0 even though the GPU is an Adreno 4xx series part.
|
|
||||||
// An example of such a device is the Huawei Honor 5x with the
|
|
||||||
// Snapdragon 616 SoC (Adreno 405).
|
|
||||||
private static boolean isGLES31SnapdragonRenderer(String glRenderer) {
|
|
||||||
String modelNumber = getAdrenoVersionString(glRenderer);
|
|
||||||
if (modelNumber == null) {
|
|
||||||
// Not an Adreno GPU
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Snapdragon 4xx and higher support GLES 3.1
|
|
||||||
return modelNumber.charAt(0) >= '4';
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void initialize(Context context, String glRenderer) {
|
|
||||||
if (initialized) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
|
||||||
|
|
||||||
isLowEndSnapdragon = isLowEndSnapdragonRenderer(glRenderer);
|
|
||||||
|
|
||||||
// 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
|
|
||||||
//
|
|
||||||
// The "good" GPUs support GLES 3.1, but we can't just check that directly
|
|
||||||
// (see comment on isGLES31SnapdragonRenderer).
|
|
||||||
//
|
|
||||||
if (isGLES31SnapdragonRenderer(glRenderer)) {
|
|
||||||
// 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. If HDR or mobile data will be used, we'll override this and use
|
|
||||||
// HEVC anyway.
|
|
||||||
LimeLog.info("Added omx.qcom to deprioritized HEVC decoders based on GLES 3.1+ support");
|
|
||||||
deprioritizedHevcDecoders.add("omx.qcom");
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
blacklistedDecoderPrefixes.add("OMX.qcom.video.decoder.hevc");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Older MediaTek SoCs have issues with HEVC rendering but the newer chips with
|
|
||||||
// PowerVR GPUs have good HEVC support.
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && isPowerVR(glRenderer)) {
|
|
||||||
LimeLog.info("Added omx.mtk to HEVC decoders based on PowerVR GPU");
|
|
||||||
whitelistedHevcDecoders.add("omx.mtk");
|
|
||||||
|
|
||||||
// This SoC (MT8176 in GPD XD+) supports AVC RFI too, but the maxNumReferenceFrames setting
|
|
||||||
// required to make it work adds a huge amount of latency.
|
|
||||||
LimeLog.info("Added omx.mtk to RFI list for HEVC");
|
|
||||||
refFrameInvalidationHevcPrefixes.add("omx.mtk");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
initialized = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static boolean isDecoderInList(List<String> decoderList, String decoderName) {
|
private static boolean isDecoderInList(List<String> decoderList, String decoderName) {
|
||||||
if (!initialized) {
|
|
||||||
throw new IllegalStateException("MediaCodecHelper must be initialized before use");
|
|
||||||
}
|
|
||||||
|
|
||||||
for (String badPrefix : decoderList) {
|
for (String badPrefix : decoderList) {
|
||||||
if (decoderName.length() >= badPrefix.length()) {
|
if (decoderName.length() >= badPrefix.length()) {
|
||||||
String prefix = decoderName.substring(0, badPrefix.length());
|
String prefix = decoderName.substring(0, badPrefix.length());
|
||||||
@@ -297,18 +69,19 @@ public class MediaCodecHelper {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static long getMonotonicMillis() {
|
@TargetApi(Build.VERSION_CODES.KITKAT)
|
||||||
return System.nanoTime() / 1000000L;
|
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
|
||||||
|
so we'll keep it off for now, since we don't know whether other devices also do the same
|
||||||
|
|
||||||
|
if (isDecoderInList(whitelistedAdaptiveResolutionPrefixes, decoderName)) {
|
||||||
|
LimeLog.info("Adaptive playback supported (whitelist)");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
public static boolean decoderSupportsAdaptivePlayback(MediaCodecInfo decoderInfo) {
|
|
||||||
// Possibly enable adaptive playback on KitKat and above
|
// Possibly enable adaptive playback on KitKat and above
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
|
||||||
if (isDecoderInList(blacklistedAdaptivePlaybackPrefixes, decoderInfo.getName())) {
|
|
||||||
LimeLog.info("Decoder blacklisted for adaptive playback");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (decoderInfo.getCapabilitiesForType("video/avc").
|
if (decoderInfo.getCapabilitiesForType("video/avc").
|
||||||
isFeatureSupported(CodecCapabilities.FEATURE_AdaptivePlayback))
|
isFeatureSupported(CodecCapabilities.FEATURE_AdaptivePlayback))
|
||||||
@@ -320,98 +93,23 @@ public class MediaCodecHelper {
|
|||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
// Tolerate buggy codecs
|
// Tolerate buggy codecs
|
||||||
}
|
}
|
||||||
}
|
}*/
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static boolean decoderNeedsConstrainedHighProfile(String decoderName) {
|
public static boolean decoderNeedsSpsBitstreamRestrictions(String decoderName, MediaCodecInfo decoderInfo) {
|
||||||
return isDecoderInList(constrainedHighProfilePrefixes, decoderName);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static boolean decoderCanDirectSubmit(String decoderName) {
|
|
||||||
return isDecoderInList(directSubmitPrefixes, decoderName) && !isExynos4Device();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static boolean decoderNeedsSpsBitstreamRestrictions(String decoderName) {
|
|
||||||
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 decoderBlacklistedFor49Fps(String decoderName) {
|
|
||||||
return isDecoderInList(blacklisted49FpsDecoderPrefixes, decoderName);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static boolean decoderSupportsRefFrameInvalidationAvc(String decoderName, int videoHeight) {
|
|
||||||
// Reference frame invalidation is broken on low-end Snapdragon SoCs at 1080p.
|
|
||||||
if (videoHeight > 720 && isLowEndSnapdragon) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// This device seems to crash constantly at 720p, so try disabling
|
|
||||||
// RFI to see if we can get that under control.
|
|
||||||
if (Build.DEVICE.equals("b3") || Build.DEVICE.equals("b5")) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return isDecoderInList(refFrameInvalidationAvcPrefixes, decoderName);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static boolean decoderSupportsRefFrameInvalidationHevc(String decoderName) {
|
|
||||||
return isDecoderInList(refFrameInvalidationHevcPrefixes, decoderName);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static boolean decoderIsWhitelistedForHevc(String decoderName, boolean meteredData) {
|
|
||||||
// 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Some devices have HEVC decoders that we prefer not to use
|
|
||||||
// typically because it can't support reference frame invalidation.
|
|
||||||
// However, we will use it for HDR and for streaming over mobile networks
|
|
||||||
// since it works fine otherwise.
|
|
||||||
if (meteredData && isDecoderInList(deprioritizedHevcDecoders, decoderName)) {
|
|
||||||
LimeLog.info("Selected deprioritized decoder");
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
||||||
@@ -448,15 +146,11 @@ 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
|
||||||
|
|
||||||
if (!initialized) {
|
|
||||||
throw new IllegalStateException("MediaCodecHelper must be initialized before use");
|
|
||||||
}
|
|
||||||
|
|
||||||
for (String preferredDecoder : preferredDecoders) {
|
for (String preferredDecoder : preferredDecoders) {
|
||||||
for (MediaCodecInfo codecInfo : getMediaCodecList()) {
|
for (MediaCodecInfo codecInfo : getMediaCodecList()) {
|
||||||
// Skip encoders
|
// Skip encoders
|
||||||
@@ -475,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()) {
|
||||||
@@ -488,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;
|
||||||
}
|
}
|
||||||
@@ -500,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) {
|
||||||
@@ -510,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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -523,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()) {
|
||||||
@@ -536,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");
|
LimeLog.info("Decoder "+codecInfo.getName()+" does NOT support high profile");
|
||||||
}
|
|
||||||
else {
|
|
||||||
return codecInfo;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,13 @@
|
|||||||
package com.limelight.computers;
|
package com.limelight.computers;
|
||||||
|
|
||||||
import java.io.ByteArrayInputStream;
|
import java.net.InetAddress;
|
||||||
import java.security.cert.CertificateEncodingException;
|
import java.net.UnknownHostException;
|
||||||
import java.security.cert.CertificateException;
|
|
||||||
import java.security.cert.CertificateFactory;
|
|
||||||
import java.security.cert.X509Certificate;
|
|
||||||
import java.util.LinkedList;
|
import java.util.LinkedList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
import com.limelight.LimeLog;
|
||||||
import com.limelight.nvstream.http.ComputerDetails;
|
import com.limelight.nvstream.http.ComputerDetails;
|
||||||
|
|
||||||
import android.content.ContentValues;
|
import android.content.ContentValues;
|
||||||
@@ -18,147 +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 = "computers2.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_UUID_COLUMN_NAME = "UUID";
|
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 LOCAL_ADDRESS_COLUMN_NAME = "LocalAddress";
|
private static final String LOCAL_IP_COLUMN_NAME = "LocalIp";
|
||||||
private static final String REMOTE_ADDRESS_COLUMN_NAME = "RemoteAddress";
|
private static final String REMOTE_IP_COLUMN_NAME = "RemoteIp";
|
||||||
private static final String MANUAL_ADDRESS_COLUMN_NAME = "ManualAddress";
|
private static final String MAC_COLUMN_NAME = "Mac";
|
||||||
private static final String MAC_ADDRESS_COLUMN_NAME = "MacAddress";
|
|
||||||
private static final String SERVER_CERT_COLUMN_NAME = "ServerCert";
|
|
||||||
|
|
||||||
private SQLiteDatabase computerDb;
|
private SQLiteDatabase computerDb;
|
||||||
|
|
||||||
public ComputerDatabaseManager(Context c) {
|
public ComputerDatabaseManager(Context c) {
|
||||||
try {
|
try {
|
||||||
// Create or open an existing DB
|
// Create or open an existing DB
|
||||||
computerDb = c.openOrCreateDatabase(COMPUTER_DB_NAME, 0, null);
|
computerDb = c.openOrCreateDatabase(COMPUTER_DB_NAME, 0, null);
|
||||||
} catch (SQLiteException e) {
|
} catch (SQLiteException e) {
|
||||||
// Delete the DB and try again
|
// Delete the DB and try again
|
||||||
c.deleteDatabase(COMPUTER_DB_NAME);
|
c.deleteDatabase(COMPUTER_DB_NAME);
|
||||||
computerDb = c.openOrCreateDatabase(COMPUTER_DB_NAME, 0, null);
|
computerDb = c.openOrCreateDatabase(COMPUTER_DB_NAME, 0, null);
|
||||||
}
|
}
|
||||||
initializeDb(c);
|
initializeDb();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void close() {
|
public void close() {
|
||||||
computerDb.close();
|
computerDb.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void initializeDb(Context c) {
|
private void initializeDb() {
|
||||||
// Add cert column to the table if not present
|
// Create tables if they aren't already there
|
||||||
try {
|
computerDb.execSQL(String.format((Locale)null, "CREATE TABLE IF NOT EXISTS %s(%s TEXT PRIMARY KEY," +
|
||||||
computerDb.execSQL(String.format((Locale)null,
|
" %s TEXT NOT NULL, %s TEXT NOT NULL, %s TEXT NOT NULL, %s TEXT NOT NULL)",
|
||||||
"ALTER TABLE %s ADD COLUMN %s TEXT",
|
COMPUTER_TABLE_NAME,
|
||||||
COMPUTER_TABLE_NAME, SERVER_CERT_COLUMN_NAME));
|
COMPUTER_NAME_COLUMN_NAME, COMPUTER_UUID_COLUMN_NAME, LOCAL_IP_COLUMN_NAME,
|
||||||
} catch (SQLiteException e) {}
|
REMOTE_IP_COLUMN_NAME, MAC_COLUMN_NAME));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void deleteComputer(String name) {
|
||||||
|
computerDb.delete(COMPUTER_TABLE_NAME, COMPUTER_NAME_COLUMN_NAME+"='"+name+"'", null);
|
||||||
|
}
|
||||||
|
|
||||||
// Create tables if they aren't already there
|
public boolean updateComputer(ComputerDetails details) {
|
||||||
computerDb.execSQL(String.format((Locale)null,
|
ContentValues values = new ContentValues();
|
||||||
"CREATE TABLE IF NOT EXISTS %s(%s TEXT PRIMARY KEY, %s TEXT NOT NULL, %s TEXT, %s TEXT, %s TEXT, %s TEXT, %s TEXT)",
|
values.put(COMPUTER_NAME_COLUMN_NAME, details.name);
|
||||||
COMPUTER_TABLE_NAME,
|
values.put(COMPUTER_UUID_COLUMN_NAME, details.uuid.toString());
|
||||||
COMPUTER_UUID_COLUMN_NAME, COMPUTER_NAME_COLUMN_NAME,
|
values.put(LOCAL_IP_COLUMN_NAME, details.localIp.getAddress());
|
||||||
LOCAL_ADDRESS_COLUMN_NAME, REMOTE_ADDRESS_COLUMN_NAME, MANUAL_ADDRESS_COLUMN_NAME,
|
values.put(REMOTE_IP_COLUMN_NAME, details.remoteIp.getAddress());
|
||||||
MAC_ADDRESS_COLUMN_NAME, SERVER_CERT_COLUMN_NAME));
|
values.put(MAC_COLUMN_NAME, details.macAddress);
|
||||||
|
return -1 != computerDb.insertWithOnConflict(COMPUTER_TABLE_NAME, null, values, SQLiteDatabase.CONFLICT_REPLACE);
|
||||||
|
}
|
||||||
|
|
||||||
// Move all computers from the old DB (if any) to the new one
|
public List<ComputerDetails> getAllComputers() {
|
||||||
List<ComputerDetails> oldComputers = LegacyDatabaseReader.migrateAllComputers(c);
|
Cursor c = computerDb.rawQuery("SELECT * FROM "+COMPUTER_TABLE_NAME, null);
|
||||||
for (ComputerDetails computer : oldComputers) {
|
LinkedList<ComputerDetails> computerList = new LinkedList<ComputerDetails>();
|
||||||
updateComputer(computer);
|
while (c.moveToNext()) {
|
||||||
}
|
ComputerDetails details = new ComputerDetails();
|
||||||
}
|
|
||||||
|
|
||||||
public void deleteComputer(String name) {
|
details.name = c.getString(0);
|
||||||
computerDb.delete(COMPUTER_TABLE_NAME, COMPUTER_NAME_COLUMN_NAME+"=?", new String[]{name});
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean updateComputer(ComputerDetails details) {
|
String uuidStr = c.getString(1);
|
||||||
ContentValues values = new ContentValues();
|
try {
|
||||||
values.put(COMPUTER_UUID_COLUMN_NAME, details.uuid);
|
details.uuid = UUID.fromString(uuidStr);
|
||||||
values.put(COMPUTER_NAME_COLUMN_NAME, details.name);
|
} catch (IllegalArgumentException e) {
|
||||||
values.put(LOCAL_ADDRESS_COLUMN_NAME, details.localAddress);
|
// We'll delete this entry
|
||||||
values.put(REMOTE_ADDRESS_COLUMN_NAME, details.remoteAddress);
|
LimeLog.severe("DB: Corrupted UUID for "+details.name);
|
||||||
values.put(MANUAL_ADDRESS_COLUMN_NAME, details.manualAddress);
|
}
|
||||||
values.put(MAC_ADDRESS_COLUMN_NAME, details.macAddress);
|
|
||||||
try {
|
|
||||||
if (details.serverCert != null) {
|
|
||||||
values.put(SERVER_CERT_COLUMN_NAME, details.serverCert.getEncoded());
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
values.put(SERVER_CERT_COLUMN_NAME, (byte[])null);
|
|
||||||
}
|
|
||||||
} catch (CertificateEncodingException e) {
|
|
||||||
values.put(SERVER_CERT_COLUMN_NAME, (byte[])null);
|
|
||||||
e.printStackTrace();
|
|
||||||
}
|
|
||||||
return -1 != computerDb.insertWithOnConflict(COMPUTER_TABLE_NAME, null, values, SQLiteDatabase.CONFLICT_REPLACE);
|
|
||||||
}
|
|
||||||
|
|
||||||
private ComputerDetails getComputerFromCursor(Cursor c) {
|
try {
|
||||||
ComputerDetails details = new ComputerDetails();
|
details.localIp = InetAddress.getByAddress(c.getBlob(2));
|
||||||
|
} catch (UnknownHostException e) {
|
||||||
|
// We'll delete this entry
|
||||||
|
LimeLog.severe("DB: Corrupted local IP for "+details.name);
|
||||||
|
}
|
||||||
|
|
||||||
details.uuid = c.getString(0);
|
try {
|
||||||
details.name = c.getString(1);
|
details.remoteIp = InetAddress.getByAddress(c.getBlob(3));
|
||||||
details.localAddress = c.getString(2);
|
} catch (UnknownHostException e) {
|
||||||
details.remoteAddress = c.getString(3);
|
// We'll delete this entry
|
||||||
details.manualAddress = c.getString(4);
|
LimeLog.severe("DB: Corrupted remote IP for "+details.name);
|
||||||
details.macAddress = c.getString(5);
|
}
|
||||||
|
|
||||||
try {
|
details.macAddress = c.getString(4);
|
||||||
byte[] derCertData = c.getBlob(6);
|
|
||||||
|
|
||||||
if (derCertData != null) {
|
// This signifies we don't have dynamic state (like pair state)
|
||||||
details.serverCert = (X509Certificate) CertificateFactory.getInstance("X.509")
|
details.state = ComputerDetails.State.UNKNOWN;
|
||||||
.generateCertificate(new ByteArrayInputStream(derCertData));
|
details.reachability = ComputerDetails.Reachability.UNKNOWN;
|
||||||
}
|
|
||||||
} catch (CertificateException e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
}
|
|
||||||
|
|
||||||
// This signifies we don't have dynamic state (like pair state)
|
// If a field is corrupt or missing, skip the database entry
|
||||||
details.state = ComputerDetails.State.UNKNOWN;
|
if (details.uuid == null || details.localIp == null || details.remoteIp == null ||
|
||||||
|
details.macAddress == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
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 critical field is corrupt or missing, skip the database entry
|
|
||||||
if (details.uuid == null) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
computerList.add(details);
|
computerList.add(details);
|
||||||
}
|
}
|
||||||
|
|
||||||
c.close();
|
c.close();
|
||||||
|
|
||||||
return computerList;
|
return computerList;
|
||||||
}
|
}
|
||||||
|
|
||||||
public ComputerDetails getComputerByUUID(String uuid) {
|
public ComputerDetails getComputerByName(String name) {
|
||||||
Cursor c = computerDb.query(COMPUTER_TABLE_NAME, null, COMPUTER_UUID_COLUMN_NAME+"=?", new String[]{ uuid }, null, null, null);
|
Cursor c = computerDb.rawQuery("SELECT * FROM "+COMPUTER_TABLE_NAME+" WHERE "+COMPUTER_NAME_COLUMN_NAME+"='"+name+"'", null);
|
||||||
if (!c.moveToFirst()) {
|
ComputerDetails details = new ComputerDetails();
|
||||||
// No matching computer
|
if (!c.moveToFirst()) {
|
||||||
c.close();
|
// No matching computer
|
||||||
return null;
|
c.close();
|
||||||
}
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
ComputerDetails details = getComputerFromCursor(c);
|
details.name = c.getString(0);
|
||||||
c.close();
|
|
||||||
|
|
||||||
// If a critical field is corrupt or missing, delete the database entry
|
String uuidStr = c.getString(1);
|
||||||
if (details.uuid == null) {
|
try {
|
||||||
deleteComputer(details.name);
|
details.uuid = UUID.fromString(uuidStr);
|
||||||
return null;
|
} catch (IllegalArgumentException e) {
|
||||||
}
|
// We'll delete this entry
|
||||||
|
LimeLog.severe("DB: Corrupted UUID for "+details.name);
|
||||||
|
}
|
||||||
|
|
||||||
return details;
|
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();
|
||||||
|
|
||||||
|
details.state = ComputerDetails.State.UNKNOWN;
|
||||||
|
details.reachability = ComputerDetails.Reachability.UNKNOWN;
|
||||||
|
|
||||||
|
// If a field is corrupt or missing, delete the database entry
|
||||||
|
if (details.uuid == null || details.localIp == null || details.remoteIp == 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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,105 +0,0 @@
|
|||||||
package com.limelight.computers;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.database.Cursor;
|
|
||||||
import android.database.sqlite.SQLiteDatabase;
|
|
||||||
import android.database.sqlite.SQLiteException;
|
|
||||||
|
|
||||||
import com.limelight.LimeLog;
|
|
||||||
import com.limelight.nvstream.http.ComputerDetails;
|
|
||||||
|
|
||||||
import java.net.InetAddress;
|
|
||||||
import java.net.UnknownHostException;
|
|
||||||
import java.util.LinkedList;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
public class LegacyDatabaseReader {
|
|
||||||
private static final String COMPUTER_DB_NAME = "computers.db";
|
|
||||||
private static final String COMPUTER_TABLE_NAME = "Computers";
|
|
||||||
|
|
||||||
private static final String ADDRESS_PREFIX = "ADDRESS_PREFIX__";
|
|
||||||
|
|
||||||
private static ComputerDetails getComputerFromCursor(Cursor c) {
|
|
||||||
ComputerDetails details = new ComputerDetails();
|
|
||||||
|
|
||||||
details.name = c.getString(0);
|
|
||||||
details.uuid = c.getString(1);
|
|
||||||
|
|
||||||
// An earlier schema defined addresses as byte blobs. We'll
|
|
||||||
// gracefully migrate those to strings so we can store DNS names
|
|
||||||
// too. To disambiguate, we'll need to prefix them with a string
|
|
||||||
// greater than the allowable IP address length.
|
|
||||||
try {
|
|
||||||
details.localAddress = InetAddress.getByAddress(c.getBlob(2)).getHostAddress();
|
|
||||||
LimeLog.warning("DB: Legacy local address for " + details.name);
|
|
||||||
} catch (UnknownHostException e) {
|
|
||||||
// This is probably a hostname/address with the prefix string
|
|
||||||
String stringData = c.getString(2);
|
|
||||||
if (stringData.startsWith(ADDRESS_PREFIX)) {
|
|
||||||
details.localAddress = c.getString(2).substring(ADDRESS_PREFIX.length());
|
|
||||||
} else {
|
|
||||||
LimeLog.severe("DB: Corrupted local address for " + details.name);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
details.remoteAddress = InetAddress.getByAddress(c.getBlob(3)).getHostAddress();
|
|
||||||
LimeLog.warning("DB: Legacy remote address for " + details.name);
|
|
||||||
} catch (UnknownHostException e) {
|
|
||||||
// This is probably a hostname/address with the prefix string
|
|
||||||
String stringData = c.getString(3);
|
|
||||||
if (stringData.startsWith(ADDRESS_PREFIX)) {
|
|
||||||
details.remoteAddress = c.getString(3).substring(ADDRESS_PREFIX.length());
|
|
||||||
} else {
|
|
||||||
LimeLog.severe("DB: Corrupted remote address for " + details.name);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// On older versions of Moonlight, this is typically where manual addresses got stored,
|
|
||||||
// so let's initialize it just to be safe.
|
|
||||||
details.manualAddress = details.remoteAddress;
|
|
||||||
|
|
||||||
details.macAddress = c.getString(4);
|
|
||||||
|
|
||||||
// This signifies we don't have dynamic state (like pair state)
|
|
||||||
details.state = ComputerDetails.State.UNKNOWN;
|
|
||||||
|
|
||||||
return details;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static List<ComputerDetails> getAllComputers(SQLiteDatabase db) {
|
|
||||||
Cursor c = db.rawQuery("SELECT * FROM " + COMPUTER_TABLE_NAME, null);
|
|
||||||
LinkedList<ComputerDetails> computerList = new LinkedList<>();
|
|
||||||
while (c.moveToNext()) {
|
|
||||||
ComputerDetails details = getComputerFromCursor(c);
|
|
||||||
|
|
||||||
// If a critical field is corrupt or missing, skip the database entry
|
|
||||||
if (details.uuid == null) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
computerList.add(details);
|
|
||||||
}
|
|
||||||
|
|
||||||
c.close();
|
|
||||||
|
|
||||||
return computerList;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static List<ComputerDetails> migrateAllComputers(Context c) {
|
|
||||||
SQLiteDatabase computerDb = null;
|
|
||||||
try {
|
|
||||||
// Open the existing database
|
|
||||||
computerDb = SQLiteDatabase.openDatabase(c.getDatabasePath(COMPUTER_DB_NAME).getPath(), null, SQLiteDatabase.OPEN_READONLY);
|
|
||||||
return getAllComputers(computerDb);
|
|
||||||
} catch (SQLiteException e) {
|
|
||||||
return new LinkedList<ComputerDetails>();
|
|
||||||
} finally {
|
|
||||||
// Close and delete the old DB
|
|
||||||
if (computerDb != null) {
|
|
||||||
computerDb.close();
|
|
||||||
}
|
|
||||||
c.deleteDatabase(COMPUTER_DB_NAME);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -16,75 +16,75 @@ import android.os.IBinder;
|
|||||||
|
|
||||||
public class DiscoveryService extends Service {
|
public class DiscoveryService extends Service {
|
||||||
|
|
||||||
private MdnsDiscoveryAgent discoveryAgent;
|
private MdnsDiscoveryAgent discoveryAgent;
|
||||||
private MdnsDiscoveryListener boundListener;
|
private MdnsDiscoveryListener boundListener;
|
||||||
private MulticastLock multicastLock;
|
private MulticastLock multicastLock;
|
||||||
|
|
||||||
public class DiscoveryBinder extends Binder {
|
public class DiscoveryBinder extends Binder {
|
||||||
public void setListener(MdnsDiscoveryListener listener) {
|
public void setListener(MdnsDiscoveryListener listener) {
|
||||||
boundListener = listener;
|
boundListener = listener;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void startDiscovery(int queryIntervalMs) {
|
public void startDiscovery(int queryIntervalMs) {
|
||||||
multicastLock.acquire();
|
multicastLock.acquire();
|
||||||
discoveryAgent.startDiscovery(queryIntervalMs);
|
discoveryAgent.startDiscovery(queryIntervalMs);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void stopDiscovery() {
|
public void stopDiscovery() {
|
||||||
discoveryAgent.stopDiscovery();
|
discoveryAgent.stopDiscovery();
|
||||||
multicastLock.release();
|
multicastLock.release();
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<MdnsComputer> getComputerSet() {
|
public List<MdnsComputer> getComputerSet() {
|
||||||
return discoveryAgent.getComputerSet();
|
return discoveryAgent.getComputerSet();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onCreate() {
|
public void onCreate() {
|
||||||
WifiManager wifiMgr = (WifiManager) getApplicationContext().getSystemService(Context.WIFI_SERVICE);
|
WifiManager wifiMgr = (WifiManager) getSystemService(Context.WIFI_SERVICE);
|
||||||
multicastLock = wifiMgr.createMulticastLock("Limelight mDNS");
|
multicastLock = wifiMgr.createMulticastLock("Limelight mDNS");
|
||||||
multicastLock.setReferenceCounted(false);
|
multicastLock.setReferenceCounted(false);
|
||||||
|
|
||||||
discoveryAgent = new MdnsDiscoveryAgent(new MdnsDiscoveryListener() {
|
discoveryAgent = new MdnsDiscoveryAgent(new MdnsDiscoveryListener() {
|
||||||
@Override
|
@Override
|
||||||
public void notifyComputerAdded(MdnsComputer computer) {
|
public void notifyComputerAdded(MdnsComputer computer) {
|
||||||
if (boundListener != null) {
|
if (boundListener != null) {
|
||||||
boundListener.notifyComputerAdded(computer);
|
boundListener.notifyComputerAdded(computer);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void notifyComputerRemoved(MdnsComputer computer) {
|
public void notifyComputerRemoved(MdnsComputer computer) {
|
||||||
if (boundListener != null) {
|
if (boundListener != null) {
|
||||||
boundListener.notifyComputerRemoved(computer);
|
boundListener.notifyComputerRemoved(computer);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void notifyDiscoveryFailure(Exception e) {
|
public void notifyDiscoveryFailure(Exception e) {
|
||||||
if (boundListener != null) {
|
if (boundListener != null) {
|
||||||
boundListener.notifyDiscoveryFailure(e);
|
boundListener.notifyDiscoveryFailure(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private final DiscoveryBinder binder = new DiscoveryBinder();
|
private DiscoveryBinder binder = new DiscoveryBinder();
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public IBinder onBind(Intent intent) {
|
public IBinder onBind(Intent intent) {
|
||||||
return binder;
|
return binder;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean onUnbind(Intent intent) {
|
public boolean onUnbind(Intent intent) {
|
||||||
// Stop any discovery session
|
// Stop any discovery session
|
||||||
discoveryAgent.stopDiscovery();
|
discoveryAgent.stopDiscovery();
|
||||||
multicastLock.release();
|
multicastLock.release();
|
||||||
|
|
||||||
// Unbind the listener
|
// Unbind the listener
|
||||||
boundListener = null;
|
boundListener = null;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,88 +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.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);
|
|
||||||
|
|
||||||
this.loader = new CachedAppAssetLoader(computer, scalingDivisor,
|
|
||||||
new NetworkAssetLoader(context, uniqueId),
|
|
||||||
new MemoryAssetLoader(),
|
|
||||||
new DiskAssetLoader(context));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -97,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,15 +1,12 @@
|
|||||||
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;
|
||||||
import com.limelight.R;
|
import com.limelight.R;
|
||||||
import com.limelight.nvstream.http.ComputerDetails;
|
import com.limelight.nvstream.http.ComputerDetails;
|
||||||
import com.limelight.nvstream.http.PairingManager;
|
|
||||||
|
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.Comparator;
|
import java.util.Comparator;
|
||||||
@@ -17,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) {
|
||||||
@@ -29,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);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -39,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.state == ComputerDetails.State.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 {
|
||||||
@@ -73,19 +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;
|
|
||||||
}
|
|
||||||
// We must check if the status is exactly online and unpaired
|
|
||||||
// to avoid colliding with the loading spinner when status is unknown
|
|
||||||
else if (obj.details.state == ComputerDetails.State.ONLINE &&
|
|
||||||
obj.details.pairState == PairingManager.PairState.NOT_PAIRED) {
|
|
||||||
overlayView.setImageResource(R.drawable.ic_lock);
|
|
||||||
overlayView.setAlpha(1.0f);
|
|
||||||
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,158 +0,0 @@
|
|||||||
package com.limelight.grid.assets;
|
|
||||||
|
|
||||||
import android.app.ActivityManager;
|
|
||||||
import android.content.Context;
|
|
||||||
import android.graphics.Bitmap;
|
|
||||||
import android.graphics.BitmapFactory;
|
|
||||||
import android.graphics.ImageDecoder;
|
|
||||||
import android.os.Build;
|
|
||||||
|
|
||||||
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 static final long MAX_ASSET_SIZE = 5 * 1024 * 1024;
|
|
||||||
|
|
||||||
// Standard box art is 300x400
|
|
||||||
private static final int STANDARD_ASSET_WIDTH = 300;
|
|
||||||
private static final int STANDARD_ASSET_HEIGHT = 400;
|
|
||||||
|
|
||||||
private final boolean isLowRamDevice;
|
|
||||||
private final File cacheDir;
|
|
||||||
|
|
||||||
public DiskAssetLoader(Context context) {
|
|
||||||
this.cacheDir = context.getCacheDir();
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
|
|
||||||
this.isLowRamDevice =
|
|
||||||
((ActivityManager)context.getSystemService(Context.ACTIVITY_SERVICE)).isLowRamDevice();
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
// Use conservative low RAM behavior on very old devices
|
|
||||||
this.isLowRamDevice = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean checkCacheExists(CachedAppAssetLoader.LoaderTuple tuple) {
|
|
||||||
return CacheHelper.cacheFileExists(cacheDir, "boxart", tuple.computer.uuid, tuple.app.getAppId() + ".png");
|
|
||||||
}
|
|
||||||
|
|
||||||
// https://developer.android.com/topic/performance/graphics/load-bitmap.html
|
|
||||||
public static int calculateInSampleSize(BitmapFactory.Options options, int reqWidth, int reqHeight) {
|
|
||||||
// Raw height and width of image
|
|
||||||
final int height = options.outHeight;
|
|
||||||
final int width = options.outWidth;
|
|
||||||
int inSampleSize = 1;
|
|
||||||
|
|
||||||
if (height > reqHeight || width > reqWidth) {
|
|
||||||
|
|
||||||
final int halfHeight = height / 2;
|
|
||||||
final int halfWidth = width / 2;
|
|
||||||
|
|
||||||
// Calculates the largest inSampleSize value that is a power of 2 and keeps both
|
|
||||||
// height and width larger than the requested height and width.
|
|
||||||
while ((halfHeight / inSampleSize) >= reqHeight && (halfWidth / inSampleSize) >= reqWidth) {
|
|
||||||
inSampleSize *= 2;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return inSampleSize;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Bitmap loadBitmapFromCache(CachedAppAssetLoader.LoaderTuple tuple, int sampleSize) {
|
|
||||||
File file = CacheHelper.openPath(false, cacheDir, "boxart", tuple.computer.uuid, tuple.app.getAppId() + ".png");
|
|
||||||
|
|
||||||
// Don't bother with anything if it doesn't exist
|
|
||||||
if (!file.exists()) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Make sure the cached asset doesn't exceed the maximum size
|
|
||||||
if (file.length() > MAX_ASSET_SIZE) {
|
|
||||||
LimeLog.warning("Removing cached tuple exceeding size threshold: "+tuple);
|
|
||||||
file.delete();
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
Bitmap bmp;
|
|
||||||
|
|
||||||
// For OSes prior to P, we have to use the ugly BitmapFactory API
|
|
||||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) {
|
|
||||||
// Lookup bounds of the downloaded image
|
|
||||||
BitmapFactory.Options decodeOnlyOptions = new BitmapFactory.Options();
|
|
||||||
decodeOnlyOptions.inJustDecodeBounds = true;
|
|
||||||
BitmapFactory.decodeFile(file.getAbsolutePath(), decodeOnlyOptions);
|
|
||||||
if (decodeOnlyOptions.outWidth <= 0 || decodeOnlyOptions.outHeight <= 0) {
|
|
||||||
// Dimensions set to -1 on error. Return value always null.
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
LimeLog.info("Tuple "+tuple+" has cached art of size: "+decodeOnlyOptions.outWidth+"x"+decodeOnlyOptions.outHeight);
|
|
||||||
|
|
||||||
// Load the image scaled to the appropriate size
|
|
||||||
BitmapFactory.Options options = new BitmapFactory.Options();
|
|
||||||
options.inSampleSize = calculateInSampleSize(decodeOnlyOptions,
|
|
||||||
STANDARD_ASSET_WIDTH / sampleSize,
|
|
||||||
STANDARD_ASSET_HEIGHT / sampleSize);
|
|
||||||
if (isLowRamDevice) {
|
|
||||||
options.inPreferredConfig = Bitmap.Config.RGB_565;
|
|
||||||
options.inDither = true;
|
|
||||||
}
|
|
||||||
else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
||||||
options.inPreferredConfig = Bitmap.Config.HARDWARE;
|
|
||||||
}
|
|
||||||
|
|
||||||
bmp = BitmapFactory.decodeFile(file.getAbsolutePath(), options);
|
|
||||||
if (bmp != null) {
|
|
||||||
LimeLog.info("Tuple "+tuple+" decoded from disk cache with sample size: "+options.inSampleSize);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
// On P, we can get a bitmap back in one step with ImageDecoder
|
|
||||||
try {
|
|
||||||
bmp = ImageDecoder.decodeBitmap(ImageDecoder.createSource(file), new ImageDecoder.OnHeaderDecodedListener() {
|
|
||||||
@Override
|
|
||||||
public void onHeaderDecoded(ImageDecoder imageDecoder, ImageDecoder.ImageInfo imageInfo, ImageDecoder.Source source) {
|
|
||||||
imageDecoder.setTargetSize(STANDARD_ASSET_WIDTH, STANDARD_ASSET_HEIGHT);
|
|
||||||
if (isLowRamDevice) {
|
|
||||||
imageDecoder.setMemorySizePolicy(ImageDecoder.MEMORY_POLICY_LOW_RAM);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} catch (IOException e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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, 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, 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+"-"+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,39 +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,
|
|
||||||
tuple.computer.serverCert, 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,12 +1,8 @@
|
|||||||
package com.limelight.preferences;
|
package com.limelight.preferences;
|
||||||
|
|
||||||
import java.net.Inet4Address;
|
|
||||||
import java.net.InetAddress;
|
import java.net.InetAddress;
|
||||||
import java.net.InterfaceAddress;
|
|
||||||
import java.net.NetworkInterface;
|
|
||||||
import java.net.SocketException;
|
|
||||||
import java.net.UnknownHostException;
|
import java.net.UnknownHostException;
|
||||||
import java.util.Collections;
|
import java.util.Locale;
|
||||||
import java.util.concurrent.LinkedBlockingQueue;
|
import java.util.concurrent.LinkedBlockingQueue;
|
||||||
|
|
||||||
import com.limelight.computers.ComputerManagerService;
|
import com.limelight.computers.ComputerManagerService;
|
||||||
@@ -18,183 +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.View;
|
|
||||||
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 boolean isWrongSubnetSiteLocalAddress(String address) {
|
private void doAddPc(String host) {
|
||||||
try {
|
String msg;
|
||||||
InetAddress targetAddress = InetAddress.getByName(address);
|
boolean finish = false;
|
||||||
if (!(targetAddress instanceof Inet4Address) || !targetAddress.isSiteLocalAddress()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// We have a site-local address. Look for a matching local interface.
|
|
||||||
for (NetworkInterface iface : Collections.list(NetworkInterface.getNetworkInterfaces())) {
|
|
||||||
for (InterfaceAddress addr : iface.getInterfaceAddresses()) {
|
|
||||||
if (!(addr.getAddress() instanceof Inet4Address) || !addr.getAddress().isSiteLocalAddress()) {
|
|
||||||
// Skip non-site-local or non-IPv4 addresses
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
byte[] targetAddrBytes = targetAddress.getAddress();
|
|
||||||
byte[] ifaceAddrBytes = addr.getAddress().getAddress();
|
|
||||||
|
|
||||||
// Compare prefix to ensure it's the same
|
|
||||||
boolean addressMatches = true;
|
|
||||||
for (int i = 0; i < addr.getNetworkPrefixLength(); i++) {
|
|
||||||
if ((ifaceAddrBytes[i / 8] & (1 << (i % 8))) != (targetAddrBytes[i / 8] & (1 << (i % 8)))) {
|
|
||||||
addressMatches = false;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (addressMatches) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Couldn't find a matching interface
|
|
||||||
return true;
|
|
||||||
} catch (SocketException e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
return false;
|
|
||||||
} catch (UnknownHostException e) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void doAddPc(String host) {
|
|
||||||
boolean wrongSiteLocal = false;
|
|
||||||
boolean success;
|
|
||||||
|
|
||||||
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);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
success = managerBinder.addComputerBlocking(host, true);
|
InetAddress addr = InetAddress.getByName(host);
|
||||||
} catch (IllegalArgumentException e) {
|
|
||||||
// This can be thrown from OkHttp if the host fails to canonicalize to a valid name.
|
if (!managerBinder.addComputerBlocking(addr)){
|
||||||
// https://github.com/square/okhttp/blob/okhttp_27/okhttp/src/main/java/com/squareup/okhttp/HttpUrl.java#L705
|
msg = getResources().getString(R.string.addpc_fail);
|
||||||
e.printStackTrace();
|
}
|
||||||
success = false;
|
else {
|
||||||
}
|
msg = getResources().getString(R.string.addpc_success);
|
||||||
if (!success){
|
finish = true;
|
||||||
wrongSiteLocal = isWrongSubnetSiteLocalAddress(host);
|
}
|
||||||
}
|
} catch (UnknownHostException e) {
|
||||||
|
msg = getResources().getString(R.string.addpc_unknown_host);
|
||||||
|
}
|
||||||
|
|
||||||
dialog.dismiss();
|
dialog.dismiss();
|
||||||
|
|
||||||
if (wrongSiteLocal) {
|
final boolean toastFinish = finish;
|
||||||
Dialog.displayDialog(this, getResources().getString(R.string.conn_error_title), getResources().getString(R.string.addpc_wrong_sitelocal), false);
|
final String toastMsg = msg;
|
||||||
}
|
AddComputerManually.this.runOnUiThread(new Runnable() {
|
||||||
else if (!success) {
|
@Override
|
||||||
Dialog.displayDialog(this, getResources().getString(R.string.conn_error_title), getResources().getString(R.string.addpc_fail), false);
|
public void run() {
|
||||||
}
|
Toast.makeText(AddComputerManually.this, toastMsg, Toast.LENGTH_LONG).show();
|
||||||
else {
|
|
||||||
AddComputerManually.this.runOnUiThread(new Runnable() {
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
Toast.makeText(AddComputerManually.this, getResources().getString(R.string.addpc_success), Toast.LENGTH_LONG).show();
|
|
||||||
|
|
||||||
if (!isFinishing()) {
|
if (toastFinish && !isFinishing()) {
|
||||||
// Close the activity
|
// Close the activity
|
||||||
AddComputerManually.this.finish();
|
AddComputerManually.this.finish();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
private void startAddThread() {
|
||||||
|
addThread = new Thread() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
while (!isInterrupted()) {
|
||||||
|
String computer;
|
||||||
|
|
||||||
private void startAddThread() {
|
try {
|
||||||
addThread = new Thread() {
|
computer = computersToAdd.take();
|
||||||
@Override
|
} catch (InterruptedException e) {
|
||||||
public void run() {
|
return;
|
||||||
while (!isInterrupted()) {
|
}
|
||||||
String computer;
|
|
||||||
|
|
||||||
try {
|
doAddPc(computer);
|
||||||
computer = computersToAdd.take();
|
}
|
||||||
} catch (InterruptedException e) {
|
}
|
||||||
return;
|
};
|
||||||
}
|
addThread.setName("UI - AddComputerManually");
|
||||||
|
addThread.start();
|
||||||
|
}
|
||||||
|
|
||||||
doAddPc(computer);
|
private void joinAddThread() {
|
||||||
}
|
if (addThread != null) {
|
||||||
}
|
addThread.interrupt();
|
||||||
};
|
|
||||||
addThread.setName("UI - AddComputerManually");
|
|
||||||
addThread.start();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void joinAddThread() {
|
try {
|
||||||
if (addThread != null) {
|
addThread.join();
|
||||||
addThread.interrupt();
|
} catch (InterruptedException ignored) {}
|
||||||
|
|
||||||
try {
|
addThread = null;
|
||||||
addThread.join();
|
}
|
||||||
} catch (InterruptedException ignored) {}
|
}
|
||||||
|
|
||||||
addThread = null;
|
@Override
|
||||||
}
|
protected void onStop() {
|
||||||
}
|
super.onStop();
|
||||||
|
|
||||||
@Override
|
Dialog.closeDialogs();
|
||||||
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)) {
|
||||||
|
Configuration config = new Configuration(getResources().getConfiguration());
|
||||||
|
config.locale = new Locale(locale);
|
||||||
|
getResources().updateConfiguration(config, getResources().getDisplayMetrics());
|
||||||
|
}
|
||||||
|
|
||||||
setContentView(R.layout.activity_add_computer_manually);
|
setContentView(R.layout.activity_add_computer_manually);
|
||||||
|
|
||||||
UiHelper.notifyNewRootView(this);
|
UiHelper.notifyNewRootView(this);
|
||||||
|
|
||||||
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
|
||||||
@@ -203,41 +154,20 @@ public class AddComputerManually extends Activity {
|
|||||||
(keyEvent != null &&
|
(keyEvent != null &&
|
||||||
keyEvent.getAction() == KeyEvent.ACTION_DOWN &&
|
keyEvent.getAction() == KeyEvent.ACTION_DOWN &&
|
||||||
keyEvent.getKeyCode() == KeyEvent.KEYCODE_ENTER)) {
|
keyEvent.getKeyCode() == KeyEvent.KEYCODE_ENTER)) {
|
||||||
return handleDoneEvent();
|
if (hostText.getText().length() == 0) {
|
||||||
}
|
Toast.makeText(AddComputerManually.this, getResources().getString(R.string.addpc_enter_ip), Toast.LENGTH_LONG).show();
|
||||||
else if (actionId == EditorInfo.IME_ACTION_PREVIOUS) {
|
return true;
|
||||||
// This is how the Fire TV dismisses the keyboard
|
}
|
||||||
InputMethodManager imm = (InputMethodManager)getSystemService(Context.INPUT_METHOD_SERVICE);
|
|
||||||
imm.hideSoftInputFromWindow(hostText.getWindowToken(), 0);
|
computersToAdd.add(hostText.getText().toString().trim());
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
findViewById(R.id.addPcButton).setOnClickListener(new View.OnClickListener() {
|
// Bind to the ComputerManager service
|
||||||
@Override
|
bindService(new Intent(AddComputerManually.this,
|
||||||
public void onClick(View view) {
|
ComputerManagerService.class), serviceConnection, Service.BIND_AUTO_CREATE);
|
||||||
handleDoneEvent();
|
}
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Bind to the ComputerManager service
|
|
||||||
bindService(new Intent(AddComputerManually.this,
|
|
||||||
ComputerManagerService.class), serviceConnection, Service.BIND_AUTO_CREATE);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Returns true if the event should be eaten
|
|
||||||
private boolean handleDoneEvent() {
|
|
||||||
String hostAddress = hostText.getText().toString().trim();
|
|
||||||
|
|
||||||
if (hostAddress.length() == 0) {
|
|
||||||
Toast.makeText(AddComputerManually.this, getResources().getString(R.string.addpc_enter_ip), Toast.LENGTH_LONG).show();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
computersToAdd.add(hostAddress);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
package com.limelight.preferences;
|
|
||||||
|
|
||||||
import android.annotation.TargetApi;
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.DialogInterface;
|
|
||||||
import android.os.Build;
|
|
||||||
import android.preference.DialogPreference;
|
|
||||||
import android.util.AttributeSet;
|
|
||||||
import android.widget.Toast;
|
|
||||||
|
|
||||||
import com.limelight.R;
|
|
||||||
|
|
||||||
import static com.limelight.binding.input.virtual_controller.VirtualControllerConfigurationLoader.OSC_PREFERENCE;
|
|
||||||
|
|
||||||
public class ConfirmDeleteOscPreference extends DialogPreference {
|
|
||||||
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
|
|
||||||
public ConfirmDeleteOscPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
|
|
||||||
super(context, attrs, defStyleAttr, defStyleRes);
|
|
||||||
}
|
|
||||||
|
|
||||||
public ConfirmDeleteOscPreference(Context context, AttributeSet attrs, int defStyleAttr) {
|
|
||||||
super(context, attrs, defStyleAttr);
|
|
||||||
}
|
|
||||||
|
|
||||||
public ConfirmDeleteOscPreference(Context context, AttributeSet attrs) {
|
|
||||||
super(context, attrs);
|
|
||||||
}
|
|
||||||
|
|
||||||
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
|
|
||||||
public ConfirmDeleteOscPreference(Context context) {
|
|
||||||
super(context);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void onClick(DialogInterface dialog, int which) {
|
|
||||||
if (which == DialogInterface.BUTTON_POSITIVE) {
|
|
||||||
getContext().getSharedPreferences(OSC_PREFERENCE, Context.MODE_PRIVATE).edit().clear().apply();
|
|
||||||
Toast.makeText(getContext(), R.string.toast_reset_osc_success, Toast.LENGTH_SHORT).show();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
package com.limelight.preferences;
|
|
||||||
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.SharedPreferences;
|
|
||||||
|
|
||||||
public class GlPreferences {
|
|
||||||
private static final String PREF_NAME = "GlPreferences";
|
|
||||||
|
|
||||||
private static final String FINGERPRINT_PREF_STRING = "Fingerprint";
|
|
||||||
private static final String GL_RENDERER_PREF_STRING = "Renderer";
|
|
||||||
|
|
||||||
private SharedPreferences prefs;
|
|
||||||
public String glRenderer;
|
|
||||||
public String savedFingerprint;
|
|
||||||
|
|
||||||
private GlPreferences(SharedPreferences prefs) {
|
|
||||||
this.prefs = prefs;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static GlPreferences readPreferences(Context context) {
|
|
||||||
SharedPreferences prefs = context.getSharedPreferences(PREF_NAME, 0);
|
|
||||||
GlPreferences glPrefs = new GlPreferences(prefs);
|
|
||||||
|
|
||||||
glPrefs.glRenderer = prefs.getString(GL_RENDERER_PREF_STRING, "");
|
|
||||||
glPrefs.savedFingerprint = prefs.getString(FINGERPRINT_PREF_STRING, "");
|
|
||||||
|
|
||||||
return glPrefs;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean writePreferences() {
|
|
||||||
return prefs.edit()
|
|
||||||
.putString(GL_RENDERER_PREF_STRING, glRenderer)
|
|
||||||
.putString(FINGERPRINT_PREF_STRING, savedFingerprint)
|
|
||||||
.commit();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -2,18 +2,14 @@ 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 {
|
||||||
private static final String LEGACY_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";
|
||||||
static final String RESOLUTION_PREF_STRING = "list_resolution";
|
private static final String DECODER_PREF_STRING = "list_decoders";
|
||||||
static final String FPS_PREF_STRING = "list_fps";
|
static final String BITRATE_PREF_STRING = "seekbar_bitrate";
|
||||||
static final String BITRATE_PREF_STRING = "seekbar_bitrate_kbps";
|
|
||||||
private static final String BITRATE_PREF_OLD_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";
|
||||||
private static final String DISABLE_TOASTS_PREF_STRING = "checkbox_disable_warnings";
|
private static final String DISABLE_TOASTS_PREF_STRING = "checkbox_disable_warnings";
|
||||||
@@ -22,24 +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 ONLY_L3_R3_PREF_STRING = "checkbox_only_show_L3R3";
|
|
||||||
private static final String DISABLE_FRAME_DROP_PREF_STRING = "checkbox_disable_frame_drop";
|
|
||||||
private static final String ENABLE_HDR_PREF_STRING = "checkbox_enable_hdr";
|
|
||||||
private static final String ENABLE_PIP_PREF_STRING = "checkbox_enable_pip";
|
|
||||||
private static final String BIND_ALL_USB_STRING = "checkbox_usb_bind_all";
|
|
||||||
private static final String MOUSE_EMULATION_STRING = "checkbox_mouse_emulation";
|
|
||||||
private static final String MOUSE_NAV_BUTTONS_STRING = "checkbox_mouse_nav_buttons";
|
|
||||||
static final String UNLOCK_FPS_STRING = "checkbox_unlock_fps";
|
|
||||||
private static final String VIBRATE_OSC_PREF_STRING = "checkbox_vibrate_osc";
|
|
||||||
private static final String VIBRATE_FALLBACK_PREF_STRING = "checkbox_vibrate_fallback";
|
|
||||||
|
|
||||||
static final String DEFAULT_RESOLUTION = "720p";
|
private static final int BITRATE_DEFAULT_720_30 = 5;
|
||||||
static final String DEFAULT_FPS = "60";
|
private static final int BITRATE_DEFAULT_720_60 = 10;
|
||||||
|
private static final int BITRATE_DEFAULT_1080_30 = 10;
|
||||||
|
private static final int BITRATE_DEFAULT_1080_60 = 20;
|
||||||
|
private static final int BITRATE_DEFAULT_MAX = 35;
|
||||||
|
|
||||||
|
private static final int DEFAULT_RESX = 1280;
|
||||||
|
private static final int DEFAULT_RESY = 720;
|
||||||
|
private static final String DEFAULT_FPS = "60";
|
||||||
|
private static final String DEFAULT_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;
|
||||||
@@ -47,270 +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 ONLY_L3_R3_DEFAULT = false;
|
|
||||||
private static final boolean DEFAULT_DISABLE_FRAME_DROP = false;
|
|
||||||
private static final boolean DEFAULT_ENABLE_HDR = false;
|
|
||||||
private static final boolean DEFAULT_ENABLE_PIP = false;
|
|
||||||
private static final boolean DEFAULT_BIND_ALL_USB = false;
|
|
||||||
private static final boolean DEFAULT_MOUSE_EMULATION = true;
|
|
||||||
private static final boolean DEFAULT_MOUSE_NAV_BUTTONS = false;
|
|
||||||
private static final boolean DEFAULT_UNLOCK_FPS = false;
|
|
||||||
private static final boolean DEFAULT_VIBRATE_OSC = true;
|
|
||||||
private static final boolean DEFAULT_VIBRATE_FALLBACK = 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 onlyL3R3;
|
|
||||||
public boolean disableFrameDrop;
|
|
||||||
public boolean enableHdr;
|
|
||||||
public boolean enablePip;
|
|
||||||
public boolean bindAllUsb;
|
|
||||||
public boolean mouseEmulation;
|
|
||||||
public boolean mouseNavButtons;
|
|
||||||
public boolean unlockFps;
|
|
||||||
public boolean vibrateOsc;
|
|
||||||
public boolean vibrateFallbackToDevice;
|
|
||||||
|
|
||||||
private static int getHeightFromResolutionString(String resString) {
|
|
||||||
if (resString.equalsIgnoreCase("360p")) {
|
|
||||||
return 360;
|
|
||||||
}
|
|
||||||
else if (resString.equalsIgnoreCase("480p")) {
|
|
||||||
return 480;
|
|
||||||
}
|
|
||||||
else if (resString.equalsIgnoreCase("720p")) {
|
|
||||||
return 720;
|
|
||||||
}
|
|
||||||
else if (resString.equalsIgnoreCase("1080p")) {
|
|
||||||
return 1080;
|
|
||||||
}
|
|
||||||
else if (resString.equalsIgnoreCase("1440p")) {
|
|
||||||
return 1440;
|
|
||||||
}
|
|
||||||
else if (resString.equalsIgnoreCase("4K")) {
|
|
||||||
return 2160;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
// Should be unreachable
|
|
||||||
return 720;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static int getWidthFromResolutionString(String resString) {
|
|
||||||
int height = getHeightFromResolutionString(resString);
|
|
||||||
if (height == 480) {
|
|
||||||
// This isn't an exact 16:9 resolution
|
|
||||||
return 854;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
return (height * 16) / 9;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String getResolutionString(int width, int height) {
|
|
||||||
switch (height) {
|
|
||||||
case 360:
|
|
||||||
return "360p";
|
|
||||||
case 480:
|
|
||||||
return "480p";
|
|
||||||
default:
|
|
||||||
case 720:
|
|
||||||
return "720p";
|
|
||||||
case 1080:
|
|
||||||
return "1080p";
|
|
||||||
case 1440:
|
|
||||||
return "1440p";
|
|
||||||
case 2160:
|
|
||||||
return "4K";
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static int getDefaultBitrate(String resString, String fpsString) {
|
|
||||||
int width = getWidthFromResolutionString(resString);
|
|
||||||
int height = getHeightFromResolutionString(resString);
|
|
||||||
int fps = Integer.parseInt(fpsString);
|
|
||||||
|
|
||||||
// This table prefers 16:10 resolutions because they are
|
|
||||||
// only slightly more pixels than the 16:9 equivalents, so
|
|
||||||
// we don't want to bump those 16:10 resolutions up to the
|
|
||||||
// next 16:9 slot.
|
|
||||||
//
|
|
||||||
// This logic is shamelessly stolen from Moonlight Qt:
|
|
||||||
// https://github.com/moonlight-stream/moonlight-qt/blob/master/app/settings/streamingpreferences.cpp
|
|
||||||
|
|
||||||
if (width * height <= 640 * 360) {
|
|
||||||
return (int)(1000 * (fps / 30.0));
|
|
||||||
}
|
|
||||||
else if (width * height <= 854 * 480) {
|
|
||||||
return (int)(1500 * (fps / 30.0));
|
|
||||||
}
|
|
||||||
// This covers 1280x720 and 1280x800 too
|
|
||||||
else if (width * height <= 1366 * 768) {
|
|
||||||
return (int)(5000 * (fps / 30.0));
|
|
||||||
}
|
|
||||||
else if (width * height <= 1920 * 1200) {
|
|
||||||
return (int)(10000 * (fps / 30.0));
|
|
||||||
}
|
|
||||||
else if (width * height <= 2560 * 1600) {
|
|
||||||
return (int)(20000 * (fps / 30.0));
|
|
||||||
}
|
|
||||||
else /* if (width * height <= 3840 * 2160) */ {
|
|
||||||
return (int)(40000 * (fps / 30.0));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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(RESOLUTION_PREF_STRING, DEFAULT_RESOLUTION),
|
long width = prefs.getInt(RESX_PREF_STRING, DEFAULT_RESX);
|
||||||
prefs.getString(FPS_PREF_STRING, DEFAULT_FPS));
|
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, HDR, and video format as "streaming settings" here
|
|
||||||
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
|
|
||||||
prefs.edit()
|
|
||||||
.remove(BITRATE_PREF_STRING)
|
|
||||||
.remove(BITRATE_PREF_OLD_STRING)
|
|
||||||
.remove(LEGACY_RES_FPS_PREF_STRING)
|
|
||||||
.remove(RESOLUTION_PREF_STRING)
|
|
||||||
.remove(FPS_PREF_STRING)
|
|
||||||
.remove(VIDEO_FORMAT_PREF_STRING)
|
|
||||||
.remove(ENABLE_HDR_PREF_STRING)
|
|
||||||
.remove(UNLOCK_FPS_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();
|
||||||
|
|
||||||
// Migrate legacy preferences to the new locations
|
config.bitrate = prefs.getInt(BITRATE_PREF_STRING, getDefaultBitrate(context));
|
||||||
String str = prefs.getString(LEGACY_RES_FPS_PREF_STRING, null);
|
config.width = prefs.getInt(RESX_PREF_STRING, DEFAULT_RESX);
|
||||||
if (str != null) {
|
config.height = prefs.getInt(RESY_PREF_STRING, DEFAULT_RESY);
|
||||||
if (str.equals("360p30")) {
|
config.fps = getFpsInt(prefs);
|
||||||
config.width = 640;
|
|
||||||
config.height = 360;
|
|
||||||
config.fps = 30;
|
|
||||||
}
|
|
||||||
else if (str.equals("360p60")) {
|
|
||||||
config.width = 640;
|
|
||||||
config.height = 360;
|
|
||||||
config.fps = 60;
|
|
||||||
}
|
|
||||||
else if (str.equals("720p30")) {
|
|
||||||
config.width = 1280;
|
|
||||||
config.height = 720;
|
|
||||||
config.fps = 30;
|
|
||||||
}
|
|
||||||
else if (str.equals("720p60")) {
|
|
||||||
config.width = 1280;
|
|
||||||
config.height = 720;
|
|
||||||
config.fps = 60;
|
|
||||||
}
|
|
||||||
else if (str.equals("1080p30")) {
|
|
||||||
config.width = 1920;
|
|
||||||
config.height = 1080;
|
|
||||||
config.fps = 30;
|
|
||||||
}
|
|
||||||
else if (str.equals("1080p60")) {
|
|
||||||
config.width = 1920;
|
|
||||||
config.height = 1080;
|
|
||||||
config.fps = 60;
|
|
||||||
}
|
|
||||||
else 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
prefs.edit()
|
config.decoder = getDecoderValue(context);
|
||||||
.remove(LEGACY_RES_FPS_PREF_STRING)
|
|
||||||
.putString(RESOLUTION_PREF_STRING, getResolutionString(config.width, config.height))
|
|
||||||
.putString(FPS_PREF_STRING, ""+config.fps)
|
|
||||||
.apply();
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
// Use the new preference location
|
|
||||||
String resStr = prefs.getString(RESOLUTION_PREF_STRING, PreferenceConfiguration.DEFAULT_RESOLUTION);
|
|
||||||
config.width = PreferenceConfiguration.getWidthFromResolutionString(resStr);
|
|
||||||
config.height = PreferenceConfiguration.getHeightFromResolutionString(resStr);
|
|
||||||
config.fps = Integer.parseInt(prefs.getString(FPS_PREF_STRING, PreferenceConfiguration.DEFAULT_FPS));
|
|
||||||
}
|
|
||||||
|
|
||||||
// This must happen after the preferences migration to ensure the preferences are populated
|
|
||||||
config.bitrate = prefs.getInt(BITRATE_PREF_STRING, prefs.getInt(BITRATE_PREF_OLD_STRING, 0) * 1000);
|
|
||||||
if (config.bitrate == 0) {
|
|
||||||
config.bitrate = getDefaultBitrate(context);
|
|
||||||
}
|
|
||||||
|
|
||||||
config.videoFormat = getVideoFormatValue(context);
|
|
||||||
|
|
||||||
config.deadzonePercentage = prefs.getInt(DEADZONE_PREF_STRING, DEFAULT_DEADZONE);
|
config.deadzonePercentage = prefs.getInt(DEADZONE_PREF_STRING, DEFAULT_DEADZONE);
|
||||||
|
|
||||||
@@ -323,20 +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.onlyL3R3 = prefs.getBoolean(ONLY_L3_R3_PREF_STRING, ONLY_L3_R3_DEFAULT);
|
|
||||||
config.disableFrameDrop = prefs.getBoolean(DISABLE_FRAME_DROP_PREF_STRING, DEFAULT_DISABLE_FRAME_DROP);
|
|
||||||
config.enableHdr = prefs.getBoolean(ENABLE_HDR_PREF_STRING, DEFAULT_ENABLE_HDR);
|
|
||||||
config.enablePip = prefs.getBoolean(ENABLE_PIP_PREF_STRING, DEFAULT_ENABLE_PIP);
|
|
||||||
config.bindAllUsb = prefs.getBoolean(BIND_ALL_USB_STRING, DEFAULT_BIND_ALL_USB);
|
|
||||||
config.mouseEmulation = prefs.getBoolean(MOUSE_EMULATION_STRING, DEFAULT_MOUSE_EMULATION);
|
|
||||||
config.mouseNavButtons = prefs.getBoolean(MOUSE_NAV_BUTTONS_STRING, DEFAULT_MOUSE_NAV_BUTTONS);
|
|
||||||
config.unlockFps = prefs.getBoolean(UNLOCK_FPS_STRING, DEFAULT_UNLOCK_FPS);
|
|
||||||
config.vibrateOsc = prefs.getBoolean(VIBRATE_OSC_PREF_STRING, DEFAULT_VIBRATE_OSC);
|
|
||||||
config.vibrateFallbackToDevice = prefs.getBoolean(VIBRATE_FALLBACK_PREF_STRING, DEFAULT_VIBRATE_FALLBACK);
|
|
||||||
|
|
||||||
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -16,48 +16,41 @@ import android.widget.TextView;
|
|||||||
// Based on a Stack Overflow example: http://stackoverflow.com/questions/1974193/slider-on-my-preferencescreen
|
// Based on a Stack Overflow example: http://stackoverflow.com/questions/1974193/slider-on-my-preferencescreen
|
||||||
public class SeekBarPreference extends DialogPreference
|
public class SeekBarPreference extends DialogPreference
|
||||||
{
|
{
|
||||||
private static final String ANDROID_SCHEMA_URL = "http://schemas.android.com/apk/res/android";
|
private static final String SCHEMA_URL = "http://schemas.android.com/apk/res/android";
|
||||||
private static final String SEEKBAR_SCHEMA_URL = "http://schemas.moonlight-stream.com/apk/res/seekbar";
|
|
||||||
|
|
||||||
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 final int stepSize;
|
|
||||||
private int currentValue;
|
|
||||||
|
|
||||||
public SeekBarPreference(Context context, AttributeSet attrs) {
|
public SeekBarPreference(Context context, AttributeSet attrs) {
|
||||||
super(context, attrs);
|
super(context, attrs);
|
||||||
this.context = context;
|
this.context = context;
|
||||||
|
|
||||||
// Read the message from XML
|
// Read the message from XML
|
||||||
int dialogMessageId = attrs.getAttributeResourceValue(ANDROID_SCHEMA_URL, "dialogMessage", 0);
|
int dialogMessageId = attrs.getAttributeResourceValue(SCHEMA_URL, "dialogMessage", 0);
|
||||||
if (dialogMessageId == 0) {
|
if (dialogMessageId == 0) {
|
||||||
dialogMessage = attrs.getAttributeValue(ANDROID_SCHEMA_URL, "dialogMessage");
|
dialogMessage = attrs.getAttributeValue(SCHEMA_URL, "dialogMessage");
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
dialogMessage = context.getString(dialogMessageId);
|
dialogMessage = context.getString(dialogMessageId);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the suffix for the number displayed in the dialog
|
// Get the suffix for the number displayed in the dialog
|
||||||
int suffixId = attrs.getAttributeResourceValue(ANDROID_SCHEMA_URL, "text", 0);
|
int suffixId = attrs.getAttributeResourceValue(SCHEMA_URL, "text", 0);
|
||||||
if (suffixId == 0) {
|
if (suffixId == 0) {
|
||||||
suffix = attrs.getAttributeValue(ANDROID_SCHEMA_URL, "text");
|
suffix = attrs.getAttributeValue(SCHEMA_URL, "text");
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
suffix = context.getString(suffixId);
|
suffix = context.getString(suffixId);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get default, min, and max seekbar values
|
// Get default, min, and max seekbar values
|
||||||
defaultValue = attrs.getAttributeIntValue(ANDROID_SCHEMA_URL, "defaultValue", PreferenceConfiguration.getDefaultBitrate(context));
|
defaultValue = attrs.getAttributeIntValue(SCHEMA_URL, "defaultValue", PreferenceConfiguration.getDefaultBitrate(context));
|
||||||
maxValue = attrs.getAttributeIntValue(ANDROID_SCHEMA_URL, "max", 100);
|
maxValue = attrs.getAttributeIntValue(SCHEMA_URL, "max", 100);
|
||||||
minValue = attrs.getAttributeIntValue(SEEKBAR_SCHEMA_URL, "min", 1);
|
minValue = 1;
|
||||||
stepSize = attrs.getAttributeIntValue(SEEKBAR_SCHEMA_URL, "step", 1);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -92,12 +85,6 @@ public class SeekBarPreference extends DialogPreference
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
int roundedValue = ((value + (stepSize - 1))/stepSize)*stepSize;
|
|
||||||
if (roundedValue != value) {
|
|
||||||
seekBar.setProgress(roundedValue);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
String t = String.valueOf(value);
|
String t = String.valueOf(value);
|
||||||
valueText.setText(suffix == null ? t : t.concat(suffix.length() > 1 ? " "+suffix : suffix));
|
valueText.setText(suffix == null ? t : t.concat(suffix.length() > 1 ? " "+suffix : suffix));
|
||||||
}
|
}
|
||||||
@@ -140,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,45 +2,35 @@ package com.limelight.preferences;
|
|||||||
|
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.content.SharedPreferences;
|
import android.content.SharedPreferences;
|
||||||
import android.media.MediaCodecInfo;
|
import android.content.res.Configuration;
|
||||||
import android.os.Build;
|
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
import android.os.Handler;
|
|
||||||
import android.preference.ListPreference;
|
|
||||||
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 android.util.Range;
|
|
||||||
import android.view.Display;
|
|
||||||
|
|
||||||
import com.limelight.LimeLog;
|
|
||||||
import com.limelight.PcView;
|
import com.limelight.PcView;
|
||||||
import com.limelight.R;
|
import com.limelight.R;
|
||||||
import com.limelight.binding.video.MediaCodecHelper;
|
|
||||||
import com.limelight.utils.UiHelper;
|
import com.limelight.utils.UiHelper;
|
||||||
|
|
||||||
|
import java.util.Locale;
|
||||||
|
|
||||||
public class StreamSettings extends Activity {
|
public class StreamSettings extends Activity {
|
||||||
private PreferenceConfiguration previousPrefs;
|
|
||||||
|
|
||||||
void reloadSettings() {
|
|
||||||
getFragmentManager().beginTransaction().replace(
|
|
||||||
R.id.stream_settings, new SettingsFragment()
|
|
||||||
).commit();
|
|
||||||
}
|
|
||||||
|
|
||||||
@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);
|
||||||
reloadSettings();
|
getFragmentManager().beginTransaction().replace(
|
||||||
|
R.id.stream_settings, new SettingsFragment()
|
||||||
|
).commit();
|
||||||
|
|
||||||
UiHelper.notifyNewRootView(this);
|
UiHelper.notifyNewRootView(this);
|
||||||
}
|
}
|
||||||
@@ -49,338 +39,18 @@ 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 {
|
||||||
|
|
||||||
private void setValue(String preferenceKey, String value) {
|
|
||||||
ListPreference pref = (ListPreference) findPreference(preferenceKey);
|
|
||||||
|
|
||||||
pref.setValue(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void removeValue(String preferenceKey, String value, Runnable onMatched) {
|
|
||||||
int matchingCount = 0;
|
|
||||||
|
|
||||||
ListPreference pref = (ListPreference) findPreference(preferenceKey);
|
|
||||||
|
|
||||||
// Count the number of matching entries we'll be removing
|
|
||||||
for (CharSequence seq : pref.getEntryValues()) {
|
|
||||||
if (seq.toString().equalsIgnoreCase(value)) {
|
|
||||||
matchingCount++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create the new arrays
|
|
||||||
CharSequence[] entries = new CharSequence[pref.getEntries().length-matchingCount];
|
|
||||||
CharSequence[] entryValues = new CharSequence[pref.getEntryValues().length-matchingCount];
|
|
||||||
int outIndex = 0;
|
|
||||||
for (int i = 0; i < pref.getEntryValues().length; i++) {
|
|
||||||
if (pref.getEntryValues()[i].toString().equalsIgnoreCase(value)) {
|
|
||||||
// Skip matching values
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
entries[outIndex] = pref.getEntries()[i];
|
|
||||||
entryValues[outIndex] = pref.getEntryValues()[i];
|
|
||||||
outIndex++;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (pref.getValue().equalsIgnoreCase(value)) {
|
|
||||||
onMatched.run();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update the preference with the new list
|
|
||||||
pref.setEntries(entries);
|
|
||||||
pref.setEntryValues(entryValues);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
private void resetBitrateToDefault(SharedPreferences prefs, String res, String fps) {
|
|
||||||
if (res == null) {
|
|
||||||
res = prefs.getString(PreferenceConfiguration.RESOLUTION_PREF_STRING, PreferenceConfiguration.DEFAULT_RESOLUTION);
|
|
||||||
}
|
|
||||||
if (fps == null) {
|
|
||||||
fps = prefs.getString(PreferenceConfiguration.FPS_PREF_STRING, PreferenceConfiguration.DEFAULT_FPS);
|
|
||||||
}
|
|
||||||
|
|
||||||
prefs.edit()
|
|
||||||
.putInt(PreferenceConfiguration.BITRATE_PREF_STRING,
|
|
||||||
PreferenceConfiguration.getDefaultBitrate(res, fps))
|
|
||||||
.apply();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onCreate(Bundle savedInstanceState) {
|
public void onCreate(Bundle savedInstanceState) {
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove PiP mode on devices pre-Oreo
|
|
||||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
|
|
||||||
PreferenceCategory category =
|
|
||||||
(PreferenceCategory) findPreference("category_basic_settings");
|
|
||||||
category.removePreference(findPreference("checkbox_enable_pip"));
|
|
||||||
}
|
|
||||||
|
|
||||||
int maxSupportedFps = 0;
|
|
||||||
|
|
||||||
// Hide non-supported resolution/FPS combinations
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
|
||||||
Display display = getActivity().getWindowManager().getDefaultDisplay();
|
|
||||||
|
|
||||||
int maxSupportedResW = 0;
|
|
||||||
|
|
||||||
// Always allow resolutions that are smaller or equal to the active
|
|
||||||
// display resolution because decoders can report total non-sense to us.
|
|
||||||
// For example, a p201 device reports:
|
|
||||||
// AVC Decoder: OMX.amlogic.avc.decoder.awesome
|
|
||||||
// HEVC Decoder: OMX.amlogic.hevc.decoder.awesome
|
|
||||||
// AVC supported width range: 64 - 384
|
|
||||||
// HEVC supported width range: 64 - 544
|
|
||||||
for (Display.Mode candidate : display.getSupportedModes()) {
|
|
||||||
// Some devices report their dimensions in the portrait orientation
|
|
||||||
// where height > width. Normalize these to the conventional width > height
|
|
||||||
// arrangement before we process them.
|
|
||||||
|
|
||||||
int width = Math.max(candidate.getPhysicalWidth(), candidate.getPhysicalHeight());
|
|
||||||
int height = Math.min(candidate.getPhysicalWidth(), candidate.getPhysicalHeight());
|
|
||||||
|
|
||||||
if ((width >= 3840 || height >= 2160) && maxSupportedResW < 3840) {
|
|
||||||
maxSupportedResW = 3840;
|
|
||||||
}
|
|
||||||
else if ((width >= 2560 || height >= 1440) && maxSupportedResW < 2560) {
|
|
||||||
maxSupportedResW = 2560;
|
|
||||||
}
|
|
||||||
else if ((width >= 1920 || height >= 1080) && maxSupportedResW < 1920) {
|
|
||||||
maxSupportedResW = 1920;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (candidate.getRefreshRate() > maxSupportedFps) {
|
|
||||||
maxSupportedFps = (int)candidate.getRefreshRate();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// This must be called to do runtime initialization before calling functions that evaluate
|
|
||||||
// decoder lists.
|
|
||||||
MediaCodecHelper.initialize(getContext(), GlPreferences.readPreferences(getContext()).glRenderer);
|
|
||||||
|
|
||||||
MediaCodecInfo avcDecoder = MediaCodecHelper.findProbableSafeDecoder("video/avc", -1);
|
|
||||||
MediaCodecInfo hevcDecoder = MediaCodecHelper.findProbableSafeDecoder("video/hevc", -1);
|
|
||||||
|
|
||||||
if (avcDecoder != null) {
|
|
||||||
Range<Integer> avcWidthRange = avcDecoder.getCapabilitiesForType("video/avc").getVideoCapabilities().getSupportedWidths();
|
|
||||||
|
|
||||||
LimeLog.info("AVC supported width range: "+avcWidthRange.getLower()+" - "+avcWidthRange.getUpper());
|
|
||||||
|
|
||||||
// If 720p is not reported as supported, ignore all results from this API
|
|
||||||
if (avcWidthRange.contains(1280)) {
|
|
||||||
if (avcWidthRange.contains(3840) && maxSupportedResW < 3840) {
|
|
||||||
maxSupportedResW = 3840;
|
|
||||||
}
|
|
||||||
else if (avcWidthRange.contains(1920) && maxSupportedResW < 1920) {
|
|
||||||
maxSupportedResW = 1920;
|
|
||||||
}
|
|
||||||
else if (maxSupportedResW < 1280) {
|
|
||||||
maxSupportedResW = 1280;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (hevcDecoder != null) {
|
|
||||||
Range<Integer> hevcWidthRange = hevcDecoder.getCapabilitiesForType("video/hevc").getVideoCapabilities().getSupportedWidths();
|
|
||||||
|
|
||||||
LimeLog.info("HEVC supported width range: "+hevcWidthRange.getLower()+" - "+hevcWidthRange.getUpper());
|
|
||||||
|
|
||||||
// If 720p is not reported as supported, ignore all results from this API
|
|
||||||
if (hevcWidthRange.contains(1280)) {
|
|
||||||
if (hevcWidthRange.contains(3840) && maxSupportedResW < 3840) {
|
|
||||||
maxSupportedResW = 3840;
|
|
||||||
}
|
|
||||||
else if (hevcWidthRange.contains(1920) && maxSupportedResW < 1920) {
|
|
||||||
maxSupportedResW = 1920;
|
|
||||||
}
|
|
||||||
else if (maxSupportedResW < 1280) {
|
|
||||||
maxSupportedResW = 1280;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
LimeLog.info("Maximum resolution slot: "+maxSupportedResW);
|
|
||||||
|
|
||||||
if (maxSupportedResW != 0) {
|
|
||||||
if (maxSupportedResW < 3840) {
|
|
||||||
// 4K is unsupported
|
|
||||||
removeValue(PreferenceConfiguration.RESOLUTION_PREF_STRING, "4K", new Runnable() {
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(SettingsFragment.this.getActivity());
|
|
||||||
setValue(PreferenceConfiguration.RESOLUTION_PREF_STRING, "1440p");
|
|
||||||
resetBitrateToDefault(prefs, null, null);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (maxSupportedResW < 2560) {
|
|
||||||
// 1440p is unsupported
|
|
||||||
removeValue(PreferenceConfiguration.RESOLUTION_PREF_STRING, "1440p", new Runnable() {
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(SettingsFragment.this.getActivity());
|
|
||||||
setValue(PreferenceConfiguration.RESOLUTION_PREF_STRING, "1080p");
|
|
||||||
resetBitrateToDefault(prefs, null, null);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (maxSupportedResW < 1920) {
|
|
||||||
// 1080p is unsupported
|
|
||||||
removeValue(PreferenceConfiguration.RESOLUTION_PREF_STRING, "1080p", new Runnable() {
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(SettingsFragment.this.getActivity());
|
|
||||||
setValue(PreferenceConfiguration.RESOLUTION_PREF_STRING, "720p");
|
|
||||||
resetBitrateToDefault(prefs, null, null);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
// Never remove 720p
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!PreferenceConfiguration.readPreferences(this.getActivity()).unlockFps) {
|
|
||||||
// We give some extra room in case the FPS is rounded down
|
|
||||||
if (maxSupportedFps < 118) {
|
|
||||||
removeValue(PreferenceConfiguration.FPS_PREF_STRING, "120", new Runnable() {
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(SettingsFragment.this.getActivity());
|
|
||||||
setValue(PreferenceConfiguration.FPS_PREF_STRING, "90");
|
|
||||||
resetBitrateToDefault(prefs, null, null);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (maxSupportedFps < 88) {
|
|
||||||
// 1080p is unsupported
|
|
||||||
removeValue(PreferenceConfiguration.FPS_PREF_STRING, "90", new Runnable() {
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(SettingsFragment.this.getActivity());
|
|
||||||
setValue(PreferenceConfiguration.FPS_PREF_STRING, "60");
|
|
||||||
resetBitrateToDefault(prefs, null, null);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
// Never remove 30 FPS or 60 FPS
|
|
||||||
}
|
|
||||||
|
|
||||||
// Android L introduces the drop duplicate behavior of releaseOutputBuffer()
|
|
||||||
// that the unlock FPS option relies on to not massively increase latency.
|
|
||||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
|
|
||||||
LimeLog.info("Excluding unlock FPS toggle based on OS");
|
|
||||||
PreferenceCategory category =
|
|
||||||
(PreferenceCategory) findPreference("category_basic_settings");
|
|
||||||
category.removePreference(findPreference("checkbox_unlock_fps"));
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
findPreference(PreferenceConfiguration.UNLOCK_FPS_STRING).setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
|
|
||||||
@Override
|
|
||||||
public boolean onPreferenceChange(Preference preference, Object newValue) {
|
|
||||||
// HACK: We need to let the preference change succeed before reinitializing to ensure
|
|
||||||
// it's reflected in the new layout.
|
|
||||||
final Handler h = new Handler();
|
|
||||||
h.postDelayed(new Runnable() {
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
// Ensure the activity is still open when this timeout expires
|
|
||||||
StreamSettings settingsActivity = (StreamSettings)SettingsFragment.this.getActivity();
|
|
||||||
if (settingsActivity != null) {
|
|
||||||
settingsActivity.reloadSettings();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, 500);
|
|
||||||
|
|
||||||
// Allow the original preference change to take place
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove HDR preference for devices below Nougat
|
|
||||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
|
|
||||||
LimeLog.info("Excluding HDR toggle based on OS");
|
|
||||||
PreferenceCategory category =
|
|
||||||
(PreferenceCategory) findPreference("category_advanced_settings");
|
|
||||||
category.removePreference(findPreference("checkbox_enable_hdr"));
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
Display display = getActivity().getWindowManager().getDefaultDisplay();
|
|
||||||
Display.HdrCapabilities hdrCaps = display.getHdrCapabilities();
|
|
||||||
|
|
||||||
// We must now ensure our display is compatible with HDR10
|
|
||||||
boolean foundHdr10 = false;
|
|
||||||
if (hdrCaps != null) {
|
|
||||||
// getHdrCapabilities() returns null on Lenovo Lenovo Mirage Solo (vega), Android 8.0
|
|
||||||
for (int hdrType : hdrCaps.getSupportedHdrTypes()) {
|
|
||||||
if (hdrType == Display.HdrCapabilities.HDR_TYPE_HDR10) {
|
|
||||||
foundHdr10 = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!foundHdr10) {
|
|
||||||
LimeLog.info("Excluding HDR toggle based on display capabilities");
|
|
||||||
PreferenceCategory category =
|
|
||||||
(PreferenceCategory) findPreference("category_advanced_settings");
|
|
||||||
category.removePreference(findPreference("checkbox_enable_hdr"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add a listener to the FPS and resolution preference
|
|
||||||
// so the bitrate can be auto-adjusted
|
|
||||||
findPreference(PreferenceConfiguration.RESOLUTION_PREF_STRING).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
|
|
||||||
resetBitrateToDefault(prefs, valueStr, null);
|
|
||||||
|
|
||||||
// Allow the original preference change to take place
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
findPreference(PreferenceConfiguration.FPS_PREF_STRING).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
|
|
||||||
resetBitrateToDefault(prefs, null, valueStr);
|
|
||||||
|
|
||||||
// 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,86 +0,0 @@
|
|||||||
package com.limelight.ui;
|
|
||||||
|
|
||||||
import android.annotation.TargetApi;
|
|
||||||
import android.content.Context;
|
|
||||||
import android.util.AttributeSet;
|
|
||||||
import android.view.KeyEvent;
|
|
||||||
import android.view.SurfaceView;
|
|
||||||
|
|
||||||
public class StreamView extends SurfaceView {
|
|
||||||
private double desiredAspectRatio;
|
|
||||||
private InputCallbacks inputCallbacks;
|
|
||||||
|
|
||||||
public void setDesiredAspectRatio(double aspectRatio) {
|
|
||||||
this.desiredAspectRatio = aspectRatio;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setInputCallbacks(InputCallbacks callbacks) {
|
|
||||||
this.inputCallbacks = callbacks;
|
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean onKeyPreIme(int keyCode, KeyEvent event) {
|
|
||||||
// This callbacks allows us to override dumb IME behavior like when
|
|
||||||
// Samsung's default keyboard consumes Shift+Space.
|
|
||||||
if (inputCallbacks != null) {
|
|
||||||
if (event.getAction() == KeyEvent.ACTION_DOWN) {
|
|
||||||
if (inputCallbacks.handleKeyDown(event)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (event.getAction() == KeyEvent.ACTION_UP) {
|
|
||||||
if (inputCallbacks.handleKeyUp(event)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return super.onKeyPreIme(keyCode, event);
|
|
||||||
}
|
|
||||||
|
|
||||||
public interface InputCallbacks {
|
|
||||||
boolean handleKeyUp(KeyEvent event);
|
|
||||||
boolean handleKeyDown(KeyEvent event);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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,9 +9,11 @@ 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 {
|
||||||
public static File openPath(boolean createPath, File root, String... path) {
|
private static File openPath(boolean createPath, File root, String... path) {
|
||||||
File f = root;
|
File f = root;
|
||||||
for (int i = 0; i < path.length; i++) {
|
for (int i = 0; i < path.length; i++) {
|
||||||
String component = path[i];
|
String component = path[i];
|
||||||
@@ -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,109 +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 Runnable runOnDismiss;
|
|
||||||
|
|
||||||
private AlertDialog alert;
|
private AlertDialog alert;
|
||||||
|
|
||||||
private static final ArrayList<Dialog> rundownDialogs = new ArrayList<>();
|
private static final ArrayList<Dialog> rundownDialogs = new ArrayList<Dialog>();
|
||||||
|
|
||||||
private Dialog(Activity activity, String title, String message, Runnable runOnDismiss)
|
public Dialog(Activity activity, String title, String message, boolean endAfterDismiss)
|
||||||
{
|
{
|
||||||
this.activity = activity;
|
this.activity = activity;
|
||||||
this.title = title;
|
this.title = title;
|
||||||
this.message = message;
|
this.message = message;
|
||||||
this.runOnDismiss = runOnDismiss;
|
this.endAfterDismiss = endAfterDismiss;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void closeDialogs()
|
public static void closeDialogs()
|
||||||
{
|
{
|
||||||
synchronized (rundownDialogs) {
|
synchronized (rundownDialogs) {
|
||||||
for (Dialog d : rundownDialogs) {
|
for (Dialog d : rundownDialogs) {
|
||||||
if (d.alert.isShowing()) {
|
if (d.alert.isShowing()) {
|
||||||
d.alert.dismiss();
|
d.alert.dismiss();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
rundownDialogs.clear();
|
rundownDialogs.clear();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void displayDialog(final Activity activity, String title, String message, final boolean endAfterDismiss)
|
public static void displayDialog(Activity activity, String title, String message, boolean endAfterDismiss)
|
||||||
{
|
{
|
||||||
activity.runOnUiThread(new Dialog(activity, title, message, new Runnable() {
|
activity.runOnUiThread(new Dialog(activity, title, message, endAfterDismiss));
|
||||||
@Override
|
}
|
||||||
public void run() {
|
|
||||||
if (endAfterDismiss) {
|
|
||||||
activity.finish();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void displayDialog(Activity activity, String title, String message, Runnable runOnDismiss)
|
@Override
|
||||||
{
|
public void run() {
|
||||||
activity.runOnUiThread(new Dialog(activity, title, message, runOnDismiss));
|
// If we're dying, don't bother creating a dialog
|
||||||
}
|
if (activity.isFinishing())
|
||||||
|
return;
|
||||||
|
|
||||||
@Override
|
alert = new AlertDialog.Builder(activity).create();
|
||||||
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.setTitle(title);
|
alert.setButton(AlertDialog.BUTTON_NEUTRAL, "OK", new DialogInterface.OnClickListener() {
|
||||||
alert.setMessage(message);
|
public void onClick(DialogInterface dialog, int which) {
|
||||||
alert.setCancelable(false);
|
synchronized (rundownDialogs) {
|
||||||
alert.setCanceledOnTouchOutside(false);
|
rundownDialogs.remove(Dialog.this);
|
||||||
|
alert.dismiss();
|
||||||
|
}
|
||||||
|
|
||||||
alert.setButton(AlertDialog.BUTTON_POSITIVE, activity.getResources().getText(android.R.string.ok), new DialogInterface.OnClickListener() {
|
if (endAfterDismiss) {
|
||||||
public void onClick(DialogInterface dialog, int which) {
|
activity.finish();
|
||||||
synchronized (rundownDialogs) {
|
}
|
||||||
rundownDialogs.remove(Dialog.this);
|
}
|
||||||
alert.dismiss();
|
});
|
||||||
}
|
|
||||||
|
|
||||||
runOnDismiss.run();
|
synchronized (rundownDialogs) {
|
||||||
}
|
rundownDialogs.add(this);
|
||||||
});
|
alert.show();
|
||||||
alert.setButton(AlertDialog.BUTTON_NEUTRAL, activity.getResources().getText(R.string.help), new DialogInterface.OnClickListener() {
|
}
|
||||||
public void onClick(DialogInterface dialog, int which) {
|
}
|
||||||
synchronized (rundownDialogs) {
|
|
||||||
rundownDialogs.remove(Dialog.this);
|
|
||||||
alert.dismiss();
|
|
||||||
}
|
|
||||||
|
|
||||||
runOnDismiss.run();
|
|
||||||
|
|
||||||
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,66 +0,0 @@
|
|||||||
package com.limelight.utils;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.content.pm.PackageManager;
|
|
||||||
import android.content.pm.ResolveInfo;
|
|
||||||
import android.net.Uri;
|
|
||||||
|
|
||||||
import com.limelight.HelpActivity;
|
|
||||||
|
|
||||||
public class HelpLauncher {
|
|
||||||
|
|
||||||
private static boolean isKnownBrowser(Context context, Intent i) {
|
|
||||||
ResolveInfo resolvedActivity = context.getPackageManager().resolveActivity(i, PackageManager.MATCH_DEFAULT_ONLY);
|
|
||||||
if (resolvedActivity == null) {
|
|
||||||
// No browser
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
String name = resolvedActivity.activityInfo.name;
|
|
||||||
if (name == null) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
name = name.toLowerCase();
|
|
||||||
return name.contains("chrome") || name.contains("firefox");
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void launchUrl(Context context, String url) {
|
|
||||||
// Try to launch the default browser
|
|
||||||
try {
|
|
||||||
Intent i = new Intent(Intent.ACTION_VIEW);
|
|
||||||
i.setData(Uri.parse(url));
|
|
||||||
|
|
||||||
// Several Android 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. We need to
|
|
||||||
// be a bit more clever on these devices and detect if the browser is a legitimate
|
|
||||||
// browser or just a fake error message activity.
|
|
||||||
if (!context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_LEANBACK) ||
|
|
||||||
isKnownBrowser(context, i)) {
|
|
||||||
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,109 +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 org.xmlpull.v1.XmlPullParserException;
|
|
||||||
|
|
||||||
import java.io.FileNotFoundException;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.net.UnknownHostException;
|
|
||||||
import java.security.cert.CertificateEncodingException;
|
|
||||||
|
|
||||||
public class ServerHelper {
|
|
||||||
public static String getCurrentAddressFromComputer(ComputerDetails computer) {
|
|
||||||
return computer.activeAddress;
|
|
||||||
}
|
|
||||||
|
|
||||||
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_APP_HDR, app.isHdrSupported());
|
|
||||||
intent.putExtra(Game.EXTRA_UNIQUEID, managerBinder.getUniqueId());
|
|
||||||
intent.putExtra(Game.EXTRA_PC_UUID, computer.uuid);
|
|
||||||
intent.putExtra(Game.EXTRA_PC_NAME, computer.name);
|
|
||||||
try {
|
|
||||||
if (computer.serverCert != null) {
|
|
||||||
intent.putExtra(Game.EXTRA_SERVER_CERT, computer.serverCert.getEncoded());
|
|
||||||
}
|
|
||||||
} catch (CertificateEncodingException e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
}
|
|
||||||
return intent;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void doStart(Activity parent, NvApp app, ComputerDetails computer,
|
|
||||||
ComputerManagerService.ComputerManagerBinder managerBinder) {
|
|
||||||
if (computer.state == ComputerDetails.State.OFFLINE ||
|
|
||||||
ServerHelper.getCurrentAddressFromComputer(computer) == null) {
|
|
||||||
Toast.makeText(parent, parent.getResources().getString(R.string.pair_pc_offline), Toast.LENGTH_SHORT).show();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
parent.startActivity(createStartIntent(parent, app, computer, managerBinder));
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void doQuit(final Activity parent,
|
|
||||||
final ComputerDetails computer,
|
|
||||||
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(ServerHelper.getCurrentAddressFromComputer(computer),
|
|
||||||
managerBinder.getUniqueId(), computer.serverCert, 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 (IOException | XmlPullParserException e) {
|
|
||||||
message = e.getMessage();
|
|
||||||
e.printStackTrace();
|
|
||||||
} 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,171 +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.Bitmap;
|
|
||||||
import android.graphics.drawable.Icon;
|
|
||||||
import android.os.Build;
|
|
||||||
|
|
||||||
import com.limelight.AppView;
|
|
||||||
import com.limelight.ShortcutTrampoline;
|
|
||||||
import com.limelight.R;
|
|
||||||
import com.limelight.nvstream.http.ComputerDetails;
|
|
||||||
import com.limelight.nvstream.http.NvApp;
|
|
||||||
|
|
||||||
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) {
|
|
||||||
if (getInfoForId(id) != 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, ShortcutTrampoline.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, forceAdd);
|
|
||||||
}
|
|
||||||
|
|
||||||
@TargetApi(Build.VERSION_CODES.O)
|
|
||||||
public boolean createPinnedGameShortcut(String id, Bitmap iconBits, String computerName, String computerUuid, String appName, String appId) {
|
|
||||||
if (sm.isRequestPinShortcutSupported()) {
|
|
||||||
Icon appIcon;
|
|
||||||
Intent i = new Intent(context, ShortcutTrampoline.class);
|
|
||||||
|
|
||||||
i.putExtra(AppView.NAME_EXTRA, computerName);
|
|
||||||
i.putExtra(AppView.UUID_EXTRA, computerUuid);
|
|
||||||
i.putExtra(ShortcutTrampoline.APP_ID_EXTRA, appId);
|
|
||||||
i.setAction(Intent.ACTION_DEFAULT);
|
|
||||||
|
|
||||||
if (iconBits != null) {
|
|
||||||
appIcon = Icon.createWithAdaptiveBitmap(iconBits);
|
|
||||||
} else {
|
|
||||||
appIcon = Icon.createWithResource(context, R.mipmap.ic_pc_scut);
|
|
||||||
}
|
|
||||||
|
|
||||||
ShortcutInfo sInfo = new ShortcutInfo.Builder(context, id)
|
|
||||||
.setIntent(i)
|
|
||||||
.setShortLabel(appName + " (" + computerName + ")")
|
|
||||||
.setIcon(appIcon)
|
|
||||||
.build();
|
|
||||||
|
|
||||||
return sm.requestPinShortcut(sInfo, null);
|
|
||||||
} else {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean createPinnedGameShortcut(String id, Bitmap iconBits, ComputerDetails cDetails, NvApp app) {
|
|
||||||
return createPinnedGameShortcut(id, iconBits, cDetails.name, cDetails.uuid, app.getAppName(), Integer.valueOf(app.getAppId()).toString());
|
|
||||||
}
|
|
||||||
|
|
||||||
public void disableShortcut(String id, CharSequence reason) {
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) {
|
|
||||||
if (getInfoForId(id) != 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<>();
|
private static final ArrayList<SpinnerDialog> rundownDialogs = new ArrayList<SpinnerDialog>();
|
||||||
|
|
||||||
private SpinnerDialog(Activity activity, String title, String message, boolean finish)
|
public SpinnerDialog(Activity activity, String title, String message, boolean finish)
|
||||||
{
|
{
|
||||||
this.activity = activity;
|
this.activity = activity;
|
||||||
this.title = title;
|
this.title = title;
|
||||||
this.message = message;
|
this.message = message;
|
||||||
this.progress = null;
|
this.progress = null;
|
||||||
this.finish = finish;
|
this.finish = finish;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static SpinnerDialog displayDialog(Activity activity, String title, String message, boolean finish)
|
public static SpinnerDialog displayDialog(Activity activity, String title, String message, boolean finish)
|
||||||
{
|
{
|
||||||
SpinnerDialog spinner = new SpinnerDialog(activity, title, message, finish);
|
SpinnerDialog spinner = new SpinnerDialog(activity, title, message, finish);
|
||||||
activity.runOnUiThread(spinner);
|
activity.runOnUiThread(spinner);
|
||||||
return spinner;
|
return spinner;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void closeDialogs(Activity activity)
|
public static void closeDialogs(Activity activity)
|
||||||
{
|
{
|
||||||
synchronized (rundownDialogs) {
|
synchronized (rundownDialogs) {
|
||||||
Iterator<SpinnerDialog> i = rundownDialogs.iterator();
|
Iterator<SpinnerDialog> i = rundownDialogs.iterator();
|
||||||
while (i.hasNext()) {
|
while (i.hasNext()) {
|
||||||
SpinnerDialog dialog = i.next();
|
SpinnerDialog dialog = i.next();
|
||||||
if (dialog.activity == activity) {
|
if (dialog.activity == activity) {
|
||||||
i.remove();
|
i.remove();
|
||||||
if (dialog.progress.isShowing()) {
|
if (dialog.progress.isShowing()) {
|
||||||
dialog.progress.dismiss();
|
dialog.progress.dismiss();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void dismiss()
|
public void dismiss()
|
||||||
{
|
{
|
||||||
// Running again with progress != null will destroy it
|
// Running again with progress != null will destroy it
|
||||||
activity.runOnUiThread(this);
|
activity.runOnUiThread(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setMessage(final String message)
|
public void setMessage(final String message)
|
||||||
{
|
{
|
||||||
activity.runOnUiThread(new Runnable() {
|
activity.runOnUiThread(new Runnable() {
|
||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
progress.setMessage(message);
|
progress.setMessage(message);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
|
|
||||||
// If we're dying, don't bother doing anything
|
// If we're dying, don't bother doing anything
|
||||||
if (activity.isFinishing()) {
|
if (activity.isFinishing()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (progress == null)
|
if (progress == null)
|
||||||
{
|
{
|
||||||
progress = new ProgressDialog(activity);
|
progress = new ProgressDialog(activity);
|
||||||
|
|
||||||
progress.setTitle(title);
|
progress.setTitle(title);
|
||||||
progress.setMessage(message);
|
progress.setMessage(message);
|
||||||
progress.setProgressStyle(ProgressDialog.STYLE_SPINNER);
|
progress.setProgressStyle(ProgressDialog.STYLE_SPINNER);
|
||||||
progress.setOnCancelListener(this);
|
progress.setOnCancelListener(this);
|
||||||
|
|
||||||
// If we want to finish the activity when this is killed, make it cancellable
|
// If we want to finish the activity when this is killed, make it cancellable
|
||||||
if (finish)
|
if (finish)
|
||||||
{
|
{
|
||||||
progress.setCancelable(true);
|
progress.setCancelable(true);
|
||||||
progress.setCanceledOnTouchOutside(false);
|
progress.setCanceledOnTouchOutside(false);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
progress.setCancelable(false);
|
progress.setCancelable(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
synchronized (rundownDialogs) {
|
synchronized (rundownDialogs) {
|
||||||
rundownDialogs.add(this);
|
rundownDialogs.add(this);
|
||||||
progress.show();
|
progress.show();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
synchronized (rundownDialogs) {
|
synchronized (rundownDialogs) {
|
||||||
if (rundownDialogs.remove(this) && progress.isShowing()) {
|
if (rundownDialogs.remove(this) && progress.isShowing()) {
|
||||||
progress.dismiss();
|
progress.dismiss();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onCancel(DialogInterface dialog) {
|
public void onCancel(DialogInterface dialog) {
|
||||||
synchronized (rundownDialogs) {
|
synchronized (rundownDialogs) {
|
||||||
rundownDialogs.remove(this);
|
rundownDialogs.remove(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
// This will only be called if finish was true, so we don't need to check again
|
// This will only be called if finish was true, so we don't need to check again
|
||||||
activity.finish();
|
activity.finish();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,21 +1,10 @@
|
|||||||
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.SharedPreferences;
|
|
||||||
import android.content.res.Configuration;
|
import android.content.res.Configuration;
|
||||||
import android.os.Build;
|
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.WindowManager;
|
|
||||||
|
|
||||||
import com.limelight.R;
|
|
||||||
import com.limelight.nvstream.http.ComputerDetails;
|
|
||||||
import com.limelight.preferences.PreferenceConfiguration;
|
|
||||||
|
|
||||||
import java.util.Locale;
|
|
||||||
|
|
||||||
public class UiHelper {
|
public class UiHelper {
|
||||||
|
|
||||||
@@ -23,28 +12,6 @@ public class UiHelper {
|
|||||||
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);
|
||||||
@@ -60,108 +27,5 @@ public class UiHelper {
|
|||||||
rootView.setPadding(horizontalPaddingPixels, verticalPaddingPixels,
|
rootView.setPadding(horizontalPaddingPixels, verticalPaddingPixels,
|
||||||
horizontalPaddingPixels, verticalPaddingPixels);
|
horizontalPaddingPixels, verticalPaddingPixels);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
|
||||||
// Allow this non-streaming activity to layout under notches.
|
|
||||||
//
|
|
||||||
// We should NOT do this for the Game activity unless
|
|
||||||
// the user specifically opts in, because it can obscure
|
|
||||||
// parts of the streaming surface.
|
|
||||||
activity.getWindow().getAttributes().layoutInDisplayCutoutMode =
|
|
||||||
WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void showDecoderCrashDialog(Activity activity) {
|
|
||||||
final SharedPreferences prefs = activity.getSharedPreferences("DecoderTombstone", 0);
|
|
||||||
final int crashCount = prefs.getInt("CrashCount", 0);
|
|
||||||
int lastNotifiedCrashCount = prefs.getInt("LastNotifiedCrashCount", 0);
|
|
||||||
|
|
||||||
// Remember the last crash count we notified at, so we don't
|
|
||||||
// display the crash dialog every time the app is started until
|
|
||||||
// they stream again
|
|
||||||
if (crashCount != 0 && crashCount != lastNotifiedCrashCount) {
|
|
||||||
if (crashCount % 3 == 0) {
|
|
||||||
// At 3 consecutive crashes, we'll forcefully reset their settings
|
|
||||||
PreferenceConfiguration.resetStreamingSettings(activity);
|
|
||||||
Dialog.displayDialog(activity,
|
|
||||||
activity.getResources().getString(R.string.title_decoding_reset),
|
|
||||||
activity.getResources().getString(R.string.message_decoding_reset),
|
|
||||||
new Runnable() {
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
// Mark notification as acknowledged on dismissal
|
|
||||||
prefs.edit().putInt("LastNotifiedCrashCount", crashCount).apply();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
Dialog.displayDialog(activity,
|
|
||||||
activity.getResources().getString(R.string.title_decoding_error),
|
|
||||||
activity.getResources().getString(R.string.message_decoding_error),
|
|
||||||
new Runnable() {
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
// Mark notification as acknowledged on dismissal
|
|
||||||
prefs.edit().putInt("LastNotifiedCrashCount", crashCount).apply();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void displayDeletePcConfirmationDialog(Activity parent, ComputerDetails computer, 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.delete_pc_msg))
|
|
||||||
.setTitle(computer.name)
|
|
||||||
.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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,4 +1,9 @@
|
|||||||
# Application.mk for Moonlight
|
# Application.mk for Limelight
|
||||||
|
|
||||||
# Our minimum version is Android 4.1
|
# Our minimum version is Android 4.1
|
||||||
APP_PLATFORM := android-16
|
APP_PLATFORM := android-16
|
||||||
|
|
||||||
|
APP_ABI := armeabi-v7a arm64-v8a x86 x86_64 mips #mips64
|
||||||
|
|
||||||
|
# We want an optimized build
|
||||||
|
APP_OPTIM := release
|
||||||
@@ -5,32 +5,9 @@ include $(call all-subdir-makefiles)
|
|||||||
|
|
||||||
LOCAL_PATH := $(MY_LOCAL_PATH)
|
LOCAL_PATH := $(MY_LOCAL_PATH)
|
||||||
|
|
||||||
# Only build evdev_reader for the rooted APK flavor
|
include $(CLEAR_VARS)
|
||||||
ifeq (root,$(PRODUCT_FLAVOR))
|
LOCAL_MODULE := evdev_reader
|
||||||
include $(CLEAR_VARS)
|
LOCAL_SRC_FILES := evdev_reader.c
|
||||||
LOCAL_MODULE := evdev_reader
|
LOCAL_LDLIBS := -llog
|
||||||
LOCAL_SRC_FILES := evdev_reader.c
|
|
||||||
LOCAL_LDLIBS := -llog
|
|
||||||
|
|
||||||
|
|
||||||
# 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
|
|
||||||
endif
|
|
||||||
|
|
||||||
|
include $(BUILD_SHARED_LIBRARY)
|
||||||
@@ -1,54 +1,41 @@
|
|||||||
#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;
|
||||||
|
|
||||||
#define REL_X 0x00
|
path = (*env)->GetStringUTFChars(env, absolutePath, NULL);
|
||||||
#define REL_Y 0x01
|
|
||||||
#define KEY_Q 16
|
|
||||||
#define BTN_LEFT 0x110
|
|
||||||
#define BTN_GAMEPAD 0x130
|
|
||||||
|
|
||||||
struct DeviceEntry {
|
return open(path, O_RDWR);
|
||||||
struct DeviceEntry *next;
|
}
|
||||||
pthread_t thread;
|
|
||||||
int fd;
|
|
||||||
char devName[128];
|
|
||||||
};
|
|
||||||
|
|
||||||
static struct DeviceEntry *DeviceListHead;
|
JNIEXPORT jboolean JNICALL
|
||||||
static int grabbing = 1;
|
Java_com_limelight_binding_input_evdev_EvdevReader_grab(JNIEnv *env, jobject this, jint fd) {
|
||||||
static pthread_mutex_t DeviceListLock = PTHREAD_MUTEX_INITIALIZER;
|
return ioctl(fd, EVIOCGRAB, 1) == 0;
|
||||||
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
|
JNIEXPORT jboolean JNICALL
|
||||||
// devices and writes the evdev output packets to a socket. This allows
|
Java_com_limelight_binding_input_evdev_EvdevReader_ungrab(JNIEnv *env, jobject this, jint fd) {
|
||||||
// Moonlight to read input devices without having to muck with changing
|
return ioctl(fd, EVIOCGRAB, 0) == 0;
|
||||||
// device permissions or modifying SELinux policy (which is prevented in
|
}
|
||||||
// Marshmallow anyway).
|
|
||||||
|
// has*() and friends are based on Android's EventHub.cpp
|
||||||
|
|
||||||
#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);
|
||||||
@@ -56,7 +43,17 @@ static int hasRelAxis(int fd, short axis) {
|
|||||||
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);
|
||||||
@@ -64,349 +61,58 @@ static int hasKey(int fd, short key) {
|
|||||||
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;
|
|
||||||
|
|
||||||
__android_log_print(ANDROID_LOG_INFO, "EvdevReader", "Entered main()");
|
data = (*env)->GetByteArrayElements(env, buffer, NULL);
|
||||||
|
if (data == NULL) {
|
||||||
port = atoi(argv[1]);
|
__android_log_print(ANDROID_LOG_ERROR, "EvdevReader",
|
||||||
__android_log_print(ANDROID_LOG_INFO, "EvdevReader", "Requested port number: %d", port);
|
"Failed to get byte array");
|
||||||
|
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
|
if (pollres > 0 && (pollinfo.revents & POLLIN)) {
|
||||||
for (;;) {
|
// We'll have data available now
|
||||||
unsigned char requestId;
|
ret = read(fd, data, sizeof(struct input_event));
|
||||||
|
if (ret < 0) {
|
||||||
do {
|
__android_log_print(ANDROID_LOG_ERROR, "EvdevReader",
|
||||||
// Every second we poll again for new devices if
|
"read() failed: %d", errno);
|
||||||
// we haven't received any new events
|
|
||||||
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 {
|
||||||
|
// There must have been a failure
|
||||||
|
ret = -1;
|
||||||
|
|
||||||
if (pollres > 0 && (pollinfo.revents & POLLIN)) {
|
if (pollres < 0) {
|
||||||
// We'll have data available now
|
__android_log_print(ANDROID_LOG_ERROR, "EvdevReader",
|
||||||
ret = recv(sock, &requestId, sizeof(requestId), 0);
|
"poll() failed: %d", errno);
|
||||||
if (ret < sizeof(requestId)) {
|
|
||||||
__android_log_print(ANDROID_LOG_ERROR, "EvdevReader", "Short read on socket");
|
|
||||||
return 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)
|
||||||
Loaded 100 of 582 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user