Compare commits
111 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e8c4df4897 | |||
| 5ee16124bc | |||
| 8702ac72f0 | |||
| 004552ec30 | |||
| 2f28400234 | |||
| 78d213d686 | |||
| 1a71dda243 | |||
| 21822f259c | |||
| 4f79607015 | |||
| d8576d4c50 | |||
| 2f4042da8f | |||
| c1397e331b | |||
| cd182b3265 | |||
| 28f2d7b84a | |||
| e8de7908fd | |||
| 419c4c5592 | |||
| a9a8346f58 | |||
| 7e1b3f861f | |||
| f4204e1268 | |||
| 60f35cd0aa | |||
| bbcdaa94a1 | |||
| 8f6e8c00ef | |||
| 24cb347b10 | |||
| d1b4e9464f | |||
| 18f7bfab7f | |||
| d84b4bcf9a | |||
| 57d919798a | |||
| efeeebb0a2 | |||
| bc1409ba6c | |||
| 6f05b2af8a | |||
| d441bef33e | |||
| 7b1f6ee483 | |||
| 332960922a | |||
| ac03f73cf9 | |||
| fa847ef2fc | |||
| b19360ac75 | |||
| 562569dc6b | |||
| a18aa51f5a | |||
| b492ac43f8 | |||
| 5bf3efb247 | |||
| 2d833c32b0 | |||
| 19bade01b8 | |||
| 1b991ba432 | |||
| 9c48850bb7 | |||
| 3e6f5ff11c | |||
| 57c3d8af8b | |||
| 8530451c8b | |||
| 69a5c0b5b3 | |||
| a7c36dcde6 | |||
| dff6fc21f4 | |||
| 895e0250d9 | |||
| fd538cbaff | |||
| 27ce6fa203 | |||
| 8efe194682 | |||
| 947882d16f | |||
| f07e927103 | |||
| a61b85b494 | |||
| 0c4a049a80 | |||
| 8403101d0f | |||
| 47d47afd73 | |||
| 1430801888 | |||
| 6efc7e254b | |||
| ace1339811 | |||
| a5171a1701 | |||
| a50211ab95 | |||
| 31677adaa0 | |||
| 247a19766c | |||
| 1f09cbd609 | |||
| 731e4dc31e | |||
| b6ee0764ff | |||
| cd4bf9a28b | |||
| f56b7ff79e | |||
| 645ea683ee | |||
| 67e22fca6b | |||
| a726ba8ea7 | |||
| 23fcaa1bab | |||
| ad684a6f6b | |||
| d3438f4938 | |||
| cafdc21bf2 | |||
| ceb9bd3342 | |||
| 196c0e6cbc | |||
| e2cb7c953c | |||
| 426b3c8522 | |||
| 9648cf257f | |||
| 31d8687f67 | |||
| 991407a2cf | |||
| 13b80eda8a | |||
| 6677949614 | |||
| 080dcd92d7 | |||
| 31b0bcf041 | |||
| 36664133f8 | |||
| a3106bffca | |||
| 94a26fb831 | |||
| 1b026f1354 | |||
| 0517e8a530 | |||
| a9fea34ac1 | |||
| 201704dc9d | |||
| 62ecb1af50 | |||
| 9d4ca6293f | |||
| 2296b80edb | |||
| 5577d48dcf | |||
| e92a281fd8 | |||
| b4c3f9678a | |||
| 82f79c466a | |||
| d428f316b4 | |||
| 828f4877b6 | |||
| 09e8e8e6b3 | |||
| 77c8051ec6 | |||
| 6bae056e3a | |||
| bb869a51fd | |||
| 25b3d08bb9 |
@@ -1,9 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<classpath>
|
||||
<classpathentry kind="src" path="src"/>
|
||||
<classpathentry kind="src" path="gen"/>
|
||||
<classpathentry kind="con" path="com.android.ide.eclipse.adt.ANDROID_FRAMEWORK"/>
|
||||
<classpathentry exported="true" kind="con" path="com.android.ide.eclipse.adt.LIBRARIES"/>
|
||||
<classpathentry exported="true" kind="con" path="com.android.ide.eclipse.adt.DEPENDENCIES"/>
|
||||
<classpathentry kind="output" path="bin/classes"/>
|
||||
</classpath>
|
||||
+35
-1
@@ -1 +1,35 @@
|
||||
/bin/*
|
||||
#built application files
|
||||
*.apk
|
||||
*.ap_
|
||||
|
||||
# files for the dex VM
|
||||
*.dex
|
||||
|
||||
# Java class files
|
||||
*.class
|
||||
|
||||
# generated files
|
||||
bin/
|
||||
gen/
|
||||
|
||||
# Local configuration file (sdk path, etc)
|
||||
local.properties
|
||||
|
||||
# Windows thumbnail db
|
||||
Thumbs.db
|
||||
|
||||
# OSX files
|
||||
.DS_Store
|
||||
|
||||
# Eclipse project files
|
||||
.classpath
|
||||
.project
|
||||
|
||||
# Android Studio
|
||||
.idea
|
||||
#.idea/workspace.xml - remove # and delete .idea if it better suit your needs.
|
||||
.gradle
|
||||
build/
|
||||
|
||||
# Compiled JNI libraries folder
|
||||
**/jniLibs
|
||||
@@ -1,2 +0,0 @@
|
||||
*** SESSION Sep 21, 2013 18:55:11.17 -------------------------------------------
|
||||
*** SESSION Sep 21, 2013 18:55:55.08 -------------------------------------------
|
||||
@@ -1 +0,0 @@
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
|
||||
Binary file not shown.
@@ -1 +0,0 @@
|
||||
|
||||
Binary file not shown.
Binary file not shown.
-3
@@ -1,3 +0,0 @@
|
||||
com.android.ide.eclipse.adt.fixLegacyEditors=1
|
||||
com.android.ide.eclipse.adt.sdk=C\:\\Users\\Andrew\\Desktop\\ADT\\adt-bundle-windows-x86_64-20130917\\sdk
|
||||
eclipse.preferences.version=1
|
||||
@@ -1,4 +0,0 @@
|
||||
eclipse.preferences.version=1
|
||||
spelling_locale_initialized=true
|
||||
useAnnotationsPrefPage=true
|
||||
useQuickDiffPrefPage=true
|
||||
@@ -1,2 +0,0 @@
|
||||
eclipse.preferences.version=1
|
||||
version=1
|
||||
@@ -1,13 +0,0 @@
|
||||
content_assist_proposals_background=255,255,255
|
||||
content_assist_proposals_foreground=0,0,0
|
||||
eclipse.preferences.version=1
|
||||
fontPropagated=true
|
||||
org.eclipse.jdt.ui.editor.tab.width=
|
||||
org.eclipse.jdt.ui.formatterprofiles.version=12
|
||||
org.eclipse.jdt.ui.javadoclocations.migrated=true
|
||||
org.eclipse.jface.textfont=1|Courier New|10.0|0|WINDOWS|1|0|0|0|0|0|0|0|0|1|0|0|0|0|Courier New;
|
||||
proposalOrderMigrated=true
|
||||
spelling_locale_initialized=true
|
||||
tabWidthPropagated=true
|
||||
useAnnotationsPrefPage=true
|
||||
useQuickDiffPrefPage=true
|
||||
@@ -1,5 +0,0 @@
|
||||
PROBLEMS_FILTERS_MIGRATE=true
|
||||
eclipse.preferences.version=1
|
||||
platformState=1379804095671
|
||||
quickStart=false
|
||||
tipsAndTricks=true
|
||||
@@ -1,2 +0,0 @@
|
||||
eclipse.preferences.version=1
|
||||
showIntro=false
|
||||
File diff suppressed because it is too large
Load Diff
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,2 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<typeInfoHistroy/>
|
||||
@@ -1,2 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<qualifiedTypeNameHistroy/>
|
||||
@@ -1,12 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<section name="Workbench">
|
||||
<section name="org.eclipse.jdt.internal.ui.packageview.PackageExplorerPart">
|
||||
<item value="true" key="group_libraries"/>
|
||||
<item value="false" key="linkWithEditor"/>
|
||||
<item value="2" key="layout"/>
|
||||
<item value="1" key="rootMode"/>
|
||||
<item value="<?xml version="1.0" encoding="UTF-8"?>
<packageExplorer group_libraries="1" layout="2" linkWithEditor="0" rootMode="1" workingSetName="">
<customFilters userDefinedPatternsEnabled="false">
<xmlDefinedFilters>
<child filterId="org.eclipse.jdt.ui.PackageExplorer.LibraryFilter" isEnabled="false"/>
<child filterId="org.eclipse.jdt.ui.PackageExplorer.LocalTypesFilter" isEnabled="false"/>
<child filterId="org.eclipse.jdt.ui.PackageExplorer.StaticsFilter" isEnabled="false"/>
<child filterId="org.eclipse.jdt.ui.PackageExplorer.ClosedProjectsFilter" isEnabled="false"/>
<child filterId="org.eclipse.jdt.ui.PackageExplorer.NonSharedProjectsFilter" isEnabled="false"/>
<child filterId="org.eclipse.jdt.ui.PackageExplorer.NonJavaElementFilter" isEnabled="false"/>
<child filterId="org.eclipse.jdt.ui.PackageExplorer.ContainedLibraryFilter" isEnabled="false"/>
<child filterId="org.eclipse.jdt.ui.PackageExplorer.CuAndClassFileFilter" isEnabled="false"/>
<child filterId="org.eclipse.jdt.ui.PackageExplorer.NonJavaProjectsFilter" isEnabled="false"/>
<child filterId="org.eclipse.jdt.internal.ui.PackageExplorer.EmptyInnerPackageFilter" isEnabled="true"/>
<child filterId="org.eclipse.jdt.ui.PackageExplorer.PackageDeclarationFilter" isEnabled="true"/>
<child filterId="org.eclipse.jdt.internal.ui.PackageExplorer.EmptyPackageFilter" isEnabled="false"/>
<child filterId="org.eclipse.jdt.ui.PackageExplorer.ImportDeclarationFilter" isEnabled="true"/>
<child filterId="org.eclipse.jdt.ui.PackageExplorer.FieldsFilter" isEnabled="false"/>
<child filterId="org.eclipse.jdt.internal.ui.PackageExplorer.HideInnerClassFilesFilter" isEnabled="true"/>
<child filterId="org.eclipse.jdt.ui.PackageExplorer.NonPublicFilter" isEnabled="false"/>
<child filterId="org.eclipse.jdt.ui.PackageExplorer_patternFilterId_.*" isEnabled="true"/>
<child filterId="org.eclipse.jdt.ui.PackageExplorer.EmptyLibraryContainerFilter" isEnabled="true"/>
<child filterId="org.eclipse.jdt.ui.PackageExplorer.SyntheticMembersFilter" isEnabled="true"/>
</xmlDefinedFilters>
</customFilters>
</packageExplorer>" key="memento"/>
|
||||
</section>
|
||||
<section name="JavaElementSearchActions">
|
||||
</section>
|
||||
</section>
|
||||
@@ -1,15 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<section name="Workbench">
|
||||
<section name="ChooseWorkspaceDialogSettings">
|
||||
<item value="185" key="DIALOG_Y_ORIGIN"/>
|
||||
<item value="381" key="DIALOG_X_ORIGIN"/>
|
||||
</section>
|
||||
<section name="WORKBENCH_SETTINGS">
|
||||
<list key="ENABLED_TRANSFERS">
|
||||
</list>
|
||||
</section>
|
||||
<section name="ExternalProjectImportWizard">
|
||||
<item value="false" key="WizardProjectsImportPage.STORE_ARCHIVE_SELECTED"/>
|
||||
<item value="false" key="WizardProjectsImportPage.STORE_COPY_PROJECT_ID"/>
|
||||
</section>
|
||||
</section>
|
||||
@@ -1,17 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<section name="Workbench">
|
||||
<section name="org.eclipse.ui.internal.QuickAccess">
|
||||
<item value="1025" key="dialogWidth"/>
|
||||
<item value="525" key="dialogHeight"/>
|
||||
<list key="orderedProviders">
|
||||
</list>
|
||||
<list key="textArray">
|
||||
</list>
|
||||
<list key="orderedElements">
|
||||
</list>
|
||||
<list key="textEntries">
|
||||
</list>
|
||||
</section>
|
||||
<section name="ImportExportAction">
|
||||
</section>
|
||||
</section>
|
||||
@@ -1,5 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<workingSetManager>
|
||||
<workingSet aggregate="true" factoryID="org.eclipse.ui.internal.WorkingSetFactory" id="1379804109849_0" label="Window Working Set" name="Aggregate for window 1379804109848"/>
|
||||
<workingSet aggregate="true" factoryID="org.eclipse.ui.internal.WorkingSetFactory" id="1379804153983_1" label="Window Working Set" name="Aggregate for window 1379804153983"/>
|
||||
</workingSetManager>
|
||||
@@ -1 +0,0 @@
|
||||
org.eclipse.core.runtime=1
|
||||
@@ -1,33 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<projectDescription>
|
||||
<name>Limelight</name>
|
||||
<comment></comment>
|
||||
<projects>
|
||||
</projects>
|
||||
<buildSpec>
|
||||
<buildCommand>
|
||||
<name>com.android.ide.eclipse.adt.ResourceManagerBuilder</name>
|
||||
<arguments>
|
||||
</arguments>
|
||||
</buildCommand>
|
||||
<buildCommand>
|
||||
<name>com.android.ide.eclipse.adt.PreCompilerBuilder</name>
|
||||
<arguments>
|
||||
</arguments>
|
||||
</buildCommand>
|
||||
<buildCommand>
|
||||
<name>org.eclipse.jdt.core.javabuilder</name>
|
||||
<arguments>
|
||||
</arguments>
|
||||
</buildCommand>
|
||||
<buildCommand>
|
||||
<name>com.android.ide.eclipse.adt.ApkBuilder</name>
|
||||
<arguments>
|
||||
</arguments>
|
||||
</buildCommand>
|
||||
</buildSpec>
|
||||
<natures>
|
||||
<nature>com.android.ide.eclipse.adt.AndroidNature</nature>
|
||||
<nature>org.eclipse.jdt.core.javanature</nature>
|
||||
</natures>
|
||||
</projectDescription>
|
||||
@@ -1,11 +0,0 @@
|
||||
eclipse.preferences.version=1
|
||||
org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled
|
||||
org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.6
|
||||
org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve
|
||||
org.eclipse.jdt.core.compiler.compliance=1.6
|
||||
org.eclipse.jdt.core.compiler.debug.lineNumber=generate
|
||||
org.eclipse.jdt.core.compiler.debug.localVariable=generate
|
||||
org.eclipse.jdt.core.compiler.debug.sourceFile=generate
|
||||
org.eclipse.jdt.core.compiler.problem.assertIdentifier=error
|
||||
org.eclipse.jdt.core.compiler.problem.enumIdentifier=error
|
||||
org.eclipse.jdt.core.compiler.source=1.6
|
||||
@@ -1,12 +1,12 @@
|
||||
#Limelight
|
||||
|
||||
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.
|
||||
|
||||
Limelight will allow you to stream your full collection of games from your Windows PC to your Android device,
|
||||
in your own home, or over the internet.
|
||||
whether in your own home or over the internet.
|
||||
|
||||
[Limelight-pc](https://github.com/limelight-stream/limelight-pc) is also currently in development for Windows, OS X and Linux. Versions for [iOS](https://github.com/limelight-stream/limelight-ios) and [Windows Phone](https://github.com/limelight-stream/limelight-wp) are also in development.
|
||||
[Limelight-pc](https://github.com/limelight-stream/limelight-pc) is also currently in development for Windows, OS X and Linux. Versions for [iOS](https://github.com/limelight-stream/limelight-ios) and [Windows and Windows Phone](https://github.com/limelight-stream/limelight-windows) are also in development.
|
||||
|
||||
##Features
|
||||
|
||||
@@ -14,9 +14,6 @@ in your own home, or over the internet.
|
||||
* Full gamepad support for MOGA, Xbox 360, PS3, OUYA, and Shield
|
||||
* Automatically finds GameStream-compatible PCs on your network
|
||||
|
||||
##Features in development
|
||||
* Keyboard input
|
||||
|
||||
##Installation
|
||||
|
||||
* Download and install Limelight for Android from
|
||||
@@ -28,7 +25,6 @@ in your own home, or over the internet.
|
||||
* [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
|
||||
* High-end wireless router (802.11n dual-band recommended)
|
||||
* Exynos/Snapdragon SoC __OR__ Quad-Core 1.4 GHz Cortex-A9 or higher (Tegra 3)
|
||||
|
||||
##Usage
|
||||
|
||||
|
||||
+112
@@ -0,0 +1,112 @@
|
||||
<?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$/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$/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="jmdns-fixed" level="project" />
|
||||
<orderEntry type="library" exported="" name="jcodec-0.1.6-3" level="project" />
|
||||
<orderEntry type="library" exported="" name="bcpkix-jdk15on-1.51" level="project" />
|
||||
<orderEntry type="library" exported="" name="tinyrtsp" level="project" />
|
||||
<orderEntry type="library" exported="" name="limelight-common" level="project" />
|
||||
</component>
|
||||
</module>
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
import com.android.builder.model.ProductFlavor
|
||||
import org.apache.tools.ant.taskdefs.condition.Os
|
||||
|
||||
apply plugin: 'com.android.application'
|
||||
|
||||
android {
|
||||
compileSdkVersion 21
|
||||
buildToolsVersion "21.0.2"
|
||||
|
||||
defaultConfig {
|
||||
minSdkVersion 16
|
||||
targetSdkVersion 21
|
||||
|
||||
versionName "2.9"
|
||||
versionCode = 38
|
||||
}
|
||||
|
||||
productFlavors {
|
||||
root {
|
||||
applicationId "com.limelight.root"
|
||||
}
|
||||
|
||||
nonRoot {
|
||||
applicationId "com.limelight"
|
||||
}
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
runProguard false
|
||||
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt'
|
||||
}
|
||||
}
|
||||
|
||||
sourceSets.main.jni.srcDirs = []
|
||||
|
||||
//noinspection GroovyAssignabilityCheck,GroovyAssignabilityCheck
|
||||
task ndkBuild(type: Exec, description: 'Compile JNI source via NDK') {
|
||||
Properties properties = new Properties()
|
||||
properties.load(project.rootProject.file('local.properties').newDataInputStream())
|
||||
def ndkDir = properties.getProperty('ndk.dir')
|
||||
|
||||
if (Os.isFamily(Os.FAMILY_WINDOWS)) {
|
||||
commandLine "$ndkDir\\ndk-build.cmd",
|
||||
'NDK_PROJECT_PATH=build/intermediates/ndk',
|
||||
'NDK_LIBS_OUT=src/main/jniLibs',
|
||||
'APP_BUILD_SCRIPT=src/main/jni/Android.mk',
|
||||
'NDK_APPLICATION_MK=src/main/jni/Application.mk'
|
||||
}
|
||||
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 {
|
||||
compile group: 'org.jcodec', name: 'jcodec', version: '0.1.6-3'
|
||||
compile group: 'org.bouncycastle', name: 'bcprov-jdk15on', version: '1.51'
|
||||
compile group: 'org.bouncycastle', name: 'bcpkix-jdk15on', version: '1.51'
|
||||
compile files('libs/jmdns-fixed.jar')
|
||||
compile files('libs/limelight-common.jar')
|
||||
compile files('libs/tinyrtsp.jar')
|
||||
}
|
||||
Binary file not shown.
@@ -1,12 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.limelight"
|
||||
android:versionCode="32"
|
||||
android:versionName="2.5.4" >
|
||||
|
||||
<uses-sdk
|
||||
android:minSdkVersion="16"
|
||||
android:targetSdkVersion="19" />
|
||||
package="com.limelight">
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
@@ -16,67 +10,70 @@
|
||||
|
||||
<uses-feature android:name="android.hardware.touchscreen" android:required="false" />
|
||||
<uses-feature android:name="android.hardware.wifi" android:required="false" />
|
||||
<uses-feature android:name="android.hardware.gamepad" android:required="false" />
|
||||
|
||||
<application
|
||||
android:allowBackup="true"
|
||||
android:icon="@drawable/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:theme="@style/AppTheme" >
|
||||
|
||||
<!-- Launcher for traditional devices -->
|
||||
<activity
|
||||
android:name="com.limelight.PcView"
|
||||
android:configChanges="mcc|mnc|locale|touchscreen|keyboard|keyboardHidden|navigation|screenLayout|fontScale|uiMode|orientation|screenSize|smallestScreenSize|layoutDirection"
|
||||
android:label="@string/app_name" >
|
||||
android:name=".PcView"
|
||||
android:configChanges="mcc|mnc|locale|touchscreen|keyboard|keyboardHidden|navigation|screenLayout|fontScale|uiMode|orientation|screenSize|smallestScreenSize|layoutDirection">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
<category android:name="tv.ouya.intent.category.APP" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<!-- Launcher for Android TV devices -->
|
||||
<activity
|
||||
android:name="com.limelight.AppView"
|
||||
android:configChanges="mcc|mnc|locale|touchscreen|keyboard|keyboardHidden|navigation|screenLayout|fontScale|uiMode|orientation|screenSize|smallestScreenSize|layoutDirection"
|
||||
android:label="App List" >
|
||||
android:name=".PcViewTv"
|
||||
android:logo="@drawable/atv_banner"
|
||||
android:icon="@drawable/atv_banner"
|
||||
android:configChanges="mcc|mnc|locale|touchscreen|keyboard|keyboardHidden|navigation|screenLayout|fontScale|uiMode|orientation|screenSize|smallestScreenSize|layoutDirection">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:name=".AppView"
|
||||
android:configChanges="mcc|mnc|locale|touchscreen|keyboard|keyboardHidden|navigation|screenLayout|fontScale|uiMode|orientation|screenSize|smallestScreenSize|layoutDirection" >
|
||||
<meta-data
|
||||
android:name="android.support.PARENT_ACTIVITY"
|
||||
android:value="com.limelight.PcView" />
|
||||
</activity>
|
||||
<activity
|
||||
android:name="com.limelight.StreamSettings"
|
||||
android:name=".preferences.StreamSettings"
|
||||
android:label="Streaming Settings" >
|
||||
<meta-data
|
||||
android:name="android.support.PARENT_ACTIVITY"
|
||||
android:value="com.limelight.PcView" />
|
||||
</activity>
|
||||
<activity
|
||||
android:name="com.limelight.AdvancedSettings"
|
||||
android:label="Advanced Settings" >
|
||||
<meta-data
|
||||
android:name="android.support.PARENT_ACTIVITY"
|
||||
android:value="com.limelight.StreamSettings" />
|
||||
</activity>
|
||||
<activity
|
||||
android:name="com.limelight.AddComputerManually"
|
||||
android:name=".preferences.AddComputerManually"
|
||||
android:label="Add Computer Manually" >
|
||||
<meta-data
|
||||
android:name="android.support.PARENT_ACTIVITY"
|
||||
android:value="com.limelight.PcView" />
|
||||
</activity>
|
||||
<activity
|
||||
android:name="com.limelight.Game"
|
||||
android:name=".Game"
|
||||
android:screenOrientation="sensorLandscape"
|
||||
android:configChanges="mcc|mnc|locale|touchscreen|keyboard|keyboardHidden|navigation|screenLayout|fontScale|uiMode|orientation|screenSize|smallestScreenSize|layoutDirection"
|
||||
android:label="@string/title_activity_game"
|
||||
android:parentActivityName="com.limelight.Connection"
|
||||
android:theme="@style/FullscreenTheme" >
|
||||
android:configChanges="mcc|mnc|locale|touchscreen|keyboard|keyboardHidden|navigation|screenLayout|fontScale|uiMode|orientation|screenSize|smallestScreenSize|layoutDirection" >
|
||||
<meta-data
|
||||
android:name="android.support.PARENT_ACTIVITY"
|
||||
android:value="com.limelight.Connection" />
|
||||
</activity>
|
||||
<service
|
||||
android:name="com.limelight.discovery.DiscoveryService"
|
||||
android:name=".discovery.DiscoveryService"
|
||||
android:label="mDNS PC Auto-Discovery Service" />
|
||||
<service
|
||||
android:name="com.limelight.computers.ComputerManagerService"
|
||||
android:name=".computers.ComputerManagerService"
|
||||
android:label="Computer Management Service" />
|
||||
</application>
|
||||
|
||||
@@ -12,6 +12,7 @@ import com.limelight.binding.PlatformBinding;
|
||||
import com.limelight.nvstream.http.GfeHttpResponseException;
|
||||
import com.limelight.nvstream.http.NvApp;
|
||||
import com.limelight.nvstream.http.NvHTTP;
|
||||
import com.limelight.R;
|
||||
import com.limelight.utils.Dialog;
|
||||
import com.limelight.utils.SpinnerDialog;
|
||||
|
||||
@@ -27,6 +28,7 @@ import android.widget.AdapterView;
|
||||
import android.widget.AdapterView.OnItemClickListener;
|
||||
import android.widget.ArrayAdapter;
|
||||
import android.widget.ListView;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
import android.widget.AdapterView.AdapterContextMenuInfo;
|
||||
|
||||
@@ -58,7 +60,10 @@ public class AppView extends Activity {
|
||||
return;
|
||||
}
|
||||
|
||||
setTitle("App List for "+getIntent().getStringExtra(NAME_EXTRA));
|
||||
String labelText = "App List for "+getIntent().getStringExtra(NAME_EXTRA);
|
||||
TextView label = (TextView) findViewById(R.id.appListText);
|
||||
setTitle(labelText);
|
||||
label.setText(labelText);
|
||||
|
||||
try {
|
||||
ipAddress = InetAddress.getByAddress(address);
|
||||
@@ -76,7 +81,7 @@ public class AppView extends Activity {
|
||||
@Override
|
||||
public void onItemClick(AdapterView<?> arg0, View arg1, int pos,
|
||||
long id) {
|
||||
AppObject app = (AppObject) appListAdapter.getItem(pos);
|
||||
AppObject app = appListAdapter.getItem(pos);
|
||||
if (app == null || app.app == null) {
|
||||
return;
|
||||
}
|
||||
@@ -129,7 +134,7 @@ public class AppView extends Activity {
|
||||
super.onCreateContextMenu(menu, v, menuInfo);
|
||||
|
||||
AdapterContextMenuInfo info = (AdapterContextMenuInfo) menuInfo;
|
||||
AppObject selectedApp = (AppObject) appListAdapter.getItem(info.position);
|
||||
AppObject selectedApp = appListAdapter.getItem(info.position);
|
||||
if (selectedApp == null || selectedApp.app == null) {
|
||||
return;
|
||||
}
|
||||
@@ -154,7 +159,7 @@ public class AppView extends Activity {
|
||||
@Override
|
||||
public boolean onContextItemSelected(MenuItem item) {
|
||||
AdapterContextMenuInfo info = (AdapterContextMenuInfo) item.getMenuInfo();
|
||||
AppObject app = (AppObject) appListAdapter.getItem(info.position);
|
||||
AppObject app = appListAdapter.getItem(info.position);
|
||||
switch (item.getItemId())
|
||||
{
|
||||
case RESUME_ID:
|
||||
@@ -216,9 +221,9 @@ public class AppView extends Activity {
|
||||
|
||||
// Success case
|
||||
return;
|
||||
} catch (GfeHttpResponseException e) {
|
||||
} catch (IOException e) {
|
||||
} catch (XmlPullParserException e) {
|
||||
} catch (GfeHttpResponseException ignored) {
|
||||
} catch (IOException ignored) {
|
||||
} catch (XmlPullParserException ignored) {
|
||||
} finally {
|
||||
spinner.dismiss();
|
||||
}
|
||||
@@ -14,13 +14,13 @@ import com.limelight.nvstream.StreamConfiguration;
|
||||
import com.limelight.nvstream.av.video.VideoDecoderRenderer;
|
||||
import com.limelight.nvstream.input.KeyboardPacket;
|
||||
import com.limelight.nvstream.input.MouseButtonPacket;
|
||||
import com.limelight.preferences.PreferenceConfiguration;
|
||||
import com.limelight.utils.Dialog;
|
||||
import com.limelight.utils.SpinnerDialog;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.graphics.Point;
|
||||
import android.media.AudioManager;
|
||||
import android.net.ConnectivityManager;
|
||||
@@ -35,6 +35,7 @@ import android.view.SurfaceHolder;
|
||||
import android.view.SurfaceView;
|
||||
import android.view.View;
|
||||
import android.view.View.OnGenericMotionListener;
|
||||
import android.view.View.OnSystemUiVisibilityChangeListener;
|
||||
import android.view.View.OnTouchListener;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.Window;
|
||||
@@ -43,7 +44,8 @@ import android.widget.Toast;
|
||||
|
||||
|
||||
public class Game extends Activity implements SurfaceHolder.Callback,
|
||||
OnGenericMotionListener, OnTouchListener, NvConnectionListener, EvdevListener
|
||||
OnGenericMotionListener, OnTouchListener, NvConnectionListener, EvdevListener,
|
||||
OnSystemUiVisibilityChangeListener
|
||||
{
|
||||
private int lastMouseX = Integer.MIN_VALUE;
|
||||
private int lastMouseY = Integer.MIN_VALUE;
|
||||
@@ -55,8 +57,7 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
||||
private ControllerHandler controllerHandler;
|
||||
private KeyboardTranslator keybTranslator;
|
||||
|
||||
private int height;
|
||||
private int width;
|
||||
private PreferenceConfiguration prefConfig;
|
||||
private Point screenSize = new Point(0, 0);
|
||||
|
||||
private NvConnection conn;
|
||||
@@ -65,10 +66,10 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
||||
private boolean connecting = false;
|
||||
private boolean connected = false;
|
||||
|
||||
private boolean stretchToFit;
|
||||
private boolean toastsDisabled;
|
||||
|
||||
private EvdevWatcher evdevWatcher;
|
||||
private int modifierFlags = 0;
|
||||
private boolean grabbedInput = true;
|
||||
private boolean grabComboDown = false;
|
||||
|
||||
private ConfigurableDecoderRenderer decoderRenderer;
|
||||
|
||||
@@ -81,35 +82,6 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
||||
public static final String EXTRA_UNIQUEID = "UniqueId";
|
||||
public static final String EXTRA_STREAMING_REMOTE = "Remote";
|
||||
|
||||
public static final String PREFS_FILE_NAME = "gameprefs";
|
||||
|
||||
public static final String WIDTH_PREF_STRING = "ResH";
|
||||
public static final String HEIGHT_PREF_STRING = "ResV";
|
||||
public static final String REFRESH_RATE_PREF_STRING = "FPS";
|
||||
public static final String DECODER_PREF_STRING = "Decoder";
|
||||
public static final String BITRATE_PREF_STRING = "Bitrate";
|
||||
public static final String STRETCH_PREF_STRING = "Stretch";
|
||||
public static final String SOPS_PREF_STRING = "Sops";
|
||||
public static final String DISABLE_TOASTS_PREF_STRING = "NoToasts";
|
||||
|
||||
public static final int BITRATE_DEFAULT_720_30 = 5;
|
||||
public static final int BITRATE_DEFAULT_720_60 = 10;
|
||||
public static final int BITRATE_DEFAULT_1080_30 = 10;
|
||||
public static final int BITRATE_DEFAULT_1080_60 = 30;
|
||||
|
||||
public static final int DEFAULT_WIDTH = 1280;
|
||||
public static final int DEFAULT_HEIGHT = 720;
|
||||
public static final int DEFAULT_REFRESH_RATE = 60;
|
||||
public static final int DEFAULT_DECODER = 0;
|
||||
public static final int DEFAULT_BITRATE = BITRATE_DEFAULT_720_60;
|
||||
public static final boolean DEFAULT_STRETCH = false;
|
||||
public static final boolean DEFAULT_SOPS = true;
|
||||
public static final boolean DEFAULT_DISABLE_TOASTS = false;
|
||||
|
||||
public static final int FORCE_HARDWARE_DECODER = -1;
|
||||
public static final int AUTOSELECT_DECODER = 0;
|
||||
public static final int FORCE_SOFTWARE_DECODER = 1;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
@@ -133,6 +105,9 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
||||
getWindow().addFlags(WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN);
|
||||
}
|
||||
|
||||
// Listen for UI visibility events
|
||||
getWindow().getDecorView().setOnSystemUiVisibilityChangeListener(this);
|
||||
|
||||
// Change volume button behavior
|
||||
setVolumeControlStream(AudioManager.STREAM_MUSIC);
|
||||
|
||||
@@ -143,31 +118,21 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
||||
spinner = SpinnerDialog.displayDialog(this, "Establishing Connection", "Starting connection", true);
|
||||
|
||||
// Read the stream preferences
|
||||
SharedPreferences prefs = getSharedPreferences(PREFS_FILE_NAME, Context.MODE_MULTI_PROCESS);
|
||||
switch (prefs.getInt(Game.DECODER_PREF_STRING, Game.DEFAULT_DECODER)) {
|
||||
case Game.FORCE_SOFTWARE_DECODER:
|
||||
prefConfig = PreferenceConfiguration.readPreferences(this);
|
||||
switch (prefConfig.decoder) {
|
||||
case PreferenceConfiguration.FORCE_SOFTWARE_DECODER:
|
||||
drFlags |= VideoDecoderRenderer.FLAG_FORCE_SOFTWARE_DECODING;
|
||||
break;
|
||||
case Game.AUTOSELECT_DECODER:
|
||||
case PreferenceConfiguration.AUTOSELECT_DECODER:
|
||||
break;
|
||||
case Game.FORCE_HARDWARE_DECODER:
|
||||
case PreferenceConfiguration.FORCE_HARDWARE_DECODER:
|
||||
drFlags |= VideoDecoderRenderer.FLAG_FORCE_HARDWARE_DECODING;
|
||||
break;
|
||||
}
|
||||
|
||||
stretchToFit = prefs.getBoolean(STRETCH_PREF_STRING, DEFAULT_STRETCH);
|
||||
if (stretchToFit) {
|
||||
if (prefConfig.stretchVideo) {
|
||||
drFlags |= VideoDecoderRenderer.FLAG_FILL_SCREEN;
|
||||
}
|
||||
|
||||
int refreshRate, bitrate;
|
||||
boolean sops;
|
||||
width = prefs.getInt(WIDTH_PREF_STRING, DEFAULT_WIDTH);
|
||||
height = prefs.getInt(HEIGHT_PREF_STRING, DEFAULT_HEIGHT);
|
||||
refreshRate = prefs.getInt(REFRESH_RATE_PREF_STRING, DEFAULT_REFRESH_RATE);
|
||||
bitrate = prefs.getInt(BITRATE_PREF_STRING, DEFAULT_BITRATE);
|
||||
sops = prefs.getBoolean(SOPS_PREF_STRING, DEFAULT_SOPS);
|
||||
toastsDisabled = prefs.getBoolean(DISABLE_TOASTS_PREF_STRING, DEFAULT_DISABLE_TOASTS);
|
||||
|
||||
Display display = getWindowManager().getDefaultDisplay();
|
||||
display.getSize(screenSize);
|
||||
@@ -189,21 +154,30 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
||||
String host = Game.this.getIntent().getStringExtra(EXTRA_HOST);
|
||||
String app = Game.this.getIntent().getStringExtra(EXTRA_APP);
|
||||
String uniqueId = Game.this.getIntent().getStringExtra(EXTRA_UNIQUEID);
|
||||
|
||||
// Initialize the connection
|
||||
conn = new NvConnection(host, uniqueId, Game.this,
|
||||
new StreamConfiguration(app, width, height, refreshRate, bitrate * 1000, sops),
|
||||
PlatformBinding.getCryptoProvider(this));
|
||||
keybTranslator = new KeyboardTranslator(conn);
|
||||
controllerHandler = new ControllerHandler(conn);
|
||||
|
||||
decoderRenderer = new ConfigurableDecoderRenderer();
|
||||
decoderRenderer.initializeWithFlags(drFlags);
|
||||
|
||||
StreamConfiguration config = new StreamConfiguration.Builder()
|
||||
.setResolution(prefConfig.width, prefConfig.height)
|
||||
.setRefreshRate(prefConfig.fps)
|
||||
.setApp(app)
|
||||
.setBitrate(prefConfig.bitrate * 1000)
|
||||
.setEnableSops(prefConfig.enableSops)
|
||||
.enableAdaptiveResolution((decoderRenderer.getCapabilities() &
|
||||
VideoDecoderRenderer.CAPABILITY_ADAPTIVE_RESOLUTION) != 0)
|
||||
.enableLocalAudioPlayback(prefConfig.playHostAudio)
|
||||
.build();
|
||||
|
||||
// Initialize the connection
|
||||
conn = new NvConnection(host, uniqueId, Game.this, config, PlatformBinding.getCryptoProvider(this));
|
||||
keybTranslator = new KeyboardTranslator(conn);
|
||||
controllerHandler = new ControllerHandler(conn);
|
||||
|
||||
SurfaceHolder sh = sv.getHolder();
|
||||
if (stretchToFit || !decoderRenderer.isHardwareAccelerated()) {
|
||||
if (prefConfig.stretchVideo || !decoderRenderer.isHardwareAccelerated()) {
|
||||
// Set the surface to the size of the video
|
||||
sh.setFixedSize(width, height);
|
||||
sh.setFixedSize(prefConfig.width, prefConfig.height);
|
||||
}
|
||||
|
||||
// Initialize touch contexts
|
||||
@@ -266,11 +240,11 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
||||
}
|
||||
};
|
||||
|
||||
private void hideSystemUi() {
|
||||
private void hideSystemUi(int delay) {
|
||||
Handler h = getWindow().getDecorView().getHandler();
|
||||
if (h != null) {
|
||||
h.removeCallbacks(hideSystemUi);
|
||||
h.postDelayed(hideSystemUi, 1000);
|
||||
h.postDelayed(hideSystemUi, delay);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -282,7 +256,7 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
||||
Dialog.closeDialogs();
|
||||
|
||||
displayedFailureDialog = true;
|
||||
conn.stop();
|
||||
stopConnection();
|
||||
|
||||
int averageEndToEndLat = decoderRenderer.getAverageEndToEndLatency();
|
||||
int averageDecoderLat = decoderRenderer.getAverageDecoderLatency();
|
||||
@@ -300,10 +274,6 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
||||
if (message != null) {
|
||||
Toast.makeText(this, message, Toast.LENGTH_LONG).show();
|
||||
}
|
||||
|
||||
if (evdevWatcher != null) {
|
||||
evdevWatcher.shutdown();
|
||||
}
|
||||
|
||||
finish();
|
||||
}
|
||||
@@ -315,6 +285,84 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
||||
wifiLock.release();
|
||||
}
|
||||
|
||||
private Runnable toggleGrab = new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
|
||||
if (evdevWatcher != null) {
|
||||
if (grabbedInput) {
|
||||
evdevWatcher.ungrabAll();
|
||||
}
|
||||
else {
|
||||
evdevWatcher.regrabAll();
|
||||
}
|
||||
}
|
||||
|
||||
grabbedInput = !grabbedInput;
|
||||
}
|
||||
};
|
||||
|
||||
// Returns true if the key stroke was consumed
|
||||
private boolean handleSpecialKeys(short translatedKey, boolean down) {
|
||||
int modifierMask = 0;
|
||||
|
||||
// Mask off the high byte
|
||||
translatedKey &= 0xff;
|
||||
|
||||
if (translatedKey == KeyboardTranslator.VK_CONTROL) {
|
||||
modifierMask = KeyboardPacket.MODIFIER_CTRL;
|
||||
}
|
||||
else if (translatedKey == KeyboardTranslator.VK_SHIFT) {
|
||||
modifierMask = KeyboardPacket.MODIFIER_SHIFT;
|
||||
}
|
||||
else if (translatedKey == KeyboardTranslator.VK_ALT) {
|
||||
modifierMask = KeyboardPacket.MODIFIER_ALT;
|
||||
}
|
||||
|
||||
if (down) {
|
||||
this.modifierFlags |= modifierMask;
|
||||
}
|
||||
else {
|
||||
this.modifierFlags &= ~modifierMask;
|
||||
}
|
||||
|
||||
// Check if Ctrl+Shift+Z is pressed
|
||||
if (translatedKey == KeyboardTranslator.VK_Z &&
|
||||
(modifierFlags & (KeyboardPacket.MODIFIER_CTRL | KeyboardPacket.MODIFIER_SHIFT)) ==
|
||||
(KeyboardPacket.MODIFIER_CTRL | KeyboardPacket.MODIFIER_SHIFT))
|
||||
{
|
||||
if (down) {
|
||||
// Now that we've pressed the magic combo
|
||||
// we'll wait for one of the keys to come up
|
||||
grabComboDown = true;
|
||||
}
|
||||
else {
|
||||
// Toggle the grab if Z comes up
|
||||
Handler h = getWindow().getDecorView().getHandler();
|
||||
if (h != null) {
|
||||
h.postDelayed(toggleGrab, 250);
|
||||
}
|
||||
|
||||
grabComboDown = false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
// Toggle the grab if control or shift comes up
|
||||
else if (grabComboDown) {
|
||||
Handler h = getWindow().getDecorView().getHandler();
|
||||
if (h != null) {
|
||||
h.postDelayed(toggleGrab, 250);
|
||||
}
|
||||
|
||||
grabComboDown = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Not a special combo
|
||||
return false;
|
||||
}
|
||||
|
||||
private static byte getModifierState(KeyEvent event) {
|
||||
byte modifier = 0;
|
||||
if (event.isShiftPressed()) {
|
||||
@@ -329,6 +377,10 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
||||
return modifier;
|
||||
}
|
||||
|
||||
private byte getModifierState() {
|
||||
return (byte) modifierFlags;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onKeyDown(int keyCode, KeyEvent event) {
|
||||
InputDevice dev = event.getDevice();
|
||||
@@ -350,6 +402,21 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
||||
return super.onKeyDown(keyCode, event);
|
||||
}
|
||||
|
||||
// Let this method take duplicate key down events
|
||||
if (handleSpecialKeys(translated, true)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Eat repeat down events
|
||||
if (event.getRepeatCount() > 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Pass through keyboard input if we're not grabbing
|
||||
if (!grabbedInput) {
|
||||
return super.onKeyDown(keyCode, event);
|
||||
}
|
||||
|
||||
keybTranslator.sendKeyDown(translated,
|
||||
getModifierState(event));
|
||||
}
|
||||
@@ -359,16 +426,6 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
||||
|
||||
@Override
|
||||
public boolean onKeyUp(int keyCode, KeyEvent event) {
|
||||
// Pressing a volume button drops the immersive flag so the UI shows up again and doesn't
|
||||
// go away. I'm not sure if that's a bug or a feature, but we're working around it here
|
||||
if (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN || keyCode == KeyEvent.KEYCODE_VOLUME_UP) {
|
||||
Handler h = getWindow().getDecorView().getHandler();
|
||||
if (h != null) {
|
||||
h.removeCallbacks(hideSystemUi);
|
||||
h.postDelayed(hideSystemUi, 2000);
|
||||
}
|
||||
}
|
||||
|
||||
InputDevice dev = event.getDevice();
|
||||
if (dev == null) {
|
||||
return super.onKeyUp(keyCode, event);
|
||||
@@ -388,6 +445,15 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
||||
return super.onKeyUp(keyCode, event);
|
||||
}
|
||||
|
||||
if (handleSpecialKeys(translated, false)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Pass through keyboard input if we're not grabbing
|
||||
if (!grabbedInput) {
|
||||
return super.onKeyUp(keyCode, event);
|
||||
}
|
||||
|
||||
keybTranslator.sendKeyUp(translated,
|
||||
getModifierState(event));
|
||||
}
|
||||
@@ -404,25 +470,35 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onTouchEvent(MotionEvent event) {
|
||||
if ((event.getSource() & InputDevice.SOURCE_CLASS_POINTER) != 0)
|
||||
|
||||
// Returns true if the event was consumed
|
||||
private boolean handleMotionEvent(MotionEvent event) {
|
||||
// Pass through keyboard input if we're not grabbing
|
||||
if (!grabbedInput) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ((event.getSource() & InputDevice.SOURCE_CLASS_JOYSTICK) != 0) {
|
||||
if (controllerHandler.handleMotionEvent(event)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
else if ((event.getSource() & InputDevice.SOURCE_CLASS_POINTER) != 0)
|
||||
{
|
||||
// This case is for touch-based input devices
|
||||
if (event.getSource() == InputDevice.SOURCE_TOUCHSCREEN ||
|
||||
event.getSource() == InputDevice.SOURCE_STYLUS)
|
||||
event.getSource() == InputDevice.SOURCE_STYLUS)
|
||||
{
|
||||
int actionIndex = event.getActionIndex();
|
||||
|
||||
|
||||
int eventX = (int)event.getX(actionIndex);
|
||||
int eventY = (int)event.getY(actionIndex);
|
||||
|
||||
|
||||
TouchContext context = getTouchContext(actionIndex);
|
||||
if (context == null) {
|
||||
return super.onTouchEvent(event);
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
switch (event.getActionMasked())
|
||||
{
|
||||
case MotionEvent.ACTION_POINTER_DOWN:
|
||||
@@ -440,12 +516,12 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
||||
case MotionEvent.ACTION_MOVE:
|
||||
// ACTION_MOVE is special because it always has actionIndex == 0
|
||||
// We'll call the move handlers for all indexes manually
|
||||
for (int i = 0; i < touchContextMap.length; i++) {
|
||||
touchContextMap[i].touchMoveEvent(eventX, eventY);
|
||||
}
|
||||
for (TouchContext aTouchContextMap : touchContextMap) {
|
||||
aTouchContextMap.touchMoveEvent(eventX, eventY);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
return super.onTouchEvent(event);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
// This case is for mice
|
||||
@@ -453,6 +529,12 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
||||
{
|
||||
int changedButtons = event.getButtonState() ^ lastButtonState;
|
||||
|
||||
if (event.getActionMasked() == MotionEvent.ACTION_SCROLL) {
|
||||
// Send the vertical scroll packet
|
||||
byte vScrollClicks = (byte) event.getAxisValue(MotionEvent.AXIS_VSCROLL);
|
||||
conn.sendMouseScroll(vScrollClicks);
|
||||
}
|
||||
|
||||
if ((changedButtons & MotionEvent.BUTTON_PRIMARY) != 0) {
|
||||
if ((event.getButtonState() & MotionEvent.BUTTON_PRIMARY) != 0) {
|
||||
conn.sendMouseButtonDown(MouseButtonPacket.BUTTON_LEFT);
|
||||
@@ -461,7 +543,7 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
||||
conn.sendMouseButtonUp(MouseButtonPacket.BUTTON_LEFT);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if ((changedButtons & MotionEvent.BUTTON_SECONDARY) != 0) {
|
||||
if ((event.getButtonState() & MotionEvent.BUTTON_SECONDARY) != 0) {
|
||||
conn.sendMouseButtonDown(MouseButtonPacket.BUTTON_RIGHT);
|
||||
@@ -470,7 +552,7 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
||||
conn.sendMouseButtonUp(MouseButtonPacket.BUTTON_RIGHT);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if ((changedButtons & MotionEvent.BUTTON_TERTIARY) != 0) {
|
||||
if ((event.getButtonState() & MotionEvent.BUTTON_TERTIARY) != 0) {
|
||||
conn.sendMouseButtonDown(MouseButtonPacket.BUTTON_MIDDLE);
|
||||
@@ -479,48 +561,36 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
||||
conn.sendMouseButtonUp(MouseButtonPacket.BUTTON_MIDDLE);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
updateMousePosition((int)event.getX(), (int)event.getY());
|
||||
|
||||
|
||||
lastButtonState = event.getButtonState();
|
||||
}
|
||||
else
|
||||
{
|
||||
return super.onTouchEvent(event);
|
||||
// Unknown source
|
||||
return false;
|
||||
}
|
||||
|
||||
// Handled a known source
|
||||
return true;
|
||||
}
|
||||
|
||||
return super.onTouchEvent(event);
|
||||
|
||||
// Unknown class
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onTouchEvent(MotionEvent event) {
|
||||
return handleMotionEvent(event) || super.onTouchEvent(event);
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onGenericMotionEvent(MotionEvent event) {
|
||||
if ((event.getSource() & InputDevice.SOURCE_CLASS_JOYSTICK) != 0) {
|
||||
if (controllerHandler.handleMotionEvent(event)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
else if ((event.getSource() & InputDevice.SOURCE_CLASS_POINTER) != 0)
|
||||
{
|
||||
switch (event.getActionMasked())
|
||||
{
|
||||
case MotionEvent.ACTION_HOVER_MOVE:
|
||||
// Send a mouse move update (if neccessary)
|
||||
updateMousePosition((int)event.getX(), (int)event.getY());
|
||||
break;
|
||||
case MotionEvent.ACTION_SCROLL:
|
||||
// Send the vertical scroll packet
|
||||
byte vScrollClicks = (byte) event.getAxisValue(MotionEvent.AXIS_VSCROLL);
|
||||
conn.sendMouseScroll(vScrollClicks);
|
||||
break;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
return super.onGenericMotionEvent(event);
|
||||
}
|
||||
return handleMotionEvent(event) || super.onGenericMotionEvent(event);
|
||||
|
||||
}
|
||||
|
||||
private void updateMousePosition(int eventX, int eventY) {
|
||||
// Send a mouse move if we already have a mouse location
|
||||
@@ -534,8 +604,8 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
||||
|
||||
// Scale the deltas if the device resolution is different
|
||||
// than the stream resolution
|
||||
deltaX = (int)Math.round((double)deltaX * ((double)width / (double)screenSize.x));
|
||||
deltaY = (int)Math.round((double)deltaY * ((double)height / (double)screenSize.y));
|
||||
deltaX = (int)Math.round((double)deltaX * ((double)prefConfig.width / (double)screenSize.x));
|
||||
deltaY = (int)Math.round((double)deltaY * ((double)prefConfig.height / (double)screenSize.y));
|
||||
|
||||
conn.sendMouseMove((short)deltaX, (short)deltaY);
|
||||
}
|
||||
@@ -547,15 +617,13 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
||||
|
||||
@Override
|
||||
public boolean onGenericMotion(View v, MotionEvent event) {
|
||||
// Send it to the activity's motion event handler
|
||||
return onGenericMotionEvent(event);
|
||||
return handleMotionEvent(event);
|
||||
}
|
||||
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
@Override
|
||||
public boolean onTouch(View v, MotionEvent event) {
|
||||
// Send it to the activity's touch event handler
|
||||
return onTouchEvent(event);
|
||||
return handleMotionEvent(event);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -568,6 +636,19 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
||||
@Override
|
||||
public void stageComplete(Stage stage) {
|
||||
}
|
||||
|
||||
private void stopConnection() {
|
||||
if (connecting || connected) {
|
||||
connecting = connected = false;
|
||||
conn.stop();
|
||||
}
|
||||
|
||||
// Close the Evdev watcher to allow use of captured input devices
|
||||
if (evdevWatcher != null) {
|
||||
evdevWatcher.shutdown();
|
||||
evdevWatcher = null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stageFailed(Stage stage) {
|
||||
@@ -578,9 +659,8 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
||||
|
||||
if (!displayedFailureDialog) {
|
||||
displayedFailureDialog = true;
|
||||
stopConnection();
|
||||
Dialog.displayDialog(this, "Connection Error", "Starting "+stage.getName()+" failed", true);
|
||||
conn.stop();
|
||||
connecting = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -589,9 +669,9 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
||||
if (!displayedFailureDialog) {
|
||||
displayedFailureDialog = true;
|
||||
e.printStackTrace();
|
||||
|
||||
stopConnection();
|
||||
Dialog.displayDialog(this, "Connection Terminated", "The connection failed unexpectedly", true);
|
||||
conn.stop();
|
||||
connected = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -605,7 +685,7 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
||||
connecting = false;
|
||||
connected = true;
|
||||
|
||||
hideSystemUi();
|
||||
hideSystemUi(1000);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -620,7 +700,7 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
||||
|
||||
@Override
|
||||
public void displayTransientMessage(final String message) {
|
||||
if (!toastsDisabled) {
|
||||
if (!prefConfig.disableWarnings) {
|
||||
runOnUiThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
@@ -641,8 +721,9 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
||||
|
||||
// Resize the surface to match the aspect ratio of the video
|
||||
// This must be done after the surface is created.
|
||||
if (!stretchToFit && decoderRenderer.isHardwareAccelerated()) {
|
||||
resizeSurfaceWithAspectRatio((SurfaceView) findViewById(R.id.surfaceView), width, height);
|
||||
if (!prefConfig.stretchVideo && decoderRenderer.isHardwareAccelerated()) {
|
||||
resizeSurfaceWithAspectRatio((SurfaceView) findViewById(R.id.surfaceView),
|
||||
prefConfig.width, prefConfig.height);
|
||||
}
|
||||
|
||||
conn.start(PlatformBinding.getDeviceName(), holder, drFlags,
|
||||
@@ -653,8 +734,7 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
||||
@Override
|
||||
public void surfaceDestroyed(SurfaceHolder holder) {
|
||||
if (connected) {
|
||||
conn.stop();
|
||||
connected = false;
|
||||
stopConnection();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -695,4 +775,44 @@ public class Game extends Activity implements SurfaceHolder.Callback,
|
||||
public void mouseScroll(byte amount) {
|
||||
conn.sendMouseScroll(amount);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void keyboardEvent(boolean buttonDown, short keyCode) {
|
||||
short keyMap = keybTranslator.translate(keyCode);
|
||||
if (keyMap != 0) {
|
||||
if (handleSpecialKeys(keyMap, buttonDown)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (buttonDown) {
|
||||
keybTranslator.sendKeyDown(keyMap, getModifierState());
|
||||
}
|
||||
else {
|
||||
keybTranslator.sendKeyUp(keyMap, getModifierState());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSystemUiVisibilityChange(int visibility) {
|
||||
// Don't do anything if we're not connected
|
||||
if (!connected) {
|
||||
return;
|
||||
}
|
||||
|
||||
// This flag is set for all devices
|
||||
if ((visibility & View.SYSTEM_UI_FLAG_FULLSCREEN) == 0) {
|
||||
hideSystemUi(2000);
|
||||
}
|
||||
// This flag is only set on 4.4+
|
||||
else if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.KITKAT &&
|
||||
(visibility & View.SYSTEM_UI_FLAG_HIDE_NAVIGATION) == 0) {
|
||||
hideSystemUi(2000);
|
||||
}
|
||||
// This flag is only set before 4.4+
|
||||
else if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.KITKAT &&
|
||||
(visibility & View.SYSTEM_UI_FLAG_LOW_PROFILE) == 0) {
|
||||
hideSystemUi(2000);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import java.net.InetAddress;
|
||||
import java.net.UnknownHostException;
|
||||
|
||||
import com.limelight.binding.PlatformBinding;
|
||||
import com.limelight.binding.crypto.AndroidCryptoProvider;
|
||||
import com.limelight.computers.ComputerManagerListener;
|
||||
import com.limelight.computers.ComputerManagerService;
|
||||
import com.limelight.nvstream.http.ComputerDetails;
|
||||
@@ -13,6 +14,8 @@ import com.limelight.nvstream.http.NvHTTP;
|
||||
import com.limelight.nvstream.http.PairingManager;
|
||||
import com.limelight.nvstream.http.PairingManager.PairState;
|
||||
import com.limelight.nvstream.wol.WakeOnLanSender;
|
||||
import com.limelight.preferences.AddComputerManually;
|
||||
import com.limelight.preferences.StreamSettings;
|
||||
import com.limelight.utils.Dialog;
|
||||
|
||||
import android.app.Activity;
|
||||
@@ -20,8 +23,10 @@ import android.app.Service;
|
||||
import android.content.ComponentName;
|
||||
import android.content.Intent;
|
||||
import android.content.ServiceConnection;
|
||||
import android.content.res.Configuration;
|
||||
import android.os.Bundle;
|
||||
import android.os.IBinder;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.view.ContextMenu;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
@@ -59,6 +64,9 @@ public class PcView extends Activity {
|
||||
|
||||
// Start updates
|
||||
startComputerUpdates();
|
||||
|
||||
// Force a keypair to be generated early to avoid discovery delays
|
||||
new AndroidCryptoProvider(PcView.this).getClientCertificate();
|
||||
}
|
||||
}.start();
|
||||
}
|
||||
@@ -68,59 +76,61 @@ public class PcView extends Activity {
|
||||
}
|
||||
};
|
||||
|
||||
public void onConfigurationChanged(Configuration newConfig) {
|
||||
super.onConfigurationChanged(newConfig);
|
||||
|
||||
// Reinitialize views just in case orientation changed
|
||||
initializeViews();
|
||||
}
|
||||
|
||||
private final static int APP_LIST_ID = 1;
|
||||
private final static int PAIR_ID = 2;
|
||||
private final static int UNPAIR_ID = 3;
|
||||
private final static int WOL_ID = 4;
|
||||
private final static int DELETE_ID = 5;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
private void initializeViews() {
|
||||
setContentView(R.layout.activity_pc_view);
|
||||
|
||||
// Bind to the computer manager service
|
||||
bindService(new Intent(PcView.this, ComputerManagerService.class), serviceConnection,
|
||||
Service.BIND_AUTO_CREATE);
|
||||
|
||||
|
||||
// Set default preferences if we've never been run
|
||||
PreferenceManager.setDefaultValues(this, R.xml.preferences, false);
|
||||
|
||||
// Setup the list view
|
||||
settingsButton = (Button)findViewById(R.id.settingsButton);
|
||||
addComputerButton = (Button)findViewById(R.id.manuallyAddPc);
|
||||
|
||||
pcList = (ListView)findViewById(R.id.pcListView);
|
||||
pcListAdapter = new ArrayAdapter<ComputerObject>(this, R.layout.simplerow, R.id.rowTextView);
|
||||
pcListAdapter.setNotifyOnChange(false);
|
||||
pcList.setAdapter(pcListAdapter);
|
||||
pcList.setItemsCanFocus(true);
|
||||
pcList.setOnItemClickListener(new OnItemClickListener() {
|
||||
pcList.setAdapter(pcListAdapter);
|
||||
pcList.setItemsCanFocus(true);
|
||||
pcList.setOnItemClickListener(new OnItemClickListener() {
|
||||
@Override
|
||||
public void onItemClick(AdapterView<?> arg0, View arg1, int pos,
|
||||
long id) {
|
||||
ComputerObject computer = (ComputerObject) pcListAdapter.getItem(pos);
|
||||
if (computer.details == null) {
|
||||
// Placeholder item; no context menu for it
|
||||
return;
|
||||
}
|
||||
else if (computer.details.reachability == ComputerDetails.Reachability.OFFLINE) {
|
||||
// Open the context menu if a PC is offline
|
||||
ComputerObject computer = pcListAdapter.getItem(pos);
|
||||
if (computer.details == null) {
|
||||
// Placeholder item; no context menu for it
|
||||
return;
|
||||
}
|
||||
else if (computer.details.reachability == ComputerDetails.Reachability.OFFLINE) {
|
||||
// Open the context menu if a PC is offline
|
||||
openContextMenu(arg1);
|
||||
}
|
||||
else if (computer.details.pairState != PairState.PAIRED) {
|
||||
// Pair an unpaired machine by default
|
||||
doPair(computer.details);
|
||||
}
|
||||
else {
|
||||
doAppList(computer.details);
|
||||
}
|
||||
}
|
||||
else if (computer.details.pairState != PairState.PAIRED) {
|
||||
// Pair an unpaired machine by default
|
||||
doPair(computer.details);
|
||||
}
|
||||
else {
|
||||
doAppList(computer.details);
|
||||
}
|
||||
}
|
||||
});
|
||||
registerForContextMenu(pcList);
|
||||
settingsButton.setOnClickListener(new OnClickListener() {
|
||||
});
|
||||
registerForContextMenu(pcList);
|
||||
settingsButton.setOnClickListener(new OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
startActivity(new Intent(PcView.this, StreamSettings.class));
|
||||
}
|
||||
});
|
||||
});
|
||||
addComputerButton.setOnClickListener(new OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
@@ -128,8 +138,27 @@ public class PcView extends Activity {
|
||||
startActivity(i);
|
||||
}
|
||||
});
|
||||
|
||||
addListPlaceholder();
|
||||
|
||||
if (pcListAdapter.isEmpty()) {
|
||||
addListPlaceholder();
|
||||
}
|
||||
else {
|
||||
pcListAdapter.notifyDataSetChanged();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
// Bind to the computer manager service
|
||||
bindService(new Intent(PcView.this, ComputerManagerService.class), serviceConnection,
|
||||
Service.BIND_AUTO_CREATE);
|
||||
|
||||
pcListAdapter = new ArrayAdapter<ComputerObject>(this, R.layout.simplerow, R.id.rowTextView);
|
||||
pcListAdapter.setNotifyOnChange(false);
|
||||
|
||||
initializeViews();
|
||||
}
|
||||
|
||||
private void startComputerUpdates() {
|
||||
@@ -156,7 +185,7 @@ public class PcView extends Activity {
|
||||
}
|
||||
}
|
||||
|
||||
private void stopComputerUpdates() {
|
||||
private void stopComputerUpdates(boolean wait) {
|
||||
if (managerBinder != null) {
|
||||
if (!runningPolling) {
|
||||
return;
|
||||
@@ -165,6 +194,11 @@ public class PcView extends Activity {
|
||||
freezeUpdates = true;
|
||||
|
||||
managerBinder.stopPolling();
|
||||
|
||||
if (wait) {
|
||||
managerBinder.waitForPollingStopped();
|
||||
}
|
||||
|
||||
runningPolling = false;
|
||||
}
|
||||
}
|
||||
@@ -189,7 +223,7 @@ public class PcView extends Activity {
|
||||
protected void onPause() {
|
||||
super.onPause();
|
||||
|
||||
stopComputerUpdates();
|
||||
stopComputerUpdates(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -201,13 +235,13 @@ public class PcView extends Activity {
|
||||
|
||||
@Override
|
||||
public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) {
|
||||
stopComputerUpdates();
|
||||
stopComputerUpdates(false);
|
||||
|
||||
// Call superclass
|
||||
super.onCreateContextMenu(menu, v, menuInfo);
|
||||
|
||||
AdapterContextMenuInfo info = (AdapterContextMenuInfo) menuInfo;
|
||||
ComputerObject computer = (ComputerObject) pcListAdapter.getItem(info.position);
|
||||
ComputerObject computer = pcListAdapter.getItem(info.position);
|
||||
if (computer == null || computer.details == null) {
|
||||
startComputerUpdates();
|
||||
return;
|
||||
@@ -258,6 +292,9 @@ public class PcView extends Activity {
|
||||
NvHTTP httpConn;
|
||||
String message;
|
||||
try {
|
||||
// Stop updates and wait while pairing
|
||||
stopComputerUpdates(true);
|
||||
|
||||
InetAddress addr = null;
|
||||
if (computer.reachability == ComputerDetails.Reachability.LOCAL) {
|
||||
addr = computer.localIp;
|
||||
@@ -312,6 +349,9 @@ public class PcView extends Activity {
|
||||
Toast.makeText(PcView.this, toastMessage, Toast.LENGTH_LONG).show();
|
||||
}
|
||||
});
|
||||
|
||||
// Start polling again
|
||||
startComputerUpdates();
|
||||
}
|
||||
}).start();
|
||||
}
|
||||
@@ -442,7 +482,7 @@ public class PcView extends Activity {
|
||||
@Override
|
||||
public boolean onContextItemSelected(MenuItem item) {
|
||||
AdapterContextMenuInfo info = (AdapterContextMenuInfo) item.getMenuInfo();
|
||||
ComputerObject computer = (ComputerObject) pcListAdapter.getItem(info.position);
|
||||
ComputerObject computer = pcListAdapter.getItem(info.position);
|
||||
switch (item.getItemId())
|
||||
{
|
||||
case PAIR_ID:
|
||||
@@ -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 {}
|
||||
+15
-8
@@ -23,15 +23,16 @@ import java.security.spec.InvalidKeySpecException;
|
||||
import java.security.spec.PKCS8EncodedKeySpec;
|
||||
import java.util.Calendar;
|
||||
import java.util.Date;
|
||||
import java.util.Locale;
|
||||
|
||||
import org.bouncycastle.asn1.x500.X500Name;
|
||||
import org.bouncycastle.asn1.x500.X500NameBuilder;
|
||||
import org.bouncycastle.asn1.x500.style.BCStyle;
|
||||
import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo;
|
||||
import org.bouncycastle.cert.X509v3CertificateBuilder;
|
||||
import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
|
||||
import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder;
|
||||
import org.bouncycastle.jce.provider.BouncyCastleProvider;
|
||||
import org.bouncycastle.openssl.PEMWriter;
|
||||
import org.bouncycastle.openssl.jcajce.JcaPEMWriter;
|
||||
import org.bouncycastle.operator.ContentSigner;
|
||||
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
|
||||
|
||||
@@ -51,6 +52,8 @@ public class AndroidCryptoProvider implements LimelightCryptoProvider {
|
||||
private RSAPrivateKey key;
|
||||
private byte[] pemCertBytes;
|
||||
|
||||
private static final Object globalCryptoLock = new Object();
|
||||
|
||||
static {
|
||||
// Install the Bouncy Castle provider
|
||||
Security.addProvider(new BouncyCastleProvider());
|
||||
@@ -71,7 +74,10 @@ public class AndroidCryptoProvider implements LimelightCryptoProvider {
|
||||
try {
|
||||
FileInputStream fin = new FileInputStream(f);
|
||||
byte[] fileData = new byte[(int) f.length()];
|
||||
fin.read(fileData);
|
||||
if (fin.read(fileData) != f.length()) {
|
||||
// Failed to read
|
||||
fileData = null;
|
||||
}
|
||||
fin.close();
|
||||
return fileData;
|
||||
} catch (IOException e) {
|
||||
@@ -150,7 +156,8 @@ public class AndroidCryptoProvider implements LimelightCryptoProvider {
|
||||
nameBuilder.addRDN(BCStyle.CN, "NVIDIA GameStream Client");
|
||||
X500Name name = nameBuilder.build();
|
||||
|
||||
X509v3CertificateBuilder certBuilder = new JcaX509v3CertificateBuilder(name, serial, now, expirationDate, name, keyPair.getPublic());
|
||||
X509v3CertificateBuilder certBuilder = new X509v3CertificateBuilder(name, serial, now, expirationDate, Locale.ENGLISH, name,
|
||||
SubjectPublicKeyInfo.getInstance(keyPair.getPublic().getEncoded()));
|
||||
|
||||
try {
|
||||
ContentSigner sigGen = new JcaContentSignerBuilder("SHA1withRSA").setProvider(BouncyCastleProvider.PROVIDER_NAME).build(keyPair.getPrivate());
|
||||
@@ -177,7 +184,7 @@ public class AndroidCryptoProvider implements LimelightCryptoProvider {
|
||||
|
||||
// Write the certificate in OpenSSL PEM format (important for the server)
|
||||
StringWriter strWriter = new StringWriter();
|
||||
PEMWriter pemWriter = new PEMWriter(strWriter);
|
||||
JcaPEMWriter pemWriter = new JcaPEMWriter(strWriter);
|
||||
pemWriter.writeObject(cert);
|
||||
pemWriter.close();
|
||||
|
||||
@@ -208,7 +215,7 @@ public class AndroidCryptoProvider implements LimelightCryptoProvider {
|
||||
public X509Certificate getClientCertificate() {
|
||||
// Use a lock here to ensure only one guy will be generating or loading
|
||||
// the certificate and key at a time
|
||||
synchronized (this) {
|
||||
synchronized (globalCryptoLock) {
|
||||
// Return a loaded cert if we have one
|
||||
if (cert != null) {
|
||||
return cert;
|
||||
@@ -235,7 +242,7 @@ public class AndroidCryptoProvider implements LimelightCryptoProvider {
|
||||
public RSAPrivateKey getClientPrivateKey() {
|
||||
// Use a lock here to ensure only one guy will be generating or loading
|
||||
// the certificate and key at a time
|
||||
synchronized (this) {
|
||||
synchronized (globalCryptoLock) {
|
||||
// Return a loaded key if we have one
|
||||
if (key != null) {
|
||||
return key;
|
||||
@@ -260,7 +267,7 @@ public class AndroidCryptoProvider implements LimelightCryptoProvider {
|
||||
}
|
||||
|
||||
public byte[] getPemEncodedClientCertificate() {
|
||||
synchronized (this) {
|
||||
synchronized (globalCryptoLock) {
|
||||
// Call our helper function to do the cert loading/generation for us
|
||||
getClientCertificate();
|
||||
|
||||
+101
-29
@@ -47,6 +47,22 @@ public class ControllerHandler {
|
||||
|
||||
public ControllerHandler(NvConnection conn) {
|
||||
this.conn = conn;
|
||||
|
||||
// We want limelight-common to scale the axis values to match Xinput values
|
||||
ControllerPacket.enableAxisScaling = true;
|
||||
}
|
||||
|
||||
private static InputDevice.MotionRange getMotionRangeForJoystickAxis(InputDevice dev, int axis) {
|
||||
InputDevice.MotionRange range;
|
||||
|
||||
// First get the axis for SOURCE_JOYSTICK
|
||||
range = dev.getMotionRange(axis, InputDevice.SOURCE_JOYSTICK);
|
||||
if (range == null) {
|
||||
// Now try the axis for SOURCE_GAMEPAD
|
||||
range = dev.getMotionRange(axis, InputDevice.SOURCE_GAMEPAD);
|
||||
}
|
||||
|
||||
return range;
|
||||
}
|
||||
|
||||
private ControllerMapping createMappingForDevice(InputDevice dev) {
|
||||
@@ -55,10 +71,10 @@ public class ControllerHandler {
|
||||
mapping.leftStickXAxis = MotionEvent.AXIS_X;
|
||||
mapping.leftStickYAxis = MotionEvent.AXIS_Y;
|
||||
|
||||
InputDevice.MotionRange leftTriggerRange = dev.getMotionRange(MotionEvent.AXIS_LTRIGGER);
|
||||
InputDevice.MotionRange rightTriggerRange = dev.getMotionRange(MotionEvent.AXIS_RTRIGGER);
|
||||
InputDevice.MotionRange brakeRange = dev.getMotionRange(MotionEvent.AXIS_BRAKE);
|
||||
InputDevice.MotionRange gasRange = dev.getMotionRange(MotionEvent.AXIS_GAS);
|
||||
InputDevice.MotionRange leftTriggerRange = getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_LTRIGGER);
|
||||
InputDevice.MotionRange rightTriggerRange = getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_RTRIGGER);
|
||||
InputDevice.MotionRange brakeRange = getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_BRAKE);
|
||||
InputDevice.MotionRange gasRange = getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_GAS);
|
||||
if (leftTriggerRange != null && rightTriggerRange != null)
|
||||
{
|
||||
// Some controllers use LTRIGGER and RTRIGGER (like Ouya)
|
||||
@@ -73,8 +89,8 @@ public class ControllerHandler {
|
||||
}
|
||||
else
|
||||
{
|
||||
InputDevice.MotionRange rxRange = dev.getMotionRange(MotionEvent.AXIS_RX);
|
||||
InputDevice.MotionRange ryRange = dev.getMotionRange(MotionEvent.AXIS_RY);
|
||||
InputDevice.MotionRange rxRange = getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_RX);
|
||||
InputDevice.MotionRange ryRange = getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_RY);
|
||||
if (rxRange != null && ryRange != null) {
|
||||
String devName = dev.getName();
|
||||
if (devName.contains("Xbox") || devName.contains("XBox") || devName.contains("X-Box")) {
|
||||
@@ -86,6 +102,7 @@ public class ControllerHandler {
|
||||
mapping.leftTriggerAxis = MotionEvent.AXIS_Z;
|
||||
mapping.rightTriggerAxis = MotionEvent.AXIS_RZ;
|
||||
mapping.triggersIdleNegative = true;
|
||||
mapping.isXboxController = true;
|
||||
}
|
||||
else {
|
||||
// DS4 controller uses RX and RY for triggers
|
||||
@@ -99,8 +116,8 @@ public class ControllerHandler {
|
||||
}
|
||||
|
||||
if (mapping.rightStickXAxis == -1 && mapping.rightStickYAxis == -1) {
|
||||
InputDevice.MotionRange zRange = dev.getMotionRange(MotionEvent.AXIS_Z);
|
||||
InputDevice.MotionRange rzRange = dev.getMotionRange(MotionEvent.AXIS_RZ);
|
||||
InputDevice.MotionRange zRange = getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_Z);
|
||||
InputDevice.MotionRange rzRange = getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_RZ);
|
||||
|
||||
// Most other controllers use Z and RZ for the right stick
|
||||
if (zRange != null && rzRange != null) {
|
||||
@@ -108,8 +125,8 @@ public class ControllerHandler {
|
||||
mapping.rightStickYAxis = MotionEvent.AXIS_RZ;
|
||||
}
|
||||
else {
|
||||
InputDevice.MotionRange rxRange = dev.getMotionRange(MotionEvent.AXIS_RX);
|
||||
InputDevice.MotionRange ryRange = dev.getMotionRange(MotionEvent.AXIS_RY);
|
||||
InputDevice.MotionRange rxRange = getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_RX);
|
||||
InputDevice.MotionRange ryRange = getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_RY);
|
||||
|
||||
// Try RX and RY now
|
||||
if (rxRange != null && ryRange != null) {
|
||||
@@ -120,8 +137,8 @@ public class ControllerHandler {
|
||||
}
|
||||
|
||||
// Some devices have "hats" for d-pads
|
||||
InputDevice.MotionRange hatXRange = dev.getMotionRange(MotionEvent.AXIS_HAT_X);
|
||||
InputDevice.MotionRange hatYRange = dev.getMotionRange(MotionEvent.AXIS_HAT_Y);
|
||||
InputDevice.MotionRange hatXRange = getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_HAT_X);
|
||||
InputDevice.MotionRange hatYRange = getMotionRangeForJoystickAxis(dev, MotionEvent.AXIS_HAT_Y);
|
||||
if (hatXRange != null && hatYRange != null) {
|
||||
mapping.hatXAxis = MotionEvent.AXIS_HAT_X;
|
||||
mapping.hatYAxis = MotionEvent.AXIS_HAT_Y;
|
||||
@@ -131,18 +148,54 @@ public class ControllerHandler {
|
||||
}
|
||||
|
||||
if (mapping.leftStickXAxis != -1 && mapping.leftStickYAxis != -1) {
|
||||
InputDevice.MotionRange lsXRange = dev.getMotionRange(mapping.leftStickXAxis);
|
||||
InputDevice.MotionRange lsYRange = dev.getMotionRange(mapping.leftStickYAxis);
|
||||
InputDevice.MotionRange lsXRange = getMotionRangeForJoystickAxis(dev, mapping.leftStickXAxis);
|
||||
InputDevice.MotionRange lsYRange = getMotionRangeForJoystickAxis(dev, mapping.leftStickYAxis);
|
||||
if (lsXRange != null && lsYRange != null) {
|
||||
mapping.leftStickDeadzoneRadius = Math.max(lsXRange.getFlat(), lsYRange.getFlat());
|
||||
// The flat values should never be negative but we'll deal with it if they are
|
||||
mapping.leftStickDeadzoneRadius = Math.max(Math.abs(lsXRange.getFlat()),
|
||||
Math.abs(lsYRange.getFlat()));
|
||||
|
||||
// Some devices (certain OUYAs at least) report a deadzone that's larger
|
||||
// than the entire range of their axis likely due to some system software bug.
|
||||
// If we see a very large deadzone, simply ignore the value and use our default.
|
||||
if (mapping.leftStickDeadzoneRadius > 0.5f) {
|
||||
mapping.leftStickDeadzoneRadius = 0;
|
||||
}
|
||||
|
||||
// If there isn't a (reasonable) deadzone at all, use 20%
|
||||
if (mapping.leftStickDeadzoneRadius < 0.02f) {
|
||||
mapping.leftStickDeadzoneRadius = 0.20f;
|
||||
}
|
||||
// Check that the deadzone is 15% at minimum
|
||||
else if (mapping.leftStickDeadzoneRadius < 0.15f) {
|
||||
mapping.leftStickDeadzoneRadius = 0.15f;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (mapping.rightStickXAxis != -1 && mapping.rightStickYAxis != -1) {
|
||||
InputDevice.MotionRange rsXRange = dev.getMotionRange(mapping.rightStickXAxis);
|
||||
InputDevice.MotionRange rsYRange = dev.getMotionRange(mapping.rightStickYAxis);
|
||||
InputDevice.MotionRange rsXRange = getMotionRangeForJoystickAxis(dev, mapping.rightStickXAxis);
|
||||
InputDevice.MotionRange rsYRange = getMotionRangeForJoystickAxis(dev, mapping.rightStickYAxis);
|
||||
if (rsXRange != null && rsYRange != null) {
|
||||
mapping.rightStickDeadzoneRadius = Math.max(rsXRange.getFlat(), rsYRange.getFlat());
|
||||
// The flat values should never be negative but we'll deal with it if they are
|
||||
mapping.rightStickDeadzoneRadius = Math.max(Math.abs(rsXRange.getFlat()),
|
||||
Math.abs(rsYRange.getFlat()));
|
||||
|
||||
// Some devices (certain OUYAs at least) report a deadzone that's larger
|
||||
// than the entire range of their axis likely due to some system software bug.
|
||||
// If we see a very large deadzone, simply ignore the value and use our default.
|
||||
if (mapping.rightStickDeadzoneRadius > 0.5f) {
|
||||
mapping.rightStickDeadzoneRadius = 0;
|
||||
}
|
||||
|
||||
// If there isn't a (reasonable) deadzone at all, use 20%
|
||||
if (mapping.rightStickDeadzoneRadius < 0.02f) {
|
||||
mapping.rightStickDeadzoneRadius = 0.20f;
|
||||
}
|
||||
// Check that the deadzone is 15% at minimum
|
||||
else if (mapping.rightStickDeadzoneRadius < 0.15f) {
|
||||
mapping.rightStickDeadzoneRadius = 0.15f;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -175,9 +228,9 @@ public class ControllerHandler {
|
||||
leftStickX, leftStickY, rightStickX, rightStickY);
|
||||
}
|
||||
|
||||
private static int handleRemapping(ControllerMapping mapping, int keyCode) {
|
||||
private static int handleRemapping(ControllerMapping mapping, KeyEvent event) {
|
||||
if (mapping.isDualShock4) {
|
||||
switch (keyCode) {
|
||||
switch (event.getKeyCode()) {
|
||||
case KeyEvent.KEYCODE_BUTTON_Y:
|
||||
return KeyEvent.KEYCODE_BUTTON_L1;
|
||||
|
||||
@@ -216,7 +269,7 @@ public class ControllerHandler {
|
||||
}
|
||||
|
||||
if (mapping.hatXAxis != -1 && mapping.hatYAxis != -1) {
|
||||
switch (keyCode) {
|
||||
switch (event.getKeyCode()) {
|
||||
// These are duplicate dpad events for hat input
|
||||
case KeyEvent.KEYCODE_DPAD_LEFT:
|
||||
case KeyEvent.KEYCODE_DPAD_RIGHT:
|
||||
@@ -226,8 +279,26 @@ public class ControllerHandler {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
else if (mapping.hatXAxis == -1 &&
|
||||
mapping.hatYAxis == -1 &&
|
||||
mapping.isXboxController &&
|
||||
event.getKeyCode() == KeyEvent.KEYCODE_UNKNOWN) {
|
||||
// If there's not a proper Xbox controller mapping, we'll translate the raw d-pad
|
||||
// scan codes into proper key codes
|
||||
switch (event.getScanCode())
|
||||
{
|
||||
case 704:
|
||||
return KeyEvent.KEYCODE_DPAD_LEFT;
|
||||
case 705:
|
||||
return KeyEvent.KEYCODE_DPAD_RIGHT;
|
||||
case 706:
|
||||
return KeyEvent.KEYCODE_DPAD_UP;
|
||||
case 707:
|
||||
return KeyEvent.KEYCODE_DPAD_DOWN;
|
||||
}
|
||||
}
|
||||
|
||||
return keyCode;
|
||||
return event.getKeyCode();
|
||||
}
|
||||
|
||||
private Vector2d handleDeadZone(float x, float y, float deadzoneRadius) {
|
||||
@@ -242,7 +313,7 @@ public class ControllerHandler {
|
||||
// Scale the input based on the distance from the deadzone
|
||||
inputVector.getNormalized(normalizedInputVector);
|
||||
normalizedInputVector.scalarMultiply((inputVector.getMagnitude() - deadzoneRadius) / (1.0f - deadzoneRadius));
|
||||
|
||||
|
||||
// Bound the X value to -1.0 to 1.0
|
||||
if (normalizedInputVector.getX() > 1.0f) {
|
||||
normalizedInputVector.setX(1.0f);
|
||||
@@ -284,7 +355,7 @@ public class ControllerHandler {
|
||||
event.getAxisValue(mapping.rightStickYAxis), mapping.rightStickDeadzoneRadius);
|
||||
|
||||
rightStickX = (short)(rightStickVector.getX() * 0x7FFE);
|
||||
rightStickY = (short)(-rightStickVector.getY() * 0x7FFE);
|
||||
rightStickY = (short)(-rightStickVector.getY() * 0x7FFE);
|
||||
}
|
||||
|
||||
// Handle controllers with analog triggers
|
||||
@@ -333,7 +404,7 @@ public class ControllerHandler {
|
||||
return false;
|
||||
}
|
||||
|
||||
keyCode = handleRemapping(mapping, keyCode);
|
||||
keyCode = handleRemapping(mapping, event);
|
||||
if (keyCode == 0) {
|
||||
return true;
|
||||
}
|
||||
@@ -347,7 +418,7 @@ public class ControllerHandler {
|
||||
// UI thread.
|
||||
try {
|
||||
Thread.sleep(ControllerHandler.MINIMUM_BUTTON_DOWN_TIME_MS);
|
||||
} catch (InterruptedException e) {}
|
||||
} catch (InterruptedException ignored) {}
|
||||
}
|
||||
|
||||
switch (keyCode) {
|
||||
@@ -424,7 +495,7 @@ public class ControllerHandler {
|
||||
|
||||
try {
|
||||
Thread.sleep(EMULATED_SELECT_UP_DELAY_MS);
|
||||
} catch (InterruptedException e) {}
|
||||
} catch (InterruptedException ignored) {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -442,7 +513,7 @@ public class ControllerHandler {
|
||||
|
||||
try {
|
||||
Thread.sleep(EMULATED_SPECIAL_UP_DELAY_MS);
|
||||
} catch (InterruptedException e) {}
|
||||
} catch (InterruptedException ignored) {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -456,7 +527,7 @@ public class ControllerHandler {
|
||||
return false;
|
||||
}
|
||||
|
||||
keyCode = handleRemapping(mapping, keyCode);
|
||||
keyCode = handleRemapping(mapping, event);
|
||||
if (keyCode == 0) {
|
||||
return true;
|
||||
}
|
||||
@@ -566,5 +637,6 @@ public class ControllerHandler {
|
||||
public float hatYDeadzone;
|
||||
|
||||
public boolean isDualShock4;
|
||||
public boolean isXboxController;
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -65,7 +65,7 @@ public class TouchContext {
|
||||
// do input detection by polling
|
||||
try {
|
||||
Thread.sleep(100);
|
||||
} catch (InterruptedException e) {}
|
||||
} catch (InterruptedException ignored) {}
|
||||
|
||||
// Raise the mouse button
|
||||
conn.sendMouseButtonUp(buttonIndex);
|
||||
+10
@@ -8,6 +8,7 @@ public class EvdevEvent {
|
||||
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;
|
||||
@@ -18,6 +19,15 @@ public class EvdevEvent {
|
||||
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;
|
||||
+39
-6
@@ -10,6 +10,7 @@ public class EvdevHandler {
|
||||
private String absolutePath;
|
||||
private EvdevListener listener;
|
||||
private boolean shutdown = false;
|
||||
private int fd = -1;
|
||||
|
||||
private Thread handlerThread = new Thread() {
|
||||
@Override
|
||||
@@ -19,16 +20,17 @@ public class EvdevHandler {
|
||||
// system-wide input problems.
|
||||
|
||||
// Open the /dev/input/eventX file
|
||||
int fd = EvdevReader.open(absolutePath);
|
||||
fd = EvdevReader.open(absolutePath);
|
||||
if (fd == -1) {
|
||||
LimeLog.warning("Unable to open "+absolutePath);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Check if it's a mouse
|
||||
if (!EvdevReader.isMouse(fd)) {
|
||||
// We only handle mice
|
||||
// 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;
|
||||
}
|
||||
|
||||
@@ -38,7 +40,7 @@ public class EvdevHandler {
|
||||
return;
|
||||
}
|
||||
|
||||
LimeLog.info("Grabbed device for raw mouse input: "+absolutePath);
|
||||
LimeLog.info("Grabbed device for raw keyboard/mouse input: "+absolutePath);
|
||||
|
||||
ByteBuffer buffer = ByteBuffer.allocate(EvdevEvent.EVDEV_MAX_EVENT_SIZE).order(ByteOrder.nativeOrder());
|
||||
|
||||
@@ -96,7 +98,31 @@ public class EvdevHandler {
|
||||
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 {
|
||||
@@ -120,12 +146,19 @@ public class EvdevHandler {
|
||||
}
|
||||
|
||||
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 e) {}
|
||||
} catch (InterruptedException ignored) {}
|
||||
}
|
||||
|
||||
public void notifyDeleted() {
|
||||
+1
@@ -8,4 +8,5 @@ public interface EvdevListener {
|
||||
public void mouseMove(int deltaX, int deltaY);
|
||||
public void mouseButtonEvent(int buttonId, boolean down);
|
||||
public void mouseScroll(byte amount);
|
||||
public void keyboardEvent(boolean buttonDown, short keyCode);
|
||||
}
|
||||
+24
-4
@@ -3,6 +3,7 @@ package com.limelight.binding.input.evdev;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.Locale;
|
||||
|
||||
import com.limelight.LimeLog;
|
||||
|
||||
@@ -12,7 +13,7 @@ public class EvdevReader {
|
||||
}
|
||||
|
||||
// Requires root to chmod /dev/input/eventX
|
||||
public static boolean setPermissions(String[] files, int octalPermissions) {
|
||||
public static boolean setPermissions(String[] files, int octalPermissions) {
|
||||
ProcessBuilder builder = new ProcessBuilder("su");
|
||||
|
||||
try {
|
||||
@@ -20,13 +21,14 @@ public class EvdevReader {
|
||||
|
||||
OutputStream stdin = p.getOutputStream();
|
||||
for (String file : files) {
|
||||
stdin.write(String.format("chmod %o %s\n", octalPermissions, file).getBytes("UTF-8"));
|
||||
stdin.write(String.format((Locale)null, "chmod %o %s\n", octalPermissions, file).getBytes("UTF-8"));
|
||||
}
|
||||
stdin.write("exit\n".getBytes("UTF-8"));
|
||||
stdin.flush();
|
||||
|
||||
p.waitFor();
|
||||
p.destroy();
|
||||
return true;
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
} catch (InterruptedException e) {
|
||||
@@ -43,8 +45,26 @@ public class EvdevReader {
|
||||
public static native boolean grab(int fd);
|
||||
public static native boolean ungrab(int fd);
|
||||
|
||||
// Returns true if the device is a mouse
|
||||
public static native boolean isMouse(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);
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
+43
-3
@@ -2,18 +2,21 @@ 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 HashMap<String, EvdevHandler> handlers = new HashMap<String, EvdevHandler>();
|
||||
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;
|
||||
|
||||
@@ -42,7 +45,11 @@ public class EvdevWatcher {
|
||||
}
|
||||
|
||||
EvdevHandler handler = new EvdevHandler(PATH + "/" + fileName, listener);
|
||||
handler.start();
|
||||
|
||||
// If we're ungrabbed now, don't start the handler
|
||||
if (!ungrabbed) {
|
||||
handler.start();
|
||||
}
|
||||
|
||||
handlers.put(fileName, handler);
|
||||
}
|
||||
@@ -67,6 +74,9 @@ public class EvdevWatcher {
|
||||
// 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];
|
||||
@@ -78,6 +88,31 @@ public class EvdevWatcher {
|
||||
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
|
||||
@@ -119,10 +154,15 @@ public class EvdevWatcher {
|
||||
// Stop the observer
|
||||
observer.stopWatching();
|
||||
|
||||
synchronized (handlers) {
|
||||
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();
|
||||
+3
-1
@@ -18,6 +18,7 @@ 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 implements VideoDecoderRenderer {
|
||||
|
||||
private Thread rendererThread;
|
||||
@@ -38,6 +39,7 @@ public class AndroidCpuDecoderRenderer implements VideoDecoderRenderer {
|
||||
|
||||
private int cpuCount = Runtime.getRuntime().availableProcessors();
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
private int findOptimalPerformanceLevel() {
|
||||
StringBuilder cpuInfo = new StringBuilder();
|
||||
BufferedReader br = null;
|
||||
@@ -93,7 +95,7 @@ public class AndroidCpuDecoderRenderer implements VideoDecoderRenderer {
|
||||
public boolean setup(int width, int height, int redrawRate, Object renderTarget, int drFlags) {
|
||||
this.targetFps = redrawRate;
|
||||
|
||||
int perfLevel = findOptimalPerformanceLevel();
|
||||
int perfLevel = LOW_PERF; //findOptimalPerformanceLevel();
|
||||
int threadCount;
|
||||
|
||||
int avcFlags = 0;
|
||||
+1
-1
@@ -25,7 +25,7 @@ public class ConfigurableDecoderRenderer implements VideoDecoderRenderer {
|
||||
public void initializeWithFlags(int drFlags) {
|
||||
if ((drFlags & VideoDecoderRenderer.FLAG_FORCE_HARDWARE_DECODING) != 0 ||
|
||||
((drFlags & VideoDecoderRenderer.FLAG_FORCE_SOFTWARE_DECODING) == 0 &&
|
||||
MediaCodecDecoderRenderer.findProbableSafeDecoder() != null)) {
|
||||
MediaCodecHelper.findProbableSafeDecoder() != null)) {
|
||||
decoderRenderer = new MediaCodecDecoderRenderer();
|
||||
}
|
||||
else {
|
||||
@@ -0,0 +1,540 @@
|
||||
package com.limelight.binding.video;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.Locale;
|
||||
import java.util.concurrent.locks.LockSupport;
|
||||
|
||||
import org.jcodec.codecs.h264.io.model.SeqParameterSet;
|
||||
import org.jcodec.codecs.h264.io.model.VUIParameters;
|
||||
|
||||
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 android.annotation.TargetApi;
|
||||
import android.media.MediaCodec;
|
||||
import android.media.MediaCodecInfo;
|
||||
import android.media.MediaFormat;
|
||||
import android.media.MediaCodec.BufferInfo;
|
||||
import android.media.MediaCodec.CodecException;
|
||||
import android.os.Build;
|
||||
import android.view.SurfaceHolder;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public class MediaCodecDecoderRenderer implements VideoDecoderRenderer {
|
||||
|
||||
private ByteBuffer[] videoDecoderInputBuffers;
|
||||
private MediaCodec videoDecoder;
|
||||
private Thread rendererThread;
|
||||
private boolean needsSpsBitstreamFixup, isExynos4;
|
||||
private VideoDepacketizer depacketizer;
|
||||
private boolean adaptivePlayback;
|
||||
private int initialWidth, initialHeight;
|
||||
|
||||
private long lastTimestampUs;
|
||||
private long totalTimeMs;
|
||||
private long decoderTimeMs;
|
||||
private int totalFrames;
|
||||
|
||||
private String decoderName;
|
||||
private int numSpsIn;
|
||||
private int numPpsIn;
|
||||
private int numIframeIn;
|
||||
|
||||
private static final boolean ENABLE_ASYNC_RENDERER = false;
|
||||
|
||||
@TargetApi(Build.VERSION_CODES.KITKAT)
|
||||
public MediaCodecDecoderRenderer() {
|
||||
//dumpDecoders();
|
||||
|
||||
MediaCodecInfo decoder = MediaCodecHelper.findProbableSafeDecoder();
|
||||
if (decoder == null) {
|
||||
decoder = MediaCodecHelper.findFirstDecoder();
|
||||
}
|
||||
if (decoder == null) {
|
||||
// This case is handled later in setup()
|
||||
return;
|
||||
}
|
||||
|
||||
decoderName = decoder.getName();
|
||||
|
||||
// Set decoder-specific attributes
|
||||
adaptivePlayback = MediaCodecHelper.decoderSupportsAdaptivePlayback(decoderName, decoder);
|
||||
needsSpsBitstreamFixup = MediaCodecHelper.decoderNeedsSpsBitstreamRestrictions(decoderName, decoder);
|
||||
if (needsSpsBitstreamFixup) {
|
||||
LimeLog.info("Decoder "+decoderName+" needs SPS bitstream restrictions fixup");
|
||||
}
|
||||
isExynos4 = MediaCodecHelper.isExynos4Device();
|
||||
if (isExynos4) {
|
||||
LimeLog.info("Decoder "+decoderName+" is on Exynos 4");
|
||||
}
|
||||
}
|
||||
|
||||
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
|
||||
@Override
|
||||
public boolean setup(int width, int height, int redrawRate, Object renderTarget, int drFlags) {
|
||||
this.initialWidth = width;
|
||||
this.initialHeight = height;
|
||||
|
||||
if (decoderName == null) {
|
||||
LimeLog.severe("No available hardware decoder!");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Codecs have been known to throw all sorts of crazy runtime exceptions
|
||||
// due to implementation problems
|
||||
try {
|
||||
videoDecoder = MediaCodec.createByCodecName(decoderName);
|
||||
} catch (Exception e) {
|
||||
return false;
|
||||
}
|
||||
|
||||
MediaFormat videoFormat = MediaFormat.createVideoFormat("video/avc", width, height);
|
||||
|
||||
// Adaptive playback can also be enabled by the whitelist on pre-KitKat devices
|
||||
// so we don't fill these pre-KitKat
|
||||
if (adaptivePlayback && Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
|
||||
videoFormat.setInteger(MediaFormat.KEY_MAX_WIDTH, width);
|
||||
videoFormat.setInteger(MediaFormat.KEY_MAX_HEIGHT, height);
|
||||
}
|
||||
|
||||
// On Lollipop, we use asynchronous mode to avoid having a busy looping renderer thread
|
||||
if (ENABLE_ASYNC_RENDERER && Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
videoDecoder.setCallback(new MediaCodec.Callback() {
|
||||
@Override
|
||||
public void onOutputFormatChanged(MediaCodec codec, MediaFormat format) {
|
||||
LimeLog.info("Output format changed");
|
||||
LimeLog.info("New output Format: " + format);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onOutputBufferAvailable(MediaCodec codec, int index,
|
||||
BufferInfo info) {
|
||||
try {
|
||||
// FIXME: It looks like we can't frameskip here
|
||||
codec.releaseOutputBuffer(index, true);
|
||||
} catch (Exception e) {
|
||||
handleDecoderException(MediaCodecDecoderRenderer.this, e, null, 0);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onInputBufferAvailable(MediaCodec codec, int index) {
|
||||
try {
|
||||
submitDecodeUnit(depacketizer.takeNextDecodeUnit(), codec.getInputBuffer(index), index);
|
||||
} catch (InterruptedException e) {
|
||||
// What do we do here?
|
||||
e.printStackTrace();
|
||||
} catch (Exception e) {
|
||||
handleDecoderException(MediaCodecDecoderRenderer.this, e, null, 0);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(MediaCodec codec, CodecException e) {
|
||||
if (e.isTransient()) {
|
||||
LimeLog.warning(e.getDiagnosticInfo());
|
||||
e.printStackTrace();
|
||||
}
|
||||
else {
|
||||
LimeLog.severe(e.getDiagnosticInfo());
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
videoDecoder.configure(videoFormat, ((SurfaceHolder)renderTarget).getSurface(), null, 0);
|
||||
videoDecoder.setVideoScalingMode(MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT);
|
||||
|
||||
LimeLog.info("Using hardware decoding");
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
|
||||
private void handleDecoderException(MediaCodecDecoderRenderer dr, Exception e, ByteBuffer buf, int codecFlags) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
if (e instanceof CodecException) {
|
||||
CodecException codecExc = (CodecException) e;
|
||||
|
||||
if (codecExc.isTransient()) {
|
||||
// We'll let transient exceptions go
|
||||
LimeLog.warning(codecExc.getDiagnosticInfo());
|
||||
return;
|
||||
}
|
||||
|
||||
LimeLog.severe(codecExc.getDiagnosticInfo());
|
||||
}
|
||||
}
|
||||
|
||||
if (buf != null || codecFlags != 0) {
|
||||
throw new RendererException(dr, e, buf, codecFlags);
|
||||
}
|
||||
else {
|
||||
throw new RendererException(dr, e);
|
||||
}
|
||||
}
|
||||
|
||||
private void startRendererThread()
|
||||
{
|
||||
rendererThread = new Thread() {
|
||||
@SuppressWarnings("deprecation")
|
||||
@Override
|
||||
public void run() {
|
||||
BufferInfo info = new BufferInfo();
|
||||
DecodeUnit du = null;
|
||||
int inputIndex = -1;
|
||||
while (!isInterrupted())
|
||||
{
|
||||
// In order to get as much data to the decoder as early as possible,
|
||||
// try to submit up to 5 decode units at once without blocking.
|
||||
if (inputIndex == -1 && du == null) {
|
||||
try {
|
||||
for (int i = 0; i < 5; i++) {
|
||||
inputIndex = videoDecoder.dequeueInputBuffer(0);
|
||||
du = depacketizer.pollNextDecodeUnit();
|
||||
|
||||
// Stop if we can't get a DU or input buffer
|
||||
if (du == null || inputIndex == -1) {
|
||||
break;
|
||||
}
|
||||
|
||||
submitDecodeUnit(du, videoDecoderInputBuffers[inputIndex], inputIndex);
|
||||
|
||||
du = null;
|
||||
inputIndex = -1;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
inputIndex = -1;
|
||||
handleDecoderException(MediaCodecDecoderRenderer.this, e, null, 0);
|
||||
}
|
||||
}
|
||||
|
||||
// Grab an input buffer if we don't have one already.
|
||||
// This way we can have one ready hopefully by the time
|
||||
// the depacketizer is done with this frame. It's important
|
||||
// that this can timeout because it's possible that we could exhaust
|
||||
// the decoder's input buffers and deadlocks because aren't pulling
|
||||
// frames out of the other end.
|
||||
if (inputIndex == -1) {
|
||||
try {
|
||||
// If we've got a DU waiting to be given to the decoder,
|
||||
// wait a full 3 ms for an input buffer. Otherwise
|
||||
// just see if we can get one immediately.
|
||||
inputIndex = videoDecoder.dequeueInputBuffer(du != null ? 3000 : 0);
|
||||
} catch (Exception e) {
|
||||
inputIndex = -1;
|
||||
handleDecoderException(MediaCodecDecoderRenderer.this, e, null, 0);
|
||||
}
|
||||
}
|
||||
|
||||
// Grab a decode unit if we don't have one already
|
||||
if (du == null) {
|
||||
du = depacketizer.pollNextDecodeUnit();
|
||||
}
|
||||
|
||||
// If we've got both a decode unit and an input buffer, we'll
|
||||
// submit now. Otherwise, we wait until we have one.
|
||||
if (du != null && inputIndex >= 0) {
|
||||
submitDecodeUnit(du, videoDecoderInputBuffers[inputIndex], inputIndex);
|
||||
|
||||
// DU and input buffer have both been consumed
|
||||
du = null;
|
||||
inputIndex = -1;
|
||||
}
|
||||
|
||||
// Try to output a frame
|
||||
try {
|
||||
int outIndex = videoDecoder.dequeueOutputBuffer(info, 0);
|
||||
|
||||
if (outIndex >= 0) {
|
||||
long presentationTimeUs = info.presentationTimeUs;
|
||||
int lastIndex = outIndex;
|
||||
|
||||
// Get the last output buffer in the queue
|
||||
while ((outIndex = videoDecoder.dequeueOutputBuffer(info, 0)) >= 0) {
|
||||
videoDecoder.releaseOutputBuffer(lastIndex, false);
|
||||
lastIndex = outIndex;
|
||||
presentationTimeUs = info.presentationTimeUs;
|
||||
}
|
||||
|
||||
// Render the last buffer
|
||||
videoDecoder.releaseOutputBuffer(lastIndex, true);
|
||||
|
||||
// Add delta time to the totals (excluding probable outliers)
|
||||
long delta = System.currentTimeMillis()-(presentationTimeUs/1000);
|
||||
if (delta > 5 && delta < 300) {
|
||||
decoderTimeMs += delta;
|
||||
totalTimeMs += delta;
|
||||
}
|
||||
} else {
|
||||
switch (outIndex) {
|
||||
case MediaCodec.INFO_TRY_AGAIN_LATER:
|
||||
// Getting an input buffer may already block
|
||||
// so don't park if we still need to do that
|
||||
if (inputIndex >= 0) {
|
||||
LockSupport.parkNanos(1);
|
||||
}
|
||||
break;
|
||||
case MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED:
|
||||
LimeLog.info("Output buffers changed");
|
||||
break;
|
||||
case MediaCodec.INFO_OUTPUT_FORMAT_CHANGED:
|
||||
LimeLog.info("Output format changed");
|
||||
LimeLog.info("New output Format: " + videoDecoder.getOutputFormat());
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
handleDecoderException(MediaCodecDecoderRenderer.this, e, null, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
rendererThread.setName("Video - Renderer (MediaCodec)");
|
||||
rendererThread.setPriority(Thread.MAX_PRIORITY);
|
||||
rendererThread.start();
|
||||
}
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
@Override
|
||||
public boolean start(VideoDepacketizer depacketizer) {
|
||||
this.depacketizer = depacketizer;
|
||||
|
||||
// Start the decoder
|
||||
videoDecoder.start();
|
||||
|
||||
// On devices pre-Lollipop, we'll use a rendering thread
|
||||
if (!ENABLE_ASYNC_RENDERER || Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
|
||||
videoDecoderInputBuffers = videoDecoder.getInputBuffers();
|
||||
startRendererThread();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop() {
|
||||
if (rendererThread != null) {
|
||||
// Halt the rendering thread
|
||||
rendererThread.interrupt();
|
||||
try {
|
||||
rendererThread.join();
|
||||
} catch (InterruptedException ignored) { }
|
||||
}
|
||||
|
||||
// Stop the decoder
|
||||
videoDecoder.stop();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void release() {
|
||||
if (videoDecoder != null) {
|
||||
videoDecoder.release();
|
||||
}
|
||||
}
|
||||
|
||||
private void queueInputBuffer(int inputBufferIndex, int offset, int length, long timestampUs, int codecFlags) {
|
||||
// Try 25 times to submit the input buffer before throwing a real exception
|
||||
int i;
|
||||
Exception lastException = null;
|
||||
|
||||
for (i = 0; i < 25; i++) {
|
||||
try {
|
||||
videoDecoder.queueInputBuffer(inputBufferIndex,
|
||||
0, length,
|
||||
timestampUs, codecFlags);
|
||||
break;
|
||||
} catch (Exception e) {
|
||||
handleDecoderException(this, e, null, codecFlags);
|
||||
lastException = e;
|
||||
}
|
||||
}
|
||||
|
||||
if (i == 25) {
|
||||
throw new RendererException(this, lastException, null, codecFlags);
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
private void submitDecodeUnit(DecodeUnit decodeUnit, ByteBuffer buf, int inputBufferIndex) {
|
||||
long currentTime = System.currentTimeMillis();
|
||||
long delta = currentTime-decodeUnit.getReceiveTimestamp();
|
||||
if (delta >= 0 && delta < 300) {
|
||||
totalTimeMs += currentTime-decodeUnit.getReceiveTimestamp();
|
||||
totalFrames++;
|
||||
}
|
||||
|
||||
long timestampUs = currentTime * 1000;
|
||||
if (timestampUs <= lastTimestampUs) {
|
||||
// We can't submit multiple buffers with the same timestamp
|
||||
// so bump it up by one before queuing
|
||||
timestampUs = lastTimestampUs + 1;
|
||||
}
|
||||
lastTimestampUs = timestampUs;
|
||||
|
||||
// Clear old input data
|
||||
buf.clear();
|
||||
|
||||
int codecFlags = 0;
|
||||
int decodeUnitFlags = decodeUnit.getFlags();
|
||||
if ((decodeUnitFlags & DecodeUnit.DU_FLAG_CODEC_CONFIG) != 0) {
|
||||
codecFlags |= MediaCodec.BUFFER_FLAG_CODEC_CONFIG;
|
||||
}
|
||||
if ((decodeUnitFlags & DecodeUnit.DU_FLAG_SYNC_FRAME) != 0) {
|
||||
codecFlags |= MediaCodec.BUFFER_FLAG_SYNC_FRAME;
|
||||
numIframeIn++;
|
||||
}
|
||||
|
||||
if ((decodeUnitFlags & DecodeUnit.DU_FLAG_CODEC_CONFIG) != 0) {
|
||||
ByteBufferDescriptor header = decodeUnit.getBufferList().get(0);
|
||||
if (header.data[header.offset+4] == 0x67) {
|
||||
numSpsIn++;
|
||||
|
||||
ByteBuffer spsBuf = ByteBuffer.wrap(header.data);
|
||||
|
||||
// Skip to the start of the NALU data
|
||||
spsBuf.position(header.offset+5);
|
||||
|
||||
SeqParameterSet sps = SeqParameterSet.read(spsBuf);
|
||||
|
||||
// TI OMAP4 requires a reference frame count of 1 to decode successfully. Exynos 4
|
||||
// also requires this fixup.
|
||||
//
|
||||
// I'm doing this fixup for all devices because I haven't seen any devices that
|
||||
// this causes issues for. At worst, it seems to do nothing and at best it fixes
|
||||
// issues with video lag, hangs, and crashes.
|
||||
LimeLog.info("Patching num_ref_frames in SPS");
|
||||
sps.num_ref_frames = 1;
|
||||
|
||||
if (needsSpsBitstreamFixup || isExynos4) {
|
||||
// The SPS that comes in the current H264 bytestream doesn't set bitstream_restriction_flag
|
||||
// or max_dec_frame_buffering which increases decoding latency on Tegra.
|
||||
LimeLog.info("Adding bitstream restrictions");
|
||||
|
||||
sps.vuiParams.bitstreamRestriction = new VUIParameters.BitstreamRestriction();
|
||||
sps.vuiParams.bitstreamRestriction.motion_vectors_over_pic_boundaries_flag = true;
|
||||
sps.vuiParams.bitstreamRestriction.max_bytes_per_pic_denom = 2;
|
||||
sps.vuiParams.bitstreamRestriction.max_bits_per_mb_denom = 1;
|
||||
sps.vuiParams.bitstreamRestriction.log2_max_mv_length_horizontal = 16;
|
||||
sps.vuiParams.bitstreamRestriction.log2_max_mv_length_vertical = 16;
|
||||
sps.vuiParams.bitstreamRestriction.num_reorder_frames = 0;
|
||||
sps.vuiParams.bitstreamRestriction.max_dec_frame_buffering = 1;
|
||||
}
|
||||
|
||||
// Write the annex B header
|
||||
buf.put(header.data, header.offset, 5);
|
||||
|
||||
// Write the modified SPS to the input buffer
|
||||
sps.write(buf);
|
||||
|
||||
queueInputBuffer(inputBufferIndex,
|
||||
0, buf.position(),
|
||||
timestampUs, codecFlags);
|
||||
|
||||
depacketizer.freeDecodeUnit(decodeUnit);
|
||||
return;
|
||||
} else if (header.data[header.offset+4] == 0x68) {
|
||||
numPpsIn++;
|
||||
}
|
||||
}
|
||||
|
||||
// Copy data from our buffer list into the input buffer
|
||||
for (ByteBufferDescriptor desc : decodeUnit.getBufferList())
|
||||
{
|
||||
buf.put(desc.data, desc.offset, desc.length);
|
||||
}
|
||||
|
||||
queueInputBuffer(inputBufferIndex,
|
||||
0, decodeUnit.getDataLength(),
|
||||
timestampUs, codecFlags);
|
||||
|
||||
depacketizer.freeDecodeUnit(decodeUnit);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getCapabilities() {
|
||||
return adaptivePlayback ?
|
||||
VideoDecoderRenderer.CAPABILITY_ADAPTIVE_RESOLUTION : 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getAverageDecoderLatency() {
|
||||
if (totalFrames == 0) {
|
||||
return 0;
|
||||
}
|
||||
return (int)(decoderTimeMs / totalFrames);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getAverageEndToEndLatency() {
|
||||
if (totalFrames == 0) {
|
||||
return 0;
|
||||
}
|
||||
return (int)(totalTimeMs / totalFrames);
|
||||
}
|
||||
|
||||
public class RendererException extends RuntimeException {
|
||||
private static final long serialVersionUID = 8985937536997012406L;
|
||||
|
||||
private Exception originalException;
|
||||
private MediaCodecDecoderRenderer renderer;
|
||||
private ByteBuffer currentBuffer;
|
||||
private int currentCodecFlags;
|
||||
|
||||
public RendererException(MediaCodecDecoderRenderer renderer, Exception e) {
|
||||
this.renderer = renderer;
|
||||
this.originalException = e;
|
||||
}
|
||||
|
||||
public RendererException(MediaCodecDecoderRenderer renderer, Exception e, ByteBuffer currentBuffer, int currentCodecFlags) {
|
||||
this.renderer = renderer;
|
||||
this.originalException = e;
|
||||
this.currentBuffer = currentBuffer;
|
||||
this.currentCodecFlags = currentCodecFlags;
|
||||
}
|
||||
|
||||
public String toString() {
|
||||
String str = "";
|
||||
|
||||
str += "Decoder: "+renderer.decoderName+"\n";
|
||||
str += "Initial video dimensions: "+renderer.initialWidth+"x"+renderer.initialHeight+"\n";
|
||||
str += "In stats: "+renderer.numSpsIn+", "+renderer.numPpsIn+", "+renderer.numIframeIn+"\n";
|
||||
str += "Total frames: "+renderer.totalFrames+"\n";
|
||||
|
||||
if (currentBuffer != null) {
|
||||
str += "Current buffer: ";
|
||||
currentBuffer.flip();
|
||||
while (currentBuffer.hasRemaining() && currentBuffer.position() < 10) {
|
||||
str += String.format((Locale)null, "%02x ", currentBuffer.get());
|
||||
}
|
||||
str += "\n";
|
||||
str += "Buffer codec flags: "+currentCodecFlags+"\n";
|
||||
}
|
||||
|
||||
str += "Is Exynos 4: "+renderer.isExynos4+"\n";
|
||||
|
||||
str += "/proc/cpuinfo:\n";
|
||||
try {
|
||||
str += MediaCodecHelper.readCpuinfo();
|
||||
} catch (Exception e) {
|
||||
str += e.getMessage();
|
||||
}
|
||||
|
||||
str += "Full decoder dump:\n";
|
||||
try {
|
||||
str += MediaCodecHelper.dumpDecoders();
|
||||
} catch (Exception e) {
|
||||
str += e.getMessage();
|
||||
}
|
||||
|
||||
str += originalException.toString();
|
||||
|
||||
return str;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,300 @@
|
||||
package com.limelight.binding.video;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.File;
|
||||
import java.io.FileReader;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.annotation.TargetApi;
|
||||
import android.media.MediaCodecInfo;
|
||||
import android.media.MediaCodecList;
|
||||
import android.media.MediaCodecInfo.CodecCapabilities;
|
||||
import android.media.MediaCodecInfo.CodecProfileLevel;
|
||||
import android.os.Build;
|
||||
|
||||
import com.limelight.LimeLog;
|
||||
|
||||
public class MediaCodecHelper {
|
||||
|
||||
public static final List<String> preferredDecoders;
|
||||
|
||||
public static final List<String> blacklistedDecoderPrefixes;
|
||||
public static final List<String> spsFixupBitstreamFixupDecoderPrefixes;
|
||||
public static final List<String> whitelistedAdaptiveResolutionPrefixes;
|
||||
|
||||
static {
|
||||
preferredDecoders = new LinkedList<String>();
|
||||
}
|
||||
|
||||
static {
|
||||
blacklistedDecoderPrefixes = new LinkedList<String>();
|
||||
|
||||
// Software decoders that don't support H264 high profile
|
||||
blacklistedDecoderPrefixes.add("omx.google");
|
||||
blacklistedDecoderPrefixes.add("AVCDecoder");
|
||||
}
|
||||
|
||||
static {
|
||||
spsFixupBitstreamFixupDecoderPrefixes = new LinkedList<String>();
|
||||
spsFixupBitstreamFixupDecoderPrefixes.add("omx.nvidia");
|
||||
spsFixupBitstreamFixupDecoderPrefixes.add("omx.qcom");
|
||||
spsFixupBitstreamFixupDecoderPrefixes.add("omx.mtk");
|
||||
|
||||
whitelistedAdaptiveResolutionPrefixes = new LinkedList<String>();
|
||||
whitelistedAdaptiveResolutionPrefixes.add("omx.nvidia");
|
||||
whitelistedAdaptiveResolutionPrefixes.add("omx.qcom");
|
||||
whitelistedAdaptiveResolutionPrefixes.add("omx.sec");
|
||||
whitelistedAdaptiveResolutionPrefixes.add("omx.TI");
|
||||
}
|
||||
|
||||
private static boolean isDecoderInList(List<String> decoderList, String decoderName) {
|
||||
for (String badPrefix : decoderList) {
|
||||
if (decoderName.length() >= badPrefix.length()) {
|
||||
String prefix = decoderName.substring(0, badPrefix.length());
|
||||
if (prefix.equalsIgnoreCase(badPrefix)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@TargetApi(Build.VERSION_CODES.KITKAT)
|
||||
public static boolean decoderSupportsAdaptivePlayback(String decoderName, MediaCodecInfo decoderInfo) {
|
||||
if (isDecoderInList(whitelistedAdaptiveResolutionPrefixes, decoderName)) {
|
||||
LimeLog.info("Adaptive playback supported (whitelist)");
|
||||
return true;
|
||||
}
|
||||
|
||||
// Possibly enable adaptive playback on KitKat and above
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
|
||||
try {
|
||||
if (decoderInfo.getCapabilitiesForType("video/avc").
|
||||
isFeatureSupported(CodecCapabilities.FEATURE_AdaptivePlayback))
|
||||
{
|
||||
// This will make getCapabilities() return that adaptive playback is supported
|
||||
LimeLog.info("Adaptive playback supported (FEATURE_AdaptivePlayback)");
|
||||
return true;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// Tolerate buggy codecs
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public static boolean decoderNeedsSpsBitstreamRestrictions(String decoderName, MediaCodecInfo decoderInfo) {
|
||||
return isDecoderInList(spsFixupBitstreamFixupDecoderPrefixes, decoderName);
|
||||
}
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
@SuppressLint("NewApi")
|
||||
private static LinkedList<MediaCodecInfo> getMediaCodecList() {
|
||||
LinkedList<MediaCodecInfo> infoList = new LinkedList<MediaCodecInfo>();
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
MediaCodecList mcl = new MediaCodecList(MediaCodecList.REGULAR_CODECS);
|
||||
Collections.addAll(infoList, mcl.getCodecInfos());
|
||||
}
|
||||
else {
|
||||
for (int i = 0; i < MediaCodecList.getCodecCount(); i++) {
|
||||
infoList.add(MediaCodecList.getCodecInfoAt(i));
|
||||
}
|
||||
}
|
||||
|
||||
return infoList;
|
||||
}
|
||||
|
||||
@SuppressWarnings("RedundantThrows")
|
||||
public static String dumpDecoders() throws Exception {
|
||||
String str = "";
|
||||
for (MediaCodecInfo codecInfo : getMediaCodecList()) {
|
||||
// Skip encoders
|
||||
if (codecInfo.isEncoder()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
str += "Decoder: "+codecInfo.getName()+"\n";
|
||||
for (String type : codecInfo.getSupportedTypes()) {
|
||||
str += "\t"+type+"\n";
|
||||
CodecCapabilities caps = codecInfo.getCapabilitiesForType(type);
|
||||
|
||||
for (CodecProfileLevel profile : caps.profileLevels) {
|
||||
str += "\t\t"+profile.profile+" "+profile.level+"\n";
|
||||
}
|
||||
}
|
||||
}
|
||||
return str;
|
||||
}
|
||||
|
||||
public static MediaCodecInfo findPreferredDecoder() {
|
||||
// This is a different algorithm than the other findXXXDecoder functions,
|
||||
// because we want to evaluate the decoders in our list's order
|
||||
// rather than MediaCodecList's order
|
||||
|
||||
for (String preferredDecoder : preferredDecoders) {
|
||||
for (MediaCodecInfo codecInfo : getMediaCodecList()) {
|
||||
// Skip encoders
|
||||
if (codecInfo.isEncoder()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check for preferred decoders
|
||||
if (preferredDecoder.equalsIgnoreCase(codecInfo.getName())) {
|
||||
LimeLog.info("Preferred decoder choice is "+codecInfo.getName());
|
||||
return codecInfo;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public static MediaCodecInfo findFirstDecoder() {
|
||||
for (MediaCodecInfo codecInfo : getMediaCodecList()) {
|
||||
// Skip encoders
|
||||
if (codecInfo.isEncoder()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check for explicitly blacklisted decoders
|
||||
if (isDecoderInList(blacklistedDecoderPrefixes, codecInfo.getName())) {
|
||||
LimeLog.info("Skipping blacklisted decoder: "+codecInfo.getName());
|
||||
continue;
|
||||
}
|
||||
|
||||
// Find a decoder that supports H.264
|
||||
for (String mime : codecInfo.getSupportedTypes()) {
|
||||
if (mime.equalsIgnoreCase("video/avc")) {
|
||||
LimeLog.info("First decoder choice is "+codecInfo.getName());
|
||||
return codecInfo;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public static MediaCodecInfo findProbableSafeDecoder() {
|
||||
// First look for a preferred decoder by name
|
||||
MediaCodecInfo info = findPreferredDecoder();
|
||||
if (info != null) {
|
||||
return info;
|
||||
}
|
||||
|
||||
// Now look for decoders we know are safe
|
||||
try {
|
||||
// If this function completes, it will determine if the decoder is safe
|
||||
return findKnownSafeDecoder();
|
||||
} catch (Exception e) {
|
||||
// Some buggy devices seem to throw exceptions
|
||||
// from getCapabilitiesForType() so we'll just assume
|
||||
// they're okay and go with the first one we find
|
||||
return findFirstDecoder();
|
||||
}
|
||||
}
|
||||
|
||||
// We declare this method as explicitly throwing Exception
|
||||
// since some bad decoders can throw IllegalArgumentExceptions unexpectedly
|
||||
// and we want to be sure all callers are handling this possibility
|
||||
@SuppressWarnings("RedundantThrows")
|
||||
public static MediaCodecInfo findKnownSafeDecoder() throws Exception {
|
||||
for (MediaCodecInfo codecInfo : getMediaCodecList()) {
|
||||
// Skip encoders
|
||||
if (codecInfo.isEncoder()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check for explicitly blacklisted decoders
|
||||
if (isDecoderInList(blacklistedDecoderPrefixes, codecInfo.getName())) {
|
||||
LimeLog.info("Skipping blacklisted decoder: "+codecInfo.getName());
|
||||
continue;
|
||||
}
|
||||
|
||||
// Find a decoder that supports H.264 high profile
|
||||
for (String mime : codecInfo.getSupportedTypes()) {
|
||||
if (mime.equalsIgnoreCase("video/avc")) {
|
||||
LimeLog.info("Examining decoder capabilities of "+codecInfo.getName());
|
||||
|
||||
CodecCapabilities caps = codecInfo.getCapabilitiesForType(mime);
|
||||
for (CodecProfileLevel profile : caps.profileLevels) {
|
||||
if (profile.profile == CodecProfileLevel.AVCProfileHigh) {
|
||||
LimeLog.info("Decoder "+codecInfo.getName()+" supports high profile");
|
||||
LimeLog.info("Selected decoder: "+codecInfo.getName());
|
||||
return codecInfo;
|
||||
}
|
||||
}
|
||||
|
||||
LimeLog.info("Decoder "+codecInfo.getName()+" does NOT support high profile");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public static String readCpuinfo() throws Exception {
|
||||
StringBuilder cpuInfo = new StringBuilder();
|
||||
BufferedReader br = new BufferedReader(new FileReader(new File("/proc/cpuinfo")));
|
||||
try {
|
||||
for (;;) {
|
||||
int ch = br.read();
|
||||
if (ch == -1)
|
||||
break;
|
||||
cpuInfo.append((char)ch);
|
||||
}
|
||||
|
||||
return cpuInfo.toString();
|
||||
} finally {
|
||||
br.close();
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean stringContainsIgnoreCase(String string, String substring) {
|
||||
return string.toLowerCase(Locale.ENGLISH).contains(substring.toLowerCase(Locale.ENGLISH));
|
||||
}
|
||||
|
||||
public static boolean isExynos4Device() {
|
||||
try {
|
||||
// Try reading CPU info too look for
|
||||
String cpuInfo = readCpuinfo();
|
||||
|
||||
// SMDK4xxx is Exynos 4
|
||||
if (stringContainsIgnoreCase(cpuInfo, "SMDK4")) {
|
||||
LimeLog.info("Found SMDK4 in /proc/cpuinfo");
|
||||
return true;
|
||||
}
|
||||
|
||||
// If we see "Exynos 4" also we'll count it
|
||||
if (stringContainsIgnoreCase(cpuInfo, "Exynos 4")) {
|
||||
LimeLog.info("Found Exynos 4 in /proc/cpuinfo");
|
||||
return true;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
try {
|
||||
File systemDir = new File("/sys/devices/system");
|
||||
File[] files = systemDir.listFiles();
|
||||
if (files != null) {
|
||||
for (File f : files) {
|
||||
if (stringContainsIgnoreCase(f.getName(), "exynos4")) {
|
||||
LimeLog.info("Found exynos4 in /sys/devices/system");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
+2
-1
@@ -4,6 +4,7 @@ import java.net.InetAddress;
|
||||
import java.net.UnknownHostException;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.UUID;
|
||||
|
||||
import com.limelight.LimeLog;
|
||||
@@ -44,7 +45,7 @@ public class ComputerDatabaseManager {
|
||||
|
||||
private void initializeDb() {
|
||||
// Create tables if they aren't already there
|
||||
computerDb.execSQL(String.format("CREATE TABLE IF NOT EXISTS %s(%s TEXT PRIMARY KEY," +
|
||||
computerDb.execSQL(String.format((Locale)null, "CREATE TABLE IF NOT EXISTS %s(%s TEXT PRIMARY KEY," +
|
||||
" %s TEXT NOT NULL, %s TEXT NOT NULL, %s TEXT NOT NULL, %s TEXT NOT NULL)",
|
||||
COMPUTER_TABLE_NAME,
|
||||
COMPUTER_NAME_COLUMN_NAME, COMPUTER_UUID_COLUMN_NAME, LOCAL_IP_COLUMN_NAME,
|
||||
+128
-202
@@ -1,11 +1,7 @@
|
||||
package com.limelight.computers;
|
||||
|
||||
import java.net.Inet4Address;
|
||||
import java.net.InetAddress;
|
||||
import java.net.InterfaceAddress;
|
||||
import java.net.NetworkInterface;
|
||||
import java.net.SocketException;
|
||||
import java.util.Enumeration;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Timer;
|
||||
import java.util.TimerTask;
|
||||
@@ -24,16 +20,12 @@ import com.limelight.nvstream.mdns.MdnsDiscoveryListener;
|
||||
|
||||
import android.app.Service;
|
||||
import android.content.ComponentName;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.ServiceConnection;
|
||||
import android.net.ConnectivityManager;
|
||||
import android.net.NetworkInfo;
|
||||
import android.os.Binder;
|
||||
import android.os.IBinder;
|
||||
|
||||
public class ComputerManagerService extends Service {
|
||||
private static final int MAX_CONCURRENT_REQUESTS = 4;
|
||||
private static final int POLLING_PERIOD_MS = 5000;
|
||||
private static final int MDNS_QUERY_PERIOD_MS = 1000;
|
||||
|
||||
@@ -43,12 +35,12 @@ public class ComputerManagerService extends Service {
|
||||
private AtomicInteger dbRefCount = new AtomicInteger(0);
|
||||
|
||||
private IdentityManager idManager;
|
||||
private ThreadPoolExecutor pollingPool;
|
||||
private Timer pollingTimer;
|
||||
private HashMap<ComputerDetails, Thread> pollingThreads;
|
||||
private ComputerManagerListener listener = null;
|
||||
private AtomicInteger activePolls = new AtomicInteger(0);
|
||||
|
||||
private DiscoveryService.DiscoveryBinder discoveryBinder;
|
||||
private ServiceConnection discoveryServiceConnection = new ServiceConnection() {
|
||||
private final ServiceConnection discoveryServiceConnection = new ServiceConnection() {
|
||||
public void onServiceConnected(ComponentName className, IBinder binder) {
|
||||
synchronized (discoveryServiceConnection) {
|
||||
DiscoveryService.DiscoveryBinder privateBinder = ((DiscoveryService.DiscoveryBinder)binder);
|
||||
@@ -66,6 +58,79 @@ public class ComputerManagerService extends Service {
|
||||
discoveryBinder = null;
|
||||
}
|
||||
};
|
||||
|
||||
// Returns true if the details object was modified
|
||||
private boolean runPoll(ComputerDetails details)
|
||||
{
|
||||
boolean newPc = (details.name == null);
|
||||
|
||||
if (!getLocalDatabaseReference()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
activePolls.incrementAndGet();
|
||||
|
||||
// Poll the machine
|
||||
if (!doPollMachine(details)) {
|
||||
details.state = ComputerDetails.State.OFFLINE;
|
||||
details.reachability = ComputerDetails.Reachability.OFFLINE;
|
||||
}
|
||||
|
||||
activePolls.decrementAndGet();
|
||||
|
||||
// If it's online, update our persistent state
|
||||
if (details.state == ComputerDetails.State.ONLINE) {
|
||||
if (!newPc) {
|
||||
// Check if it's in the database because it could have been
|
||||
// removed after this was issued
|
||||
if (dbManager.getComputerByName(details.name) == null) {
|
||||
// It's gone
|
||||
releaseLocalDatabaseReference();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
dbManager.updateComputer(details);
|
||||
}
|
||||
|
||||
// Don't call the listener if this is a failed lookup of a new PC
|
||||
if ((!newPc || details.state == ComputerDetails.State.ONLINE) && listener != null) {
|
||||
listener.notifyComputerUpdated(details);
|
||||
}
|
||||
|
||||
releaseLocalDatabaseReference();
|
||||
return true;
|
||||
}
|
||||
|
||||
private Thread createPollingThread(final ComputerDetails details) {
|
||||
Thread t = new Thread() {
|
||||
@Override
|
||||
public void run() {
|
||||
while (!isInterrupted()) {
|
||||
ComputerDetails originalDetails = new ComputerDetails();
|
||||
originalDetails.update(details);
|
||||
|
||||
// Check if this poll has modified the details
|
||||
if (runPoll(details) && !originalDetails.equals(details)) {
|
||||
// Replace our thread entry with the new one
|
||||
synchronized (pollingThreads) {
|
||||
pollingThreads.remove(originalDetails);
|
||||
pollingThreads.put(details, this);
|
||||
}
|
||||
}
|
||||
|
||||
// Wait until the next polling interval
|
||||
try {
|
||||
Thread.sleep(POLLING_PERIOD_MS);
|
||||
} catch (InterruptedException e) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
t.setName("Polling thread for "+details.localIp.getHostAddress());
|
||||
return t;
|
||||
}
|
||||
|
||||
public class ComputerManagerBinder extends Binder {
|
||||
public void startPolling(ComputerManagerListener listener) {
|
||||
@@ -76,8 +141,22 @@ public class ComputerManagerService extends Service {
|
||||
discoveryBinder.startDiscovery(MDNS_QUERY_PERIOD_MS);
|
||||
|
||||
// Start polling known machines
|
||||
pollingTimer = new Timer();
|
||||
pollingTimer.schedule(getTimerTask(), 0, POLLING_PERIOD_MS);
|
||||
if (!getLocalDatabaseReference()) {
|
||||
return;
|
||||
}
|
||||
List<ComputerDetails> computerList = dbManager.getAllComputers();
|
||||
releaseLocalDatabaseReference();
|
||||
|
||||
synchronized (pollingThreads) {
|
||||
for (ComputerDetails computer : computerList) {
|
||||
// This polling thread might already be there
|
||||
if (!pollingThreads.containsKey(computer)) {
|
||||
Thread t = createPollingThread(computer);
|
||||
pollingThreads.put(computer, t);
|
||||
t.start();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void waitForReady() {
|
||||
@@ -87,11 +166,19 @@ public class ComputerManagerService extends Service {
|
||||
// Wait for the bind notification
|
||||
discoveryServiceConnection.wait(1000);
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
} catch (InterruptedException ignored) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void waitForPollingStopped() {
|
||||
while (activePolls.get() != 0) {
|
||||
try {
|
||||
Thread.sleep(250);
|
||||
} catch (InterruptedException ignored) {}
|
||||
}
|
||||
}
|
||||
|
||||
public boolean addComputerBlocking(InetAddress addr) {
|
||||
return ComputerManagerService.this.addComputerBlocking(addr);
|
||||
}
|
||||
@@ -120,10 +207,12 @@ public class ComputerManagerService extends Service {
|
||||
discoveryBinder.stopDiscovery();
|
||||
|
||||
// Stop polling
|
||||
if (pollingTimer != null) {
|
||||
pollingTimer.cancel();
|
||||
pollingTimer = null;
|
||||
}
|
||||
synchronized (pollingThreads) {
|
||||
for (Thread t : pollingThreads.values()) {
|
||||
t.interrupt();
|
||||
}
|
||||
pollingThreads.clear();
|
||||
}
|
||||
|
||||
// Remove the listener
|
||||
listener = null;
|
||||
@@ -151,17 +240,24 @@ public class ComputerManagerService extends Service {
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
public void addComputer(InetAddress addr) {
|
||||
// Setup a placeholder
|
||||
ComputerDetails fakeDetails = new ComputerDetails();
|
||||
fakeDetails.localIp = addr;
|
||||
fakeDetails.remoteIp = addr;
|
||||
|
||||
// Put it in the thread pool to process later
|
||||
pollingPool.execute(getPollingRunnable(fakeDetails));
|
||||
|
||||
// Spawn a thread for this computer
|
||||
synchronized (pollingThreads) {
|
||||
// This polling thread might already be there
|
||||
if (!pollingThreads.containsKey(fakeDetails)) {
|
||||
Thread t = createPollingThread(fakeDetails);
|
||||
pollingThreads.put(fakeDetails, t);
|
||||
t.start();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public boolean addComputerBlocking(InetAddress addr) {
|
||||
// Setup a placeholder
|
||||
ComputerDetails fakeDetails = new ComputerDetails();
|
||||
@@ -169,7 +265,7 @@ public class ComputerManagerService extends Service {
|
||||
fakeDetails.remoteIp = addr;
|
||||
|
||||
// Block while we try to fill the details
|
||||
getPollingRunnable(fakeDetails).run();
|
||||
runPoll(fakeDetails);
|
||||
|
||||
// If the machine is reachable, it was successful
|
||||
return fakeDetails.state == ComputerDetails.State.ONLINE;
|
||||
@@ -201,115 +297,11 @@ public class ComputerManagerService extends Service {
|
||||
}
|
||||
}
|
||||
|
||||
private TimerTask getTimerTask() {
|
||||
return new TimerTask() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (!getLocalDatabaseReference()) {
|
||||
return;
|
||||
}
|
||||
List<ComputerDetails> computerList = dbManager.getAllComputers();
|
||||
releaseLocalDatabaseReference();
|
||||
|
||||
for (ComputerDetails computer : computerList) {
|
||||
pollingPool.execute(getPollingRunnable(computer));
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private int getActiveNetworkType() {
|
||||
ConnectivityManager connMgr = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE);
|
||||
NetworkInfo activeNetworkInfo = connMgr.getActiveNetworkInfo();
|
||||
if (activeNetworkInfo == null) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
return activeNetworkInfo.getType();
|
||||
}
|
||||
|
||||
private InterfaceAddress getActiveInterfaceAddress() {
|
||||
String matchingPrefix;
|
||||
|
||||
switch (getActiveNetworkType())
|
||||
{
|
||||
case ConnectivityManager.TYPE_ETHERNET:
|
||||
matchingPrefix = "eth";
|
||||
break;
|
||||
case ConnectivityManager.TYPE_WIFI:
|
||||
matchingPrefix = "wlan";
|
||||
break;
|
||||
|
||||
default:
|
||||
// Must be on Ethernet or Wifi to consider that we can send large packets
|
||||
return null;
|
||||
}
|
||||
|
||||
// Try to find the interface that corresponds to the active network
|
||||
try {
|
||||
Enumeration<NetworkInterface> ifaceList = NetworkInterface.getNetworkInterfaces();
|
||||
while (ifaceList.hasMoreElements()) {
|
||||
NetworkInterface iface = ifaceList.nextElement();
|
||||
|
||||
// Look for an interface that matches the prefix we expect
|
||||
if (iface.isUp() && iface.getName().startsWith(matchingPrefix)) {
|
||||
// Find the IPv4 address for the interface
|
||||
for (InterfaceAddress addr : iface.getInterfaceAddresses()) {
|
||||
if (!(addr.getAddress() instanceof Inet4Address)) {
|
||||
// Skip non-IPv4 addresses
|
||||
continue;
|
||||
}
|
||||
|
||||
// Found the right address on the right interface
|
||||
return addr;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (SocketException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
// We didn't find the interface or something else went wrong
|
||||
return null;
|
||||
}
|
||||
|
||||
private boolean isOnSameSubnet(InetAddress targetAddress, InetAddress localAddress, short networkPrefixLength) {
|
||||
byte[] targetBytes = targetAddress.getAddress();
|
||||
byte[] localBytes = localAddress.getAddress();
|
||||
|
||||
for (int byteIndex = 0; networkPrefixLength > 0; byteIndex++) {
|
||||
byte target = targetBytes[byteIndex];
|
||||
byte local = localBytes[byteIndex];
|
||||
|
||||
if (networkPrefixLength >= 8) {
|
||||
// Do a full byte comparison
|
||||
if (target != local) {
|
||||
return false;
|
||||
}
|
||||
|
||||
networkPrefixLength -= 8;
|
||||
}
|
||||
else {
|
||||
target &= (byte)(0xFF << (8 - networkPrefixLength));
|
||||
local &= (byte)(0xFF << (8 - networkPrefixLength));
|
||||
|
||||
// Do a masked comparison
|
||||
if (target != local) {
|
||||
return false;
|
||||
}
|
||||
|
||||
networkPrefixLength = 0;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private ComputerDetails tryPollIp(InetAddress ipAddr) {
|
||||
try {
|
||||
NvHTTP http = new NvHTTP(ipAddr, idManager.getUniqueId(),
|
||||
null, PlatformBinding.getCryptoProvider(ComputerManagerService.this));
|
||||
|
||||
|
||||
return http.getComputerDetails();
|
||||
} catch (Exception e) {
|
||||
return null;
|
||||
@@ -326,7 +318,7 @@ public class ComputerManagerService extends Service {
|
||||
polledDetails = tryPollIp(details.remoteIp);
|
||||
}
|
||||
|
||||
if (polledDetails == null) {
|
||||
if (polledDetails == null && !details.localIp.equals(details.remoteIp)) {
|
||||
// Failed, so let's try the fallback
|
||||
if (!localFirst) {
|
||||
polledDetails = tryPollIp(details.localIp);
|
||||
@@ -340,7 +332,8 @@ public class ComputerManagerService extends Service {
|
||||
polledDetails.reachability = !localFirst ? ComputerDetails.Reachability.LOCAL :
|
||||
ComputerDetails.Reachability.REMOTE;
|
||||
}
|
||||
} else {
|
||||
}
|
||||
else if (polledDetails != null) {
|
||||
polledDetails.reachability = localFirst ? ComputerDetails.Reachability.LOCAL :
|
||||
ComputerDetails.Reachability.REMOTE;
|
||||
}
|
||||
@@ -356,68 +349,7 @@ public class ComputerManagerService extends Service {
|
||||
}
|
||||
|
||||
private boolean doPollMachine(ComputerDetails details) {
|
||||
// Get the network type
|
||||
int networkType = getActiveNetworkType();
|
||||
switch (networkType) {
|
||||
// We'll check local first on these if we find
|
||||
// we're on the same subnet
|
||||
case ConnectivityManager.TYPE_ETHERNET:
|
||||
case ConnectivityManager.TYPE_WIFI:
|
||||
InterfaceAddress ifaceAddr = getActiveInterfaceAddress();
|
||||
if (ifaceAddr != null) {
|
||||
if (isOnSameSubnet(details.localIp, ifaceAddr.getAddress(), ifaceAddr.getNetworkPrefixLength())) {
|
||||
// It's on the same subnet, so poll local first
|
||||
LimeLog.info("Machine looks local; trying local IP first");
|
||||
return pollComputer(details, true);
|
||||
}
|
||||
}
|
||||
// Fall through to remote first
|
||||
default:
|
||||
LimeLog.info("Machine looks remote; trying remote IP first");
|
||||
return pollComputer(details, false);
|
||||
}
|
||||
}
|
||||
|
||||
private Runnable getPollingRunnable(final ComputerDetails details) {
|
||||
return new Runnable() {
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
boolean newPc = (details.name == null);
|
||||
|
||||
if (!getLocalDatabaseReference()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Poll the machine
|
||||
if (!doPollMachine(details)) {
|
||||
details.state = ComputerDetails.State.OFFLINE;
|
||||
details.reachability = ComputerDetails.Reachability.OFFLINE;
|
||||
}
|
||||
|
||||
// If it's online, update our persistent state
|
||||
if (details.state == ComputerDetails.State.ONLINE) {
|
||||
if (!newPc) {
|
||||
// Check if it's in the database because it could have been
|
||||
// removed after this was issued
|
||||
if (dbManager.getComputerByName(details.name) == null) {
|
||||
// It's gone
|
||||
releaseLocalDatabaseReference();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
dbManager.updateComputer(details);
|
||||
}
|
||||
|
||||
// Don't call the listener if this is a failed lookup of a new PC
|
||||
if ((!newPc || details.state == ComputerDetails.State.ONLINE) && listener != null) {
|
||||
listener.notifyComputerUpdated(details);
|
||||
}
|
||||
|
||||
releaseLocalDatabaseReference();
|
||||
}
|
||||
};
|
||||
return pollComputer(details, true);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -425,11 +357,8 @@ public class ComputerManagerService extends Service {
|
||||
// Bind to the discovery service
|
||||
bindService(new Intent(this, DiscoveryService.class),
|
||||
discoveryServiceConnection, Service.BIND_AUTO_CREATE);
|
||||
|
||||
// Create the thread pool for updating computer state
|
||||
pollingPool = new ThreadPoolExecutor(MAX_CONCURRENT_REQUESTS, MAX_CONCURRENT_REQUESTS,
|
||||
Long.MAX_VALUE, TimeUnit.DAYS, new LinkedBlockingQueue<Runnable>(),
|
||||
new ThreadPoolExecutor.DiscardPolicy());
|
||||
|
||||
pollingThreads = new HashMap<ComputerDetails, Thread>();
|
||||
|
||||
// Lookup or generate this device's UID
|
||||
idManager = new IdentityManager(this);
|
||||
@@ -446,9 +375,6 @@ public class ComputerManagerService extends Service {
|
||||
unbindService(discoveryServiceConnection);
|
||||
}
|
||||
|
||||
// Stop the thread pool
|
||||
pollingPool.shutdownNow();
|
||||
|
||||
// FIXME: Should await termination here but we have timeout issues in HttpURLConnection
|
||||
|
||||
// Remove the initial DB reference
|
||||
+4
-3
@@ -4,6 +4,7 @@ import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.OutputStreamWriter;
|
||||
import java.util.Locale;
|
||||
import java.util.Random;
|
||||
|
||||
import com.limelight.LimeLog;
|
||||
@@ -53,7 +54,7 @@ public class IdentityManager {
|
||||
if (reader != null) {
|
||||
try {
|
||||
reader.close();
|
||||
} catch (IOException e) {}
|
||||
} catch (IOException ignored) {}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -61,7 +62,7 @@ public class IdentityManager {
|
||||
private static String generateNewUniqueId(Context c) {
|
||||
// Generate a new UID hex string
|
||||
LimeLog.info("Generating new UID");
|
||||
String uidStr = String.format("%016x", new Random().nextLong());
|
||||
String uidStr = String.format((Locale)null, "%016x", new Random().nextLong());
|
||||
|
||||
OutputStreamWriter writer = null;
|
||||
try {
|
||||
@@ -75,7 +76,7 @@ public class IdentityManager {
|
||||
if (writer != null) {
|
||||
try {
|
||||
writer.close();
|
||||
} catch (IOException e) {}
|
||||
} catch (IOException ignored) {}
|
||||
}
|
||||
}
|
||||
|
||||
+3
-2
@@ -1,10 +1,11 @@
|
||||
package com.limelight;
|
||||
package com.limelight.preferences;
|
||||
|
||||
import java.net.InetAddress;
|
||||
import java.net.UnknownHostException;
|
||||
import java.util.concurrent.LinkedBlockingQueue;
|
||||
|
||||
import com.limelight.computers.ComputerManagerService;
|
||||
import com.limelight.R;
|
||||
import com.limelight.utils.Dialog;
|
||||
|
||||
import android.app.Activity;
|
||||
@@ -97,7 +98,7 @@ public class AddComputerManually extends Activity {
|
||||
|
||||
try {
|
||||
addThread.join();
|
||||
} catch (InterruptedException e) {}
|
||||
} catch (InterruptedException ignored) {}
|
||||
|
||||
addThread = null;
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
package com.limelight.preferences;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.preference.PreferenceManager;
|
||||
|
||||
public class PreferenceConfiguration {
|
||||
static final String RES_FPS_PREF_STRING = "list_resolution_fps";
|
||||
private static final String DECODER_PREF_STRING = "list_decoders";
|
||||
static final String BITRATE_PREF_STRING = "seekbar_bitrate";
|
||||
private static final String STRETCH_PREF_STRING = "checkbox_stretch_video";
|
||||
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 HOST_AUDIO_PREF_STRING = "checkbox_host_audio";
|
||||
|
||||
private static final int BITRATE_DEFAULT_720_30 = 5;
|
||||
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 = 30;
|
||||
|
||||
private static final String DEFAULT_RES_FPS = "720p60";
|
||||
private static final String DEFAULT_DECODER = "auto";
|
||||
private static final int DEFAULT_BITRATE = BITRATE_DEFAULT_720_60;
|
||||
private static final boolean DEFAULT_STRETCH = false;
|
||||
private static final boolean DEFAULT_SOPS = true;
|
||||
private static final boolean DEFAULT_DISABLE_TOASTS = false;
|
||||
private static final boolean DEFAULT_HOST_AUDIO = false;
|
||||
|
||||
public static final int FORCE_HARDWARE_DECODER = -1;
|
||||
public static final int AUTOSELECT_DECODER = 0;
|
||||
public static final int FORCE_SOFTWARE_DECODER = 1;
|
||||
|
||||
public int width, height, fps;
|
||||
public int bitrate;
|
||||
public int decoder;
|
||||
public boolean stretchVideo, enableSops, playHostAudio, disableWarnings;
|
||||
|
||||
public static int getDefaultBitrate(String resFpsString) {
|
||||
if (resFpsString.equals("720p30")) {
|
||||
return BITRATE_DEFAULT_720_30;
|
||||
}
|
||||
else if (resFpsString.equals("720p60")) {
|
||||
return BITRATE_DEFAULT_720_60;
|
||||
}
|
||||
else if (resFpsString.equals("1080p30")) {
|
||||
return BITRATE_DEFAULT_1080_30;
|
||||
}
|
||||
else if (resFpsString.equals("1080p60")) {
|
||||
return BITRATE_DEFAULT_1080_60;
|
||||
}
|
||||
else {
|
||||
// Should never get here
|
||||
return DEFAULT_BITRATE;
|
||||
}
|
||||
}
|
||||
|
||||
public static int getDefaultBitrate(Context context) {
|
||||
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
|
||||
|
||||
String str = prefs.getString(RES_FPS_PREF_STRING, DEFAULT_RES_FPS);
|
||||
if (str.equals("720p30")) {
|
||||
return BITRATE_DEFAULT_720_30;
|
||||
}
|
||||
else if (str.equals("720p60")) {
|
||||
return BITRATE_DEFAULT_720_60;
|
||||
}
|
||||
else if (str.equals("1080p30")) {
|
||||
return BITRATE_DEFAULT_1080_30;
|
||||
}
|
||||
else if (str.equals("1080p60")) {
|
||||
return BITRATE_DEFAULT_1080_60;
|
||||
}
|
||||
else {
|
||||
// Should never get here
|
||||
return DEFAULT_BITRATE;
|
||||
}
|
||||
}
|
||||
|
||||
private static int getDecoderValue(Context context) {
|
||||
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
|
||||
|
||||
String str = prefs.getString(DECODER_PREF_STRING, DEFAULT_DECODER);
|
||||
if (str.equals("auto")) {
|
||||
return AUTOSELECT_DECODER;
|
||||
}
|
||||
else if (str.equals("software")) {
|
||||
return FORCE_SOFTWARE_DECODER;
|
||||
}
|
||||
else if (str.equals("hardware")) {
|
||||
return FORCE_HARDWARE_DECODER;
|
||||
}
|
||||
else {
|
||||
// Should never get here
|
||||
return AUTOSELECT_DECODER;
|
||||
}
|
||||
}
|
||||
|
||||
public static PreferenceConfiguration readPreferences(Context context) {
|
||||
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
|
||||
PreferenceConfiguration config = new PreferenceConfiguration();
|
||||
|
||||
config.bitrate = prefs.getInt(BITRATE_PREF_STRING, getDefaultBitrate(context));
|
||||
String str = prefs.getString(RES_FPS_PREF_STRING, DEFAULT_RES_FPS);
|
||||
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 {
|
||||
// Should never get here
|
||||
config.width = 1280;
|
||||
config.height = 720;
|
||||
config.fps = 60;
|
||||
}
|
||||
|
||||
config.decoder = getDecoderValue(context);
|
||||
|
||||
// Checkbox preferences
|
||||
config.disableWarnings = prefs.getBoolean(DISABLE_TOASTS_PREF_STRING, DEFAULT_DISABLE_TOASTS);
|
||||
config.enableSops = prefs.getBoolean(SOPS_PREF_STRING, DEFAULT_SOPS);
|
||||
config.stretchVideo = prefs.getBoolean(STRETCH_PREF_STRING, DEFAULT_STRETCH);
|
||||
config.playHostAudio = prefs.getBoolean(HOST_AUDIO_PREF_STRING, DEFAULT_HOST_AUDIO);
|
||||
|
||||
return config;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,159 @@
|
||||
package com.limelight.preferences;
|
||||
|
||||
import android.app.AlertDialog;
|
||||
import android.content.Context;
|
||||
import android.os.Bundle;
|
||||
import android.preference.DialogPreference;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.Gravity;
|
||||
import android.view.View;
|
||||
import android.view.View.OnClickListener;
|
||||
import android.widget.Button;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.SeekBar;
|
||||
import android.widget.TextView;
|
||||
|
||||
// Based on a Stack Overflow example: http://stackoverflow.com/questions/1974193/slider-on-my-preferencescreen
|
||||
public class SeekBarPreference extends DialogPreference
|
||||
{
|
||||
private static final String SCHEMA_URL = "http://schemas.android.com/apk/res/android";
|
||||
|
||||
private SeekBar seekBar;
|
||||
private TextView splashText, valueText;
|
||||
private Context context;
|
||||
|
||||
private String dialogMessage, suffix;
|
||||
private int defaultValue, maxValue, currentValue;
|
||||
|
||||
public SeekBarPreference(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
this.context = context;
|
||||
|
||||
// Read the message from XML
|
||||
int dialogMessageId = attrs.getAttributeResourceValue(SCHEMA_URL, "dialogMessage", 0);
|
||||
if (dialogMessageId == 0) {
|
||||
dialogMessage = attrs.getAttributeValue(SCHEMA_URL, "dialogMessage");
|
||||
}
|
||||
else {
|
||||
dialogMessage = context.getString(dialogMessageId);
|
||||
}
|
||||
|
||||
// Get the suffix for the number displayed in the dialog
|
||||
int suffixId = attrs.getAttributeResourceValue(SCHEMA_URL, "text", 0);
|
||||
if (suffixId == 0) {
|
||||
suffix = attrs.getAttributeValue(SCHEMA_URL, "text");
|
||||
}
|
||||
else {
|
||||
suffix = context.getString(suffixId);
|
||||
}
|
||||
|
||||
// Get default and max seekbar values
|
||||
defaultValue = PreferenceConfiguration.getDefaultBitrate(context);
|
||||
maxValue = attrs.getAttributeIntValue(SCHEMA_URL, "max", 100);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected View onCreateDialogView() {
|
||||
|
||||
LinearLayout.LayoutParams params;
|
||||
LinearLayout layout = new LinearLayout(context);
|
||||
layout.setOrientation(LinearLayout.VERTICAL);
|
||||
layout.setPadding(6, 6, 6, 6);
|
||||
|
||||
splashText = new TextView(context);
|
||||
splashText.setPadding(30, 10, 30, 10);
|
||||
if (dialogMessage != null) {
|
||||
splashText.setText(dialogMessage);
|
||||
}
|
||||
layout.addView(splashText);
|
||||
|
||||
valueText = new TextView(context);
|
||||
valueText.setGravity(Gravity.CENTER_HORIZONTAL);
|
||||
valueText.setTextSize(32);
|
||||
params = new LinearLayout.LayoutParams(
|
||||
LinearLayout.LayoutParams.FILL_PARENT,
|
||||
LinearLayout.LayoutParams.WRAP_CONTENT);
|
||||
layout.addView(valueText, params);
|
||||
|
||||
seekBar = new SeekBar(context);
|
||||
seekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
|
||||
@Override
|
||||
public void onProgressChanged(SeekBar seekBar, int value, boolean b) {
|
||||
String t = String.valueOf(value);
|
||||
valueText.setText(suffix == null ? t : t.concat(" " + suffix));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStartTrackingTouch(SeekBar seekBar) {}
|
||||
|
||||
@Override
|
||||
public void onStopTrackingTouch(SeekBar seekBar) {}
|
||||
});
|
||||
|
||||
layout.addView(seekBar, new LinearLayout.LayoutParams(LinearLayout.LayoutParams.FILL_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT));
|
||||
|
||||
if (shouldPersist()) {
|
||||
currentValue = getPersistedInt(defaultValue);
|
||||
}
|
||||
|
||||
seekBar.setMax(maxValue);
|
||||
seekBar.setProgress(currentValue);
|
||||
|
||||
return layout;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onBindDialogView(View v) {
|
||||
super.onBindDialogView(v);
|
||||
seekBar.setMax(maxValue);
|
||||
seekBar.setProgress(currentValue);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onSetInitialValue(boolean restore, Object defaultValue)
|
||||
{
|
||||
super.onSetInitialValue(restore, defaultValue);
|
||||
if (restore) {
|
||||
currentValue = shouldPersist() ? getPersistedInt(this.defaultValue) : 0;
|
||||
}
|
||||
else {
|
||||
currentValue = (Integer) defaultValue;
|
||||
}
|
||||
}
|
||||
|
||||
public void setMax(int max) {
|
||||
this.maxValue = max;
|
||||
}
|
||||
public int getMax() {
|
||||
return this.maxValue;
|
||||
}
|
||||
|
||||
public void setProgress(int progress) {
|
||||
this.currentValue = progress;
|
||||
if (seekBar != null) {
|
||||
seekBar.setProgress(progress);
|
||||
}
|
||||
}
|
||||
public int getProgress() {
|
||||
return currentValue;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void showDialog(Bundle state) {
|
||||
super.showDialog(state);
|
||||
|
||||
Button positiveButton = ((AlertDialog) getDialog()).getButton(AlertDialog.BUTTON_POSITIVE);
|
||||
positiveButton.setOnClickListener(new OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View view) {
|
||||
if (shouldPersist()) {
|
||||
currentValue = seekBar.getProgress();
|
||||
persistInt(seekBar.getProgress());
|
||||
callChangeListener(Integer.valueOf(seekBar.getProgress()));
|
||||
}
|
||||
|
||||
((AlertDialog) getDialog()).dismiss();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package com.limelight.preferences;
|
||||
|
||||
import android.content.SharedPreferences;
|
||||
import android.os.Bundle;
|
||||
import android.app.Activity;
|
||||
import android.preference.Preference;
|
||||
import android.preference.PreferenceFragment;
|
||||
import android.preference.PreferenceManager;
|
||||
|
||||
import com.limelight.R;
|
||||
|
||||
public class StreamSettings extends Activity {
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
setContentView(R.layout.activity_stream_settings);
|
||||
getFragmentManager().beginTransaction().replace(
|
||||
R.id.stream_settings, new SettingsFragment()
|
||||
).commit();
|
||||
}
|
||||
|
||||
public static class SettingsFragment extends PreferenceFragment {
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
addPreferencesFromResource(R.xml.preferences);
|
||||
|
||||
// Add a listener to the FPS and resolution preference
|
||||
// so the bitrate can be auto-adjusted
|
||||
Preference pref = findPreference(PreferenceConfiguration.RES_FPS_PREF_STRING);
|
||||
pref.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
|
||||
@Override
|
||||
public boolean onPreferenceChange(Preference preference, Object newValue) {
|
||||
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(SettingsFragment.this.getActivity());
|
||||
String valueStr = (String) newValue;
|
||||
|
||||
// Write the new bitrate value
|
||||
prefs.edit()
|
||||
.putInt(PreferenceConfiguration.BITRATE_PREF_STRING,
|
||||
PreferenceConfiguration.getDefaultBitrate(valueStr))
|
||||
.apply();
|
||||
|
||||
// Allow the original preference change to take place
|
||||
return true;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
+2
-2
@@ -13,7 +13,7 @@ public class Dialog implements Runnable {
|
||||
|
||||
private AlertDialog alert;
|
||||
|
||||
private static ArrayList<Dialog> rundownDialogs = new ArrayList<Dialog>();
|
||||
private static final ArrayList<Dialog> rundownDialogs = new ArrayList<Dialog>();
|
||||
|
||||
public Dialog(Activity activity, String title, String message, boolean endAfterDismiss)
|
||||
{
|
||||
@@ -57,7 +57,7 @@ public class Dialog implements Runnable {
|
||||
alert.setButton(AlertDialog.BUTTON_NEUTRAL, "OK", new DialogInterface.OnClickListener() {
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
synchronized (rundownDialogs) {
|
||||
rundownDialogs.remove(this);
|
||||
rundownDialogs.remove(Dialog.this);
|
||||
alert.dismiss();
|
||||
}
|
||||
|
||||
+1
-1
@@ -14,7 +14,7 @@ public class SpinnerDialog implements Runnable,OnCancelListener {
|
||||
private ProgressDialog progress;
|
||||
private boolean finish;
|
||||
|
||||
private static ArrayList<SpinnerDialog> rundownDialogs = new ArrayList<SpinnerDialog>();
|
||||
private static final ArrayList<SpinnerDialog> rundownDialogs = new ArrayList<SpinnerDialog>();
|
||||
|
||||
public SpinnerDialog(Activity activity, String title, String message, boolean finish)
|
||||
{
|
||||
@@ -8,5 +8,6 @@ LOCAL_PATH := $(MY_LOCAL_PATH)
|
||||
include $(CLEAR_VARS)
|
||||
LOCAL_MODULE := evdev_reader
|
||||
LOCAL_SRC_FILES := evdev_reader.c
|
||||
LOCAL_LDLIBS := -llog
|
||||
|
||||
include $(BUILD_SHARED_LIBRARY)
|
||||
@@ -0,0 +1,118 @@
|
||||
#include <stdlib.h>
|
||||
#include <jni.h>
|
||||
|
||||
#include <sys/types.h>
|
||||
#include <sys/stat.h>
|
||||
#include <fcntl.h>
|
||||
#include <linux/input.h>
|
||||
#include <unistd.h>
|
||||
#include <poll.h>
|
||||
#include <errno.h>
|
||||
|
||||
#include <android/log.h>
|
||||
|
||||
JNIEXPORT jint JNICALL
|
||||
Java_com_limelight_binding_input_evdev_EvdevReader_open(JNIEnv *env, jobject this, jstring absolutePath) {
|
||||
const char *path;
|
||||
|
||||
path = (*env)->GetStringUTFChars(env, absolutePath, NULL);
|
||||
|
||||
return open(path, O_RDWR);
|
||||
}
|
||||
|
||||
JNIEXPORT jboolean JNICALL
|
||||
Java_com_limelight_binding_input_evdev_EvdevReader_grab(JNIEnv *env, jobject this, jint fd) {
|
||||
return ioctl(fd, EVIOCGRAB, 1) == 0;
|
||||
}
|
||||
|
||||
JNIEXPORT jboolean JNICALL
|
||||
Java_com_limelight_binding_input_evdev_EvdevReader_ungrab(JNIEnv *env, jobject this, jint fd) {
|
||||
return ioctl(fd, EVIOCGRAB, 0) == 0;
|
||||
}
|
||||
|
||||
// has*() and friends are based on Android's EventHub.cpp
|
||||
|
||||
#define test_bit(bit, array) (array[bit/8] & (1<<(bit%8)))
|
||||
|
||||
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];
|
||||
|
||||
ioctl(fd, EVIOCGBIT(EV_REL, sizeof(relBitmask)), relBitmask);
|
||||
|
||||
return test_bit(axis, relBitmask);
|
||||
}
|
||||
|
||||
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];
|
||||
|
||||
ioctl(fd, EVIOCGBIT(EV_KEY, sizeof(keyBitmask)), keyBitmask);
|
||||
|
||||
return test_bit(key, keyBitmask);
|
||||
}
|
||||
|
||||
JNIEXPORT jint JNICALL
|
||||
Java_com_limelight_binding_input_evdev_EvdevReader_read(JNIEnv *env, jobject this, jint fd, jbyteArray buffer) {
|
||||
jint ret;
|
||||
jbyte *data;
|
||||
int pollres;
|
||||
struct pollfd pollinfo;
|
||||
|
||||
data = (*env)->GetByteArrayElements(env, buffer, NULL);
|
||||
if (data == NULL) {
|
||||
__android_log_print(ANDROID_LOG_ERROR, "EvdevReader",
|
||||
"Failed to get byte array");
|
||||
return -1;
|
||||
}
|
||||
|
||||
do
|
||||
{
|
||||
// Unwait every 250 ms to return to caller if the fd is closed
|
||||
pollinfo.fd = 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(fd, data, sizeof(struct input_event));
|
||||
if (ret < 0) {
|
||||
__android_log_print(ANDROID_LOG_ERROR, "EvdevReader",
|
||||
"read() failed: %d", errno);
|
||||
}
|
||||
}
|
||||
else {
|
||||
// There must have been a failure
|
||||
ret = -1;
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
(*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);
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user